Configure Per-Seat Billing for Team Subscriptions
Implement per-seat pricing for your SaaS. Makerkit automatically tracks team members and updates seat counts with your billing provider when members join or leave.
Per-seat billing charges customers based on the number of users (seats) in their team. Makerkit handles this automatically: when team members are added or removed, the subscription is updated with the new seat count.
How It Works
- You define a
per_seatline item in your billing schema - When a team subscribes, Makerkit counts current members and sets the initial quantity
- When members join or leave, Makerkit updates the subscription quantity
- Stripe handles proration
No custom code required for basic per-seat billing.
Schema Configuration
Define a per-seat line item in your billing schema:
apps/web/config/billing.config.ts
import { createBillingSchema } from '@kit/billing';export default createBillingSchema({ provider: process.env.VITE_BILLING_PROVIDER, products: [ { id: 'team', name: 'Team', description: 'For growing teams', currency: 'USD', features: [ 'Unlimited projects', 'Team collaboration', 'Priority support', ], plans: [ { id: 'team-monthly', name: 'Team Monthly', paymentType: 'recurring', interval: 'month', lineItems: [ { id: 'price_team_monthly', // Your Stripe Price ID name: 'Team Seats', cost: 0, // Base cost (calculated from tiers) type: 'per_seat', tiers: [ { upTo: 3, cost: 0 }, // First 3 seats free { upTo: 10, cost: 15 }, // $15/seat for seats 4-10 { upTo: 'unlimited', cost: 12 }, // Volume discount ], }, ], }, ], }, ],});Pricing Tier Patterns
Free Tier + Per-Seat
Include free seats for small teams:
tiers: [ { upTo: 5, cost: 0 }, // 5 free seats { upTo: 'unlimited', cost: 10 }, // $10/seat after]Flat Per-Seat (No Tiers)
Simple per-seat pricing:
tiers: [ { upTo: 'unlimited', cost: 15 }, // $15/seat for all seats]Volume Discounts
Reward larger teams:
tiers: [ { upTo: 10, cost: 20 }, // $20/seat for 1-10 { upTo: 50, cost: 15 }, // $15/seat for 11-50 { upTo: 100, cost: 12 }, // $12/seat for 51-100 { upTo: 'unlimited', cost: 10 }, // $10/seat for 100+]Base Fee + Per-Seat (Stripe Only)
Combine a flat fee with per-seat pricing:
lineItems: [ { id: 'price_base_fee', name: 'Platform Fee', cost: 49, type: 'flat', }, { id: 'price_seats', name: 'Team Seats', cost: 0, type: 'per_seat', tiers: [ { upTo: 5, cost: 0 }, { upTo: 'unlimited', cost: 10 }, ], },]Stripe Setup
- Create a product in Stripe Dashboard
- Add a price with Graduated pricing or Volume pricing
- Set the pricing tiers to match your schema
- Copy the Price ID (e.g.,
price_xxx) to your line itemid
Stripe pricing types:
- Graduated: Each tier applies to that range only (e.g., seats 1-5 at $0, seats 6-10 at $15)
- Volume: The price for all units is determined by the total quantity
Automatic Seat Updates
Makerkit automatically updates seat counts when:
| Action | Effect |
|---|---|
| Team member accepts invitation | Seat count increases |
| Team member is removed | Seat count decreases |
| Team member leaves | Seat count decreases |
| Account is deleted | Subscription is canceled |
The billing provider handles proration based on your settings.
Testing Per-Seat Billing
- Create a team subscription:
- Sign up and create a team account
- Subscribe to a per-seat plan
- Verify the initial seat count matches team size
- Add a member:
- Invite a new member to the team
- Have them accept the invitation
- Check Stripe: subscription quantity should increase
- Remove a member:
- Remove a member from the team
- Check: subscription quantity should decrease
- Verify proration:
- Check the upcoming invoice in your provider dashboard
- Confirm proration is calculated correctly
Manual Seat Updates (Advanced)
In rare cases, you might need to manually update seat counts:
import { createBillingGatewayService } from '@kit/billing-gateway';import { getSupabaseServerClient } from '@kit/supabase/server-client';export async function updateSeatCount( subscriptionId: string, subscriptionItemId: string, newQuantity: number) { const supabase = getSupabaseServerClient(); // Get subscription to find the provider const { data: subscription } = await supabase .from('subscriptions') .select('billing_provider') .eq('id', subscriptionId) .single(); if (!subscription) { throw new Error('Subscription not found'); } const service = createBillingGatewayService( subscription.billing_provider ); return service.updateSubscriptionItem({ subscriptionId, subscriptionItemId, quantity: newQuantity, });}Checking Seat Limits
To enforce seat limits in your application:
import { createAccountsApi } from '@kit/accounts/api';import { getSupabaseServerClient } from '@kit/supabase/server-client';export async function canAddMember(accountId: string): Promise<boolean> { const supabase = getSupabaseServerClient(); const api = createAccountsApi(supabase); // Get current subscription const subscription = await api.getSubscription(accountId); if (!subscription) { return false; // No subscription } // Get per-seat item const { data: seatItem } = await supabase .from('subscription_items') .select('quantity') .eq('subscription_id', subscription.id) .eq('type', 'per_seat') .single(); // Get current member count const { count: memberCount } = await supabase .from('accounts_memberships') .select('*', { count: 'exact', head: true }) .eq('account_id', accountId); // Check if under limit (if you have a max seats limit) const maxSeats = 100; // Your limit return (memberCount ?? 0) < maxSeats;}Common Issues
Seat count not updating
- Check that the line item has
type: 'per_seat' - Verify the subscription is active
- Check webhook logs for errors
- Ensure the subscription item ID is correct in the database
Proration not working as expected
Configure proration behavior in Stripe through the Customer Portal settings or API parameters.
"Minimum quantity" errors
Some plans require at least 1 seat. Ensure your tiers start at a valid minimum.
Related Documentation
- Billing Schema - Define pricing plans
- Stripe Setup - Configure Stripe
- Billing API - Manual subscription updates
- Team Accounts - Team management