Skip to content

/template recipe-per-seat-billing

Commercial License Required

MakerKit requires a commercial license. While Orchestre can help you build with it, you must obtain a valid license from MakerKit for commercial use.

Access: /template recipe-per-seat-billing or /t recipe-per-seat-billing

Purpose

Implements a comprehensive per-seat billing model where teams pay based on the number of active users. This recipe creates infrastructure for seat management, invitation flows, billing calculations, and automatic scaling of subscriptions.

How It Actually Works

The recipe:

  1. Analyzes your current team and billing structure
  2. Creates seat tracking and management systems
  3. Implements invitation and onboarding flows
  4. Adds seat-based pricing calculations
  5. Integrates with Stripe for dynamic subscriptions
  6. Sets up usage reporting and analytics

Use Cases

  • Business SaaS: Teams pay per active member
  • Collaboration Tools: Workspace seat management
  • Enterprise Software: Department-based licensing
  • Development Platforms: Per-developer pricing
  • Educational Tools: Per-student subscriptions
  • Design Tools: Per-designer seat allocation

Examples

Basic Per-Seat Model

bash
/template recipe-per-seat-billing
# Implements standard per-seat billing with:
# - $10/user/month pricing
# - Unlimited invitations
# - Auto-scaling subscriptions
# - Seat usage analytics

Tiered Seat Pricing

bash
/template recipe-per-seat-billing tiered
# Creates volume-based pricing with:
# - 1-5 seats: $15/seat
# - 6-20 seats: $12/seat
# - 21+ seats: $10/seat
# - Bulk discounts

Role-Based Seats

bash
/template recipe-per-seat-billing role-based
# Advanced seat system with:
# - Admin seats: $20/month
# - Editor seats: $15/month
# - Viewer seats: $5/month
# - Custom role pricing

What Gets Created

Database Schema

sql
-- Seat allocations table
create table team_seats (
  id uuid primary key default gen_random_uuid(),
  team_id uuid references teams(id) on delete cascade,
  subscription_id text references subscriptions(id),
  total_seats integer not null default 1,
  used_seats integer not null default 0,
  seat_type text default 'standard',
  price_per_seat decimal(10,2) not null,
  created_at timestamp with time zone default now(),
  updated_at timestamp with time zone default now()
);

-- Seat assignments
create table seat_assignments (
  id uuid primary key default gen_random_uuid(),
  team_id uuid references teams(id) on delete cascade,
  user_id uuid references auth.users(id) on delete cascade,
  seat_type text default 'standard',
  role text not null,
  assigned_at timestamp with time zone default now(),
  assigned_by uuid references auth.users(id),
  last_active_at timestamp with time zone,
  is_billable boolean default true,
  metadata jsonb default '{}'
);

-- Pending invitations
create table seat_invitations (
  id uuid primary key default gen_random_uuid(),
  team_id uuid references teams(id) on delete cascade,
  email text not null,
  role text not null,
  seat_type text default 'standard',
  invited_by uuid references auth.users(id),
  token text unique not null,
  expires_at timestamp with time zone not null,
  accepted_at timestamp with time zone,
  created_at timestamp with time zone default now()
);

-- Seat usage history
create table seat_usage_history (
  id uuid primary key default gen_random_uuid(),
  team_id uuid references teams(id) on delete cascade,
  period_start timestamp with time zone not null,
  period_end timestamp with time zone not null,
  average_seats decimal(10,2),
  peak_seats integer,
  total_cost decimal(10,2),
  created_at timestamp with time zone default now()
);

API Routes

typescript
// app/api/seats/manage/route.ts
POST /api/seats/manage
- Add/remove seats
- Update subscription quantity
- Calculate prorated charges

// app/api/seats/invite/route.ts
POST /api/seats/invite
- Send seat invitations
- Check seat availability
- Generate invitation tokens

// app/api/seats/assign/route.ts
POST /api/seats/assign
- Assign user to seat
- Update seat usage
- Trigger billing updates

// app/api/seats/usage/route.ts
GET /api/seats/usage
- Current seat utilization
- Historical usage data
- Cost projections

React Components

typescript
// components/billing/seat-manager.tsx
- Visual seat allocation
- Add/remove seat controls
- Usage indicators
- Cost calculator

// components/billing/seat-invitation-form.tsx
- Bulk invitation UI
- Role selection
- Seat type assignment
- Preview costs

// components/billing/seat-usage-chart.tsx
- Usage over time
- Cost trends
- Seat utilization
- Forecast display

// components/team/member-seats-table.tsx
- Member list with seats
- Role management
- Last active status
- Remove member action

Hooks and Utilities

typescript
// hooks/use-seats.ts
- useSeatAllocation()
- useSeatUsage()
- useSeatInvitations()
- useSeatCosts()

// lib/seats/calculator.ts
- Calculate seat costs
- Proration logic
- Tiered pricing
- Discount application

// lib/seats/manager.ts
- Seat assignment
- Availability checking
- Auto-scaling logic
- Usage tracking

Technical Details

Dynamic Subscription Updates

typescript
// Stripe subscription quantity updates
async function updateSeatCount(teamId: string, newSeatCount: number) {
  const team = await getTeam(teamId);
  const subscription = await stripe.subscriptions.retrieve(team.subscriptionId);
  
  // Update quantity with proration
  await stripe.subscriptions.update(subscription.id, {
    items: [{
      id: subscription.items.data[0].id,
      quantity: newSeatCount,
    }],
    proration_behavior: 'create_prorations',
  });
  
  // Update local records
  await updateTeamSeats(teamId, newSeatCount);
}

