Self-service seat changes: account-area "change seats" → Paddle subscription update #12

Closed
opened 2026-07-30 23:48:45 +00:00 by Cordy · 1 comment
Owner

Today there is no customer-facing path to grow a license from 55 to 65 seats. The account area's only commerce affordance is the buy flow, which starts a new checkout → new subscription → a second full-price license, while the old subscription keeps billing — and seats don't stack across keys in cairnd, so this is a trap. The only correct route is editing the subscription quantity in the Paddle seller dashboard by hand.

The underlying machinery is already right: a quantity change on the existing subscription produces a prorated transaction.completed on the same subscription_id, the webhook's refresh branch re-signs the same license id with the new seat count, and the instance auto-installs the refreshed key at its next check-in (Cairn#58). Only the trigger is missing.

Plan:

  • Server: POST /v1/licenses/{id}/seats {seats} (Bearer; org-admin; license must belong to the caller's org and be subscription-backed). Reads the subscription from Paddle, PATCHes its item quantity with proration_billing_mode: "prorated_immediately", returns 202 — the webhook then updates the license asynchronously. Extends the existing paddleAPI client with a write method.
  • Site: a "Change seats" control on subscription-backed licenses in account.html, with copy that makes the proration behaviour clear.
  • Blocked on key scope: the PADDLE_API_KEY on the VPS was deliberately created read-only (receipts). Subscription writes need Read+Write on Subscriptions — Nikola updates the key in the Paddle sandbox dashboard and on the VPS.

Depends on the origin fix (#11) landing first, so an upgrade transaction doesn't wrongly extend expiry.

Today there is no customer-facing path to grow a license from 55 to 65 seats. The account area's only commerce affordance is the buy flow, which starts a **new** checkout → new subscription → a second full-price license, while the old subscription keeps billing — and seats don't stack across keys in cairnd, so this is a trap. The only correct route is editing the subscription quantity in the Paddle seller dashboard by hand. The underlying machinery is already right: a quantity change on the existing subscription produces a prorated `transaction.completed` on the same `subscription_id`, the webhook's refresh branch re-signs the **same license id** with the new seat count, and the instance auto-installs the refreshed key at its next check-in (Cairn#58). Only the trigger is missing. Plan: - **Server:** `POST /v1/licenses/{id}/seats` `{seats}` (Bearer; org-admin; license must belong to the caller's org and be subscription-backed). Reads the subscription from Paddle, PATCHes its item quantity with `proration_billing_mode: "prorated_immediately"`, returns 202 — the webhook then updates the license asynchronously. Extends the existing `paddleAPI` client with a write method. - **Site:** a "Change seats" control on subscription-backed licenses in account.html, with copy that makes the proration behaviour clear. - **Blocked on key scope:** the `PADDLE_API_KEY` on the VPS was deliberately created read-only (receipts). Subscription writes need Read+Write on Subscriptions — Nikola updates the key in the Paddle sandbox dashboard and on the VPS. Depends on the origin fix (#11) landing first, so an upgrade transaction doesn't wrongly extend expiry.
Cordy closed this issue 2026-07-30 23:55:36 +00:00
Author
Owner

Shipped and deployed (PRs #13, #14, cairn-site #3; license server restarted healthy, site pushed). Remaining before this actually works end-to-end — both Nikola's:

  1. Paddle API key scope (sandbox dashboard → Developer tools → Authentication): the current key is read-only. It needs Subscriptions: Read + Write added (keep Transactions: Read for receipts). If sandbox keys aren't editable, create a new key with Transactions Read + Subscriptions Read/Write, replace PADDLE_API_KEY in /etc/cairn-license/env on the VPS, sudo systemctl restart cairn-license-server. The signing key and everything else stay untouched.
  2. Sandbox e2e: buy a small license via the sandbox checkout, then use Change seats on it and verify — Paddle charges the prorated difference, the webhook logs license refreshed, the license row shows the new seat count with an unchanged expiry, and a POST /v1/checkin returns the re-signed key.

Until (1) is done the endpoint answers 502 with a "billing provider rejected" message and logs the scope hint — nothing breaks, the button is just inert.

Shipped and deployed (PRs #13, #14, cairn-site #3; license server restarted healthy, site pushed). Remaining before this actually works end-to-end — both Nikola's: 1. **Paddle API key scope** (sandbox dashboard → Developer tools → Authentication): the current key is read-only. It needs **Subscriptions: Read + Write** added (keep Transactions: Read for receipts). If sandbox keys aren't editable, create a new key with Transactions Read + Subscriptions Read/Write, replace `PADDLE_API_KEY` in `/etc/cairn-license/env` on the VPS, `sudo systemctl restart cairn-license-server`. The signing key and everything else stay untouched. 2. **Sandbox e2e**: buy a small license via the sandbox checkout, then use Change seats on it and verify — Paddle charges the prorated difference, the webhook logs `license refreshed`, the license row shows the new seat count with an **unchanged expiry**, and a `POST /v1/checkin` returns the re-signed key. Until (1) is done the endpoint answers 502 with a "billing provider rejected" message and logs the scope hint — nothing breaks, the button is just inert.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/cairn-license-server#12
No description provided.