Seat-Based Pricing
Include seats in a plan, price each extra one flat or in tiers, choose who takes a seat, and set a minimum, blocks and a cap.
Seat pricing charges a workspace for the people in it. A seat-priced plan includes some seats in its base price and bills for each seat past them. BuildBase counts the seats for you. As members join and leave, it keeps the seat count on the Stripe subscription in step. And when the plan has no seat left for someone, they're refused at the invitation, before anything is billed.
import { useSaaSWorkspaces, useSeatStatus } from '@buildbase/sdk/react';
function InviteButton({
pendingInvitations,
onInvite,
}: {
pendingInvitations: number;
onInvite: () => void;
}) {
const { currentWorkspace } = useSaaSWorkspaces();
const seats = useSeatStatus(currentWorkspace, { pendingInvitations });
return (
<div>
{seats.maxUsers > 0 && (
<p>
{seats.memberCount} of {seats.maxUsers} seats taken
</p>
)}
<button type="button" onClick={onInvite} disabled={!seats.canInvite}>
Invite a teammate
</button>
</div>
);
}useSeatStatus is in @buildbase/sdk 1.3.0. It returns:
memberCount,includedSeatsandavailableSeatsmaxUsers, which is 0 when there is no capisAtMaxandisNearMaxperSeatPriceCents, with itscurrencycanInvite, andinviteBlockReasonwhen that is false
Pass it the live count of pending invitations, because each one holds a seat. The hook counts every member in the browser. So the plan's seat rules below (roles, a minimum, blocks) show on the invoice and in the server's refusals, but not in this count. The members screen in the settings dialog reads the same answer.
Before you start
In the console, under Billing > Plans, turn on seat pricing for each currency of a draft plan version. Set the included seats, a per-seat price for each billing interval (or tiers), and a seat cap if you want one. Then publish the version.
What one seat costs
The base price pays for the includedSeats. Every seat past them is billed one of two ways:
- A flat price per seat,
perSeat, for the subscription's interval, in the currency's smallest unit (cents for USD). - Tiers,
tiers, in place ofperSeat. Graduated tiers price each seat at the band it falls in and add the bands up. Volume tiers price every seat at the band the total lands in.
Tiers count billable seats only: the ones past the included seats. See Tiered overage and seats for showing a tiered price.
Seats are their own line on the Stripe subscription. When the count changes, that line's quantity changes with it, and Stripe prorates the difference onto the next invoice: it charges or credits only for the part of the billing period that's left.
Who takes a seat
By default every active human member takes a seat, and so does every pending invitation. A blocked member doesn't, and an agent or a service account never does. A pending invitation holds its seat from the moment the email is sent until it is accepted, declined, revoked or expires.
A plan can count differently with seatCountRule on its seat pricing:
| Field | Default | What it does |
|---|---|---|
includeRoles | all | Only members with these roles take a seat, such as ["admin", "editor"] |
excludeRoles | none | Members with these roles never do, such as ["viewer"] |
excludeBlocked | true | false counts blocked members too |
includePendingInvitations | true | false stops counting invitations until they are accepted |
A role that takes no seat needs no seat to join. So on a plan that leaves viewers free, a viewer can be invited to a workspace that's at its cap. Promoting that viewer to a role that takes a seat is checked like an invitation, and refused at the cap.
The rule is checked when you publish the version. No role may be both included and excluded, and an includeRoles list must name at least one role.
Minimums, blocks and the cap
Three more settings shape the bill. The plan below includes 3 seats, caps at 50, leaves viewers free, bills at least 5 seats and sells them in blocks of 5:
{
"seatPricing": {
"enabled": true,
"includedSeats": 3,
"maxSeats": 50,
"perSeat": { "monthly": 1200, "yearly": 12000 },
"seatCountRule": { "excludeRoles": ["viewer"] },
"minQuantity": 5,
"blockSize": 5
}
}minQuantityis the fewest billable seats the plan charges for once seat pricing is on. The Stripe quantity never goes below it.blockSizesells seats in blocks: the billable count rounds up to the next full block. With the plan above, a workspace with 14 seated members has 11 billable seats and is billed for 15.maxSeatsis a hard cap, whatever state the subscription is in. At the cap, an invitation, a direct add and a promotion to a role that takes a seat are all refused with402 SEAT_LIMIT_REACHED, which carriescurrentUserCountandlimit.- During a trial, by default only the included seats can be filled, so nobody is billed for extra seats before the first payment. The plan version's Members during the trial setting (
trial.seatCap) can raise that to the plan's maximum or to a set number. Past it, the same402says so.
The console's plan editor covers the included seats, the price, tiers and the cap. Set the rest (seatCountRule, minQuantity, blockSize and seatScope) with the admin API, on the draft version's pricingVariants[].seatPricing. Make that the last change before you publish the version.
Two requests for the last seat
When two people try to take the last seat at the same moment, only one gets it. Your code doesn't have to guard against that.
Each workspace has one seat lease: a short lock that lets one seat check, and the write that takes the seat, through at a time. Invitations or adds that arrive together for one free seat take turns, and exactly one gets it. The rest are refused with the cap's 402. A request that waits too long for its turn gets 409 SEAT_CHECK_BUSY: send it again.
Accepting an invitation holds the same lease from the moment the invitation stops counting until the member is added, so nothing can take that seat in between.
One seat per person per payer
When one customer pays for several workspaces (see Several workspaces on one card), seatScope: "payer" bills each person once across all of them. The payer's workspaces are taken in the order their subscriptions started, and a member who already takes a seat in an earlier one takes none in the next. A workspace that pays only for itself bills as before.
Seats that count installations
A seat doesn't have to be a person. On a plan sold to individual people, unit: "installation" charges for each of the person's self-hosted installations (copies they run on their own servers) instead of for members. The cap comes from the plan's max-installations limit when the version is published. The platform relays the count rather than reading it from members, so seatCountRule and seatScope: "payer" don't apply.
Seat classes
To charge some members a second price on top of the plan's seats, such as an extra fee for each editor, sell a seat class: a recurring add-on whose quantity follows a seatCountRule. See Seat classes.
Next steps
- Invitations - How a pending invitation holds a seat.
- Pricing page - Show included seats and a tiered seat price.
- Error codes -
SEAT_LIMIT_REACHEDand the rest.