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
- Your billing provider (Stripe, Lemon Squeezy, Paddle) 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.NEXT_PUBLIC_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 only
Multiple line items (flat + per-seat) only work with Stripe. Lemon Squeezy and Paddle support one line item per plan.
Provider Setup
Stripe
- 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
Lemon Squeezy
- Create a product with Usage-based pricing
- Configure the pricing tiers
- Copy the Variant ID to your line item
id
Paddle
- Create a product with quantity-based pricing
- Configure as needed (Paddle handles proration automatically)
- Copy the Price ID to your line item
id
Paddle trial limitation
Paddle doesn't support updating subscription quantities during a trial period. If using per-seat billing with Paddle trials, consider using Feature Policies to restrict invitations during trials.
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/LS/Paddle: 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 = await 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 your billing provider:
- Stripe: Customer Portal settings or API parameters
- Lemon Squeezy: Product settings
- Paddle: Automatic proration
"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