First 50 seats always free: bill only seats above the free tier #25

Closed
opened 2026-08-24 12:35:48 +00:00 by Cordy · 1 comment
Owner

Flag: today a 51-user instance is billed for all 51 seats. The free tier should stack — the first 50 seats of every license are included free, so 51 total seats bills exactly 1 paid seat.

Decision (Nikola, 2026-08-24): absolute presentation. Every user-facing surface keeps talking in TOTAL instance seats (min 51); only the Paddle boundary deals in paid seats.

Design — one conversion boundary:

  • Paddle prices/quantities carry paid seats (total − 50); the Paddle price quantity minimum drops from 51 to 1 (max 10000 → 9950).
  • The signed license key keeps carrying the total user cap, so Cairn enforcement needs no change at all (cap = key seats, free tier ≤50 unchanged).
  • paddleTransactionCompleted: after deriving quantity (line-item sum or absolute subscription lookup), seats += freeSeats when > 0.
  • resolveSeatChange: request stays total seats; validation floor becomes > freeSeats; subItemsBody sends seats − freeSeats to Paddle.
  • account.html: checkout quantity: paidSeats(seats); buy/quote totals price only paid seats; quotation shows the free-50 row as genuinely unbilled.
  • index.html pricing copy states the stacking rule.
  • Cairn docs/handbook/licensing-faq.md gains the billing sentence.

Note: any license issued before this change was billed for its full seat count; its next renewal/seat-change webhook re-signs it as quantity+50 (an effective +50 seat grant). Sandbox-only today — no live customers.

Flag: today a 51-user instance is billed for all 51 seats. The free tier should stack — the first 50 seats of every license are included free, so 51 total seats bills exactly 1 paid seat. **Decision (Nikola, 2026-08-24): absolute presentation.** Every user-facing surface keeps talking in TOTAL instance seats (min 51); only the Paddle boundary deals in paid seats. Design — one conversion boundary: - Paddle prices/quantities carry **paid seats** (`total − 50`); the Paddle price quantity minimum drops from 51 to 1 (max 10000 → 9950). - The signed license key keeps carrying the **total** user cap, so Cairn enforcement needs no change at all (cap = key seats, free tier ≤50 unchanged). - `paddleTransactionCompleted`: after deriving quantity (line-item sum or absolute subscription lookup), `seats += freeSeats` when > 0. - `resolveSeatChange`: request stays total seats; validation floor becomes `> freeSeats`; `subItemsBody` sends `seats − freeSeats` to Paddle. - account.html: checkout `quantity: paidSeats(seats)`; buy/quote totals price only paid seats; quotation shows the free-50 row as genuinely unbilled. - index.html pricing copy states the stacking rule. - Cairn `docs/handbook/licensing-faq.md` gains the billing sentence. Note: any license issued before this change was billed for its full seat count; its next renewal/seat-change webhook re-signs it as quantity+50 (an effective +50 seat grant). Sandbox-only today — no live customers.
Cordy referenced this issue from a commit 2026-08-24 12:36:46 +00:00
Cordy closed this issue 2026-08-24 12:42:51 +00:00
Author
Owner

Shipped and live.

  • Server: PR #26 squash-merged, deployed, healthz ok. Full suite green on the runner.
  • Site: cairn-site PR #13 merged and deployed (account.html 69,711 B / index.html 47,091 B behind the gate).
  • Paddle sandbox: both seat prices (Monthly (per seat) pri_01kyr0wc…, Yearly (per seat) pri_01kyfy4x…) updated quantity 51–10000 → 1–9950 via API. Repeat on the live prices at the #26-launch sandbox→live swap.
  • Cairn docs: licensing-faq.md gained the "How is the price calculated?" answer (a 51-seat license bills 1 seat; 120 bills 70). No Cairn code change — the key still carries the total cap.

Sandbox dogfood checklist (needs a browser + Paddle sandbox card):

  1. Buy 51 seats → checkout shows 1 × $3 (or $32.40 yearly), key mints with seats=51, dashboard shows 51 seats with the free-tier mark.
  2. Buy floor: try 50 in the seat input — client clamps to 51; Paddle itself now allows quantity 1, so the client/server floor is the guard.
  3. Change seats 51 → 120 → preview charges the prorated 69-seat delta; after confirm, webhook re-signs the key at 120 (subscription shows quantity 70).
  4. Change seats to 50 → 400 "seats must be above the free tier (minimum 51)".
  5. Quotation for 120 seats → priced row shows 70 paid seats, free-50 row at $0, recurring total 70 × per-seat.
  6. Pre-change license (dogfood): next renewal/seat-change re-signs at quantity+50 — expected +50 grant, sandbox only.
Shipped and live. - Server: PR #26 squash-merged, deployed, healthz ok. Full suite green on the runner. - Site: cairn-site PR #13 merged and deployed (account.html 69,711 B / index.html 47,091 B behind the gate). - Paddle sandbox: both seat prices (`Monthly (per seat)` pri_01kyr0wc…, `Yearly (per seat)` pri_01kyfy4x…) updated quantity 51–10000 → **1–9950** via API. **Repeat on the live prices at the #26-launch sandbox→live swap.** - Cairn docs: licensing-faq.md gained the "How is the price calculated?" answer (a 51-seat license bills 1 seat; 120 bills 70). No Cairn code change — the key still carries the total cap. Sandbox dogfood checklist (needs a browser + Paddle sandbox card): 1. Buy 51 seats → checkout shows 1 × $3 (or $32.40 yearly), key mints with seats=51, dashboard shows 51 seats with the free-tier mark. 2. Buy floor: try 50 in the seat input — client clamps to 51; Paddle itself now allows quantity 1, so the client/server floor is the guard. 3. Change seats 51 → 120 → preview charges the prorated 69-seat delta; after confirm, webhook re-signs the key at 120 (subscription shows quantity 70). 4. Change seats to 50 → 400 "seats must be above the free tier (minimum 51)". 5. Quotation for 120 seats → priced row shows 70 paid seats, free-50 row at $0, recurring total 70 × per-seat. 6. Pre-change license (dogfood): next renewal/seat-change re-signs at quantity+50 — expected +50 grant, sandbox only.
Cordy referenced this issue from a commit 2026-08-25 00:31:03 +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#25
No description provided.