Seats
Seats
Cbox Billing separates what you pay for from who is using it. A seat is bought explicitly and then assigned to a specific member — membership changes never move the bill.
Purchased vs assigned
| Concept | What it is | Where it lives | Bills? |
|---|---|---|---|
| Purchased Full seats | The subscription's seat quantity — what the plan's per-seat price charges. | subscriptions.seats |
Yes — the only billing driver. |
| Assignment | One purchased Full seat handed to a specific eligible member. | seat_assignments (org, subject, source, assigned_at) |
No — it moves no money; it spends a purchased seat. |
| Eligibility | Which members may hold a seat. | cbox_id_access_grants (the access mirror the Cbox ID webhooks maintain) |
No. |
The load-bearing rule: purchased Full seats are the sole billing driver. An admin buys
or releases them explicitly, which calls the engine's own changeQuantity() — a prorated
charge (or credit), a re-established per-seat allotment, and an MRR movement. No membership
event ever changes the billed quantity.
Full vs Light
- Full — a member with a
seat_assignment. They hold one purchased Full seat and are billed as part of the subscription quantity. - Light — an eligible member (in the access mirror) without an assignment. Counted and displayed everywhere seat totals appear, but free.
The invariant
assigned count ≤ purchased seats
Enforced on every write:
- Assigning with no free seat is refused — buy more seats first.
- Releasing purchased seats below the assigned count is refused — unassign a member first. Purchased seats also never drop below one.
Removing a member (the organization.member_removed webhook) releases their assignment
so the seat can be reused, but does not reduce the purchased count — the organization
keeps the seat it paid for until an admin explicitly releases it.
The auto-assign toggle
By default assignment is manual: a joining member only becomes eligible (Light), and an
operator assigns a seat deliberately. Turning config('billing.seats.auto_assign') on (env
CBOX_BILLING_SEAT_AUTO_ASSIGN) makes a join self-service:
- On
member_added/role.assigned, if the member's role is inauto_assign_roles(defaultbilling-admin,billing-operator) and a purchased seat is free, the member is auto-assigned a seat (source = auto). - It never auto-buys and never exceeds the purchased cap — with no free seat the member simply stays Light.
- If a member's role later drops out of
auto_assign_roles, an auto-sourced seat is released; a manual seat is never auto-released.
Configuration
config/billing.php → seats:
'seats' => [
'types' => [
'full' => ['label' => 'Full', 'billable' => true], // the plan's per-seat price
'light' => ['label' => 'Light', 'billable' => false], // free today
],
'auto_assign' => (bool) env('CBOX_BILLING_SEAT_AUTO_ASSIGN', false),
'auto_assign_roles' => ['billing-admin', 'billing-operator'],
],
Light is free today. The types map is deliberately kept price-shaped so a priced
Light tier can be introduced later (add a price to the light type) without a breaking
change to the model, the reporting, or the API — Light stays free until then.
Surfaces
- Console — the subscription-detail Seats panel: the purchased count with Buy / Release controls (guardrailed against the assigned count), the assigned Full members with Assign / Unassign, the Light members list, and Full vs Light totals.
- Management API —
GET /subscriptions/{org}/seatsreads the breakdown;POST /subscriptions/{org}/seatssets the purchased count (buy/release);POST /subscriptions/{org}/seats/assignand.../seats/unassignmove a member between Full and Light. A refused invariant is a409. - Customer portal — the token-scoped hosted portal
lets the customer self-serve the same operations: preview the prorated due-now for a seat
buy/release then confirm, and assign / unassign their own members — driven through the same
SeatManagersemantics and cap guardrails (a refusal is a422). - Reporting — MRR reflects the purchased Full-seat count (you bill what is purchased); Full-billed vs Light-free counts are surfaced wherever seats appear.
Related
- Subscriptions & lifecycle — the quantity change the seat purchase rides on.
- Identity: tenancy & the access mirror — how eligibility is kept fresh by the Cbox ID provisioning webhooks.