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

  1. You define a per_seat line item in your billing schema
  2. When a team subscribes, Makerkit counts current members and sets the initial quantity
  3. When members join or leave, Makerkit updates the subscription quantity
  4. 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 },
],
},
]

Provider Setup

Stripe

  1. Create a product in Stripe Dashboard
  2. Add a price with Graduated pricing or Volume pricing
  3. Set the pricing tiers to match your schema
  4. Copy the Price ID (e.g., price_xxx) to your line item id

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

  1. Create a product with Usage-based pricing
  2. Configure the pricing tiers
  3. Copy the Variant ID to your line item id

Paddle

  1. Create a product with quantity-based pricing
  2. Configure as needed (Paddle handles proration automatically)
  3. Copy the Price ID to your line item id

Automatic Seat Updates

Makerkit automatically updates seat counts when:

ActionEffect
Team member accepts invitationSeat count increases
Team member is removedSeat count decreases
Team member leavesSeat count decreases
Account is deletedSubscription is canceled

The billing provider handles proration based on your settings.

Testing Per-Seat Billing

  1. 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
  2. Add a member:
    • Invite a new member to the team
    • Have them accept the invitation
    • Check Stripe/LS/Paddle: subscription quantity should increase
  3. Remove a member:
    • Remove a member from the team
    • Check: subscription quantity should decrease
  4. 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

  1. Check that the line item has type: 'per_seat'
  2. Verify the subscription is active
  3. Check webhook logs for errors
  4. 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.