Per-seat billing
Charge organizations based on the number of seats they use.
For the complete documentation index, see llms.txt. Prefer markdown by appending.mdto documentation URLs or sendingAccept: text/markdown.
Per-seat billing is a great fit for team plans. Instead of charging a flat subscription price for the whole organization, you charge based on how many members are in it.
TurboStarter supports this out of the box for organization billing:
- define a billing variant as
BillingType.PER_SEAT - start checkout for an organization
- let the kit calculate the seat quantity automatically
- sync subscription quantity when members are added or removed
Per-seat builds on organization billing
Per-seat pricing is one way to do B2B billing in TurboStarter, but it is not the only one. Organizations can also be billed through flat subscriptions, one-time purchases, metered usage, or credits-based models by starting checkout on behalf of the organization.
How it works
Per-seat billing follows a simple flow:
Create a per-seat billing variant in your billing config.
A user starts checkout on behalf of an organization.
TurboStarter counts the organization's billable seats and sends that quantity to the billing provider.
If the organization membership changes later, the subscription quantity can be updated to match.
This keeps billing aligned with the actual team size without making you manually pass seat counts around in your UI.
Per-seat plans are organization-only
Per-seat variants are meant for organization purchases. They are not shown for personal billing.
Configuration
Per-seat billing is configured the same way as other billing variants, but with type: BillingType.PER_SEAT.
export const config = billingConfigSchema.parse({
plans: [
{
id: BillingPlan.PREMIUM,
name: "Premium",
description: "Best for growing teams",
badge: "Popular",
features: [
"Unlimited projects",
"Priority support",
"Team collaboration",
],
variants: [
{
id: "price_monthly_per_seat",
type: BillingType.PER_SEAT,
model: BillingModel.RECURRING,
interval: RecurringInterval.MONTH,
trialDays: 7,
tiers: [
{ cost: 2_000, upTo: 5 },
{ cost: 1_700, upTo: 25 },
{ cost: 1_300 },
],
},
],
},
],
}) satisfies BillingConfig;Let's break down the fields:
id: The provider-specific price, variant, or product ID. This must match your billing provider exactly.type: Must beBillingType.PER_SEAT.model: Can beBillingModel.ONE_TIMEorBillingModel.RECURRING.interval: Required for recurring variants.trialDays: Optional for recurring variants.cost: Use this for a simple fixed price per seat.tiers: Use this when the price per seat changes as the team grows.
Fixed per-seat pricing
Use cost when every seat should cost the same amount.
{
id: "price_yearly_per_seat",
type: BillingType.PER_SEAT,
model: BillingModel.RECURRING,
interval: RecurringInterval.YEAR,
cost: 19_900,
}Tiered per-seat pricing
Use tiers when you want volume pricing, such as cheaper seats for larger teams.
{
id: "price_monthly_per_seat",
type: BillingType.PER_SEAT,
model: BillingModel.RECURRING,
interval: RecurringInterval.MONTH,
tiers: [
{ cost: 2_000, upTo: 5 },
{ cost: 1_700, upTo: 25 },
{ cost: 1_300 },
],
}This works well for patterns like:
- a simple flat price per seat
- discounted pricing for larger teams
- free or cheaper seats up to a threshold
Tiered one-time seat pricing is not supported
One-time per-seat variants can use cost, but not tiers.
Checkout
When a user starts checkout for an organization, TurboStarter automatically determines the seat quantity from that organization's member count.
That means:
- you do not pass seat quantity manually from the frontend
- the selected
referenceIddecides which organization is being billed - the initial subscription quantity matches the team's current size
The current implementation uses the organization member count as the billable seat count, with a minimum of 1.
Syncing subscription quantity
For recurring per-seat billing, the subscription quantity should stay aligned with the organization as it changes over time.
TurboStarter includes hooks you can use to sync seats after events such as:
- adding a member
- accepting an invitation
- removing a member
This gives you automatic quantity updates while letting the provider handle proration according to its own rules and settings.
Seat sync vs seat enforcement
These are related, but they solve different problems:
- Per-seat billing keeps subscription quantity in sync with team size
- Seat enforcement blocks actions when an organization should not be allowed to add more members
One simple way to handle enforcement is to store a members limit on the plan and check it before creating an invite:
import { checkPlanLimit } from "@workspace/billing/shared/utils/plan";
const { allowed } = checkPlanLimit({
id: BillingPlan.PREMIUM,
key: "members",
currentUsage: organization.members.length,
});
if (!allowed) {
throw new Error("This plan has reached its member limit.");
}This works especially well for organization plans like:
limits: {
members: 5,
},If you want hard limits, add that logic before inviting or adding members. For example, you might:
- block invitations when no paid seats remain
- allow invites for owners and admins only after upgrading
- show a warning when removing a member would free up a paid seat
This is usually the best place to put product-specific business rules, because every SaaS handles seats a little differently.
Permissions
Organization billing is permission-aware, when checkout or portal actions are performed on behalf of an organization, TurboStarter checks the member's billing permissions for that organization.
By default:
- members can read billing data
- admins can read billing data and create billing actions
- owners can fully manage billing
This helps keep team billing safe while still allowing the right people to upgrade plans or manage subscriptions.
Provider notes
Per-seat billing works across the supported web billing providers, but the provider IDs still need to match your configuration exactly.
- Stripe: quantity is sent in checkout line items and can later be updated on the subscription item
- Lemon Squeezy: quantity is sent at checkout and later updated through subscription items
- Polar: quantity is sent as seats and recurring subscriptions can be updated later
As always, the configured variant.id must match the billing provider's identifier exactly.
Testing
Before shipping, test the full flow:
- Start a checkout for an organization and confirm the initial quantity matches the number of members.
- Add a member and verify that the subscription quantity increases.
- Remove a member and verify that the quantity decreases.
- Check your billing provider dashboard to confirm proration behaves the way you expect.
If something looks off, the most common causes are:
- the variant is not configured with
type: BillingType.PER_SEAT - the provider ID in
variant.iddoes not match - the checkout is being created for the wrong billing reference
- seat sync hooks are missing or not firing
Recommended setup
For most team-based SaaS products, the simplest setup is:
- Create a recurring organization variant with
type: BillingType.PER_SEAT. - Configure the matching price in your billing provider.
- Use organization checkout so quantity is derived automatically.
- Keep membership-driven seat sync enabled.
- Add custom seat enforcement only if your product needs hard invite or membership limits.
This gives you a clean default: teams pay for the seats they actually use, and your billing stays in sync as the organization grows.
How is this guide?
Last updated on