Seat Assignment Logic

typescript
// Intelligent seat assignment
async function assignSeat(teamId: string, userId: string, role: string) {
  const seats = await getTeamSeats(teamId);
  
  if (seats.used >= seats.total) {
    // Auto-scale if enabled
    if (seats.autoScale) {
      await addSeats(teamId, 1);
    } else {
      throw new Error('No available seats');
    }
  }
  
  // Assign the seat
  await createSeatAssignment({
    teamId,
    userId,
    role,
    seatType: determineSeatType(role),
  });
  
  // Update usage
  await incrementSeatUsage(teamId);
}

Usage Tracking

typescript
// Track seat utilization
const seatUsageTracker = {
  async recordDailyUsage(teamId: string) {
    const usage = await calculateCurrentUsage(teamId);
    await saveDailySnapshot({
      teamId,
      date: new Date(),
      usedSeats: usage.active,
      totalSeats: usage.total,
      utilization: usage.percentage,
    });
  },
  
  async generateMonthlyReport(teamId: string) {
    const usage = await getMonthlyUsage(teamId);
    return {
      averageUtilization: usage.average,
      peakUsage: usage.peak,
      recommendedSeats: usage.recommended,
      potentialSavings: usage.savings,
    };
  }
};

Memory Evolution

The recipe creates detailed seat management memory:

markdown
## Per-Seat Billing Configuration

### Pricing Model
- Base price: $10/seat/month
- Minimum seats: 1
- Auto-scaling: Enabled
- Proration: Enabled

### Seat Types
- Standard: $10/month (full access)
- Limited: $5/month (read-only)
- Guest: Free (limited features)

### Current Policies
- Unused seats: Retained for 30 days
- Invitation expiry: 7 days
- Seat reassignment: Immediate
- Billing cycle: Monthly

### Integration Status
- Stripe: Subscription quantity sync
- Analytics: Daily usage tracking
- Notifications: Seat limit warnings
- Admin tools: Bulk seat management

Best Practices

Flexible Seat Management

  • Allow seat pooling across teams
  • Implement seat reservations
  • Support temporary seat increases
  • Enable seat sharing for part-time users

Cost Optimization

  • Show real-time cost implications
  • Provide usage recommendations
  • Alert on underutilized seats
  • Suggest optimal seat counts

User Experience

  • Clear seat availability indicators
  • Smooth invitation flows
  • Self-service seat management
  • Transparent pricing display

Compliance

  • Audit trail for seat changes
  • Usage reports for finance
  • License compliance tracking
  • Export capabilities

Integration Points

With Team Management

typescript
// Automatic seat assignment on team join
const addTeamMember = async (teamId, userId, role) => {
  await assignSeat(teamId, userId, role);
  await addMemberToTeam(teamId, userId, role);
  await notifyTeamOwner(teamId, 'New member added');
};

With Billing System

typescript
// Seat-based invoice generation
const generateInvoice = async (teamId) => {
  const seatUsage = await calculateSeatUsage(teamId);
  const amount = seatUsage.seats * seatUsage.pricePerSeat;
  return createInvoice({
    teamId,
    lineItems: [{
      description: `${seatUsage.seats} seats`,
      amount,
    }],
  });
};

With Analytics

typescript
// Seat utilization metrics
const seatMetrics = {
  utilization: async (teamId) => {
    const { used, total } = await getSeatUsage(teamId);
    return (used / total) * 100;
  },
  costPerActiveUser: async (teamId) => {
    const cost = await getMonthlyCost(teamId);
    const activeUsers = await getActiveUserCount(teamId);
    return cost / activeUsers;
  },
};

Troubleshooting

Common Issues

"No Available Seats" Error

  • Check current seat allocation
  • Verify auto-scaling settings
  • Review seat assignment logs
  • Ensure billing is current

Subscription Sync Issues

  • Verify Stripe webhook configuration
  • Check subscription item IDs
  • Review proration settings
  • Validate quantity updates

Invitation Problems

  • Check email delivery
  • Verify invitation tokens
  • Review expiration settings
  • Test invitation flow

Debug Helpers

typescript
// Seat debugging utilities
const seatDebug = {
  async checkAvailability(teamId: string) {
    const seats = await getTeamSeats(teamId);
    console.log('Seat Status:', {
      total: seats.total,
      used: seats.used,
      available: seats.total - seats.used,
      assignments: await getSeatAssignments(teamId),
    });
  },
};

Advanced Features

Seat Forecasting

typescript
// ML-based seat prediction
const forecastSeats = async (teamId: string) => {
  const history = await getSeatHistory(teamId);
  const growth = calculateGrowthRate(history);
  return {
    next30Days: predictSeats(30, growth),
    next90Days: predictSeats(90, growth),
    recommendedSeats: optimalSeatCount(history),
  };
};

Bulk Operations

typescript
// Bulk seat management
const bulkOperations = {
  async importUsers(teamId: string, csvData: string) {
    const users = parseCSV(csvData);
    const results = await Promise.allSettled(
      users.map(user => assignSeat(teamId, user))
    );
    return summarizeResults(results);
  },
};
  • /template add-team-management - Enhanced team features
  • /template add-billing - Core billing setup
  • /template add-usage-analytics - Detailed usage tracking
  • /template add-admin-panel - Seat management UI

Built with ❤️ for the AI Coding community, by Praney Behl