Renewal refreshes the existing license — no re-issue #3

Closed
opened 2026-07-29 22:32:57 +00:00 by Cordy · 1 comment
Owner

Today every transaction.completed mints a NEW license (new id, new key) — so a subscription renewal would create a second key the customer must re-paste into Cairn. Terrible UX; blocks monthly billing.

Design

  • Webhook matches the incoming transaction's subscription_id against existing records (paddleSub). Match found → refresh in place: extend expiresAt to max(now, old expiry) + billing period, re-sign the payload with the same license id, replace the stored key. No match (first purchase) → issue as today.
  • Seat changes on renewal (quantity up/down) update seats in the refreshed payload.
  • /v1/checkin response gains the current signed key: {status: "ok", key: "CAIRN1…"}. Since >50-seat instances check in monthly, a renewed key propagates to the instance automatically within one check-in cycle — zero manual re-pasting for exactly the instances that need a license.
  • Companion change in cairnd (Cairn repo issue): on a successful check-in whose key differs from the installed one, verify offline and install it.
  • Idempotency stays per-transaction (paddleTxn) so Paddle retries don't double-extend.

Acceptance: replay a renewal simulation for an existing subscription → same license id, later expiry, updated stored key; check-in returns the new key; cairnd (companion issue) picks it up; a fresh purchase still creates a new license.

Today every `transaction.completed` mints a NEW license (new id, new key) — so a subscription renewal would create a second key the customer must re-paste into Cairn. Terrible UX; blocks monthly billing. **Design** - Webhook matches the incoming transaction's `subscription_id` against existing records (`paddleSub`). Match found → **refresh in place**: extend `expiresAt` to max(now, old expiry) + billing period, re-sign the payload with the **same license id**, replace the stored key. No match (first purchase) → issue as today. - Seat changes on renewal (quantity up/down) update `seats` in the refreshed payload. - `/v1/checkin` response gains the current signed key: `{status: "ok", key: "CAIRN1…"}`. Since >50-seat instances check in monthly, a renewed key propagates to the instance automatically within one check-in cycle — **zero manual re-pasting** for exactly the instances that need a license. - Companion change in cairnd (Cairn repo issue): on a successful check-in whose `key` differs from the installed one, verify offline and install it. - Idempotency stays per-transaction (`paddleTxn`) so Paddle retries don't double-extend. **Acceptance:** replay a renewal simulation for an existing subscription → same license id, later expiry, updated stored key; check-in returns the new key; cairnd (companion issue) picks it up; a fresh purchase still creates a new license.
Author
Owner

Shipped and deployed (PR #7).

  • Webhook matches subscription_id against existing records: match → refresh() extends expiry one billing period past max(now, current expiry), updates seats, re-signs the same license id, replaces the stored key, records the new txn. No match → new license as before. Idempotency by PaddleTxn intact.
  • /v1/checkin now returns {status: "ok", key: <current>}, and Cairn v0.3.13 installs it automatically on the monthly check-in (same-license-id guard; Cairn#58). Customers on subscriptions never re-paste keys.

Covered by TestRefreshOnRenewal and TestCheckinReturnsCurrentKey.

Shipped and deployed (PR #7). - Webhook matches `subscription_id` against existing records: match → `refresh()` extends expiry one billing period past max(now, current expiry), updates seats, **re-signs the same license id**, replaces the stored key, records the new txn. No match → new license as before. Idempotency by PaddleTxn intact. - `/v1/checkin` now returns `{status: "ok", key: <current>}`, and Cairn v0.3.13 installs it automatically on the monthly check-in (same-license-id guard; Cairn#58). Customers on subscriptions never re-paste keys. Covered by `TestRefreshOnRenewal` and `TestCheckinReturnsCurrentKey`.
Cordy closed this issue 2026-07-29 23:17:17 +00:00
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#3
No description provided.