Skip to content

Billing Enhancements in MakerKit ​

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.

Overview ​

MakerKit v2.11.0 introduces powerful billing enhancements that enable sophisticated monetization strategies. These features include checkout addons, subscription entitlements, and flexible billing models.

Key Features ​

1. Checkout Addons ​

Enable customers to purchase additional items during subscription checkout:

  • One-time setup fees
  • Additional seats or licenses
  • Premium onboarding services
  • Training packages

2. Subscription Entitlements ​

Fine-grained feature access control based on subscription plans:

  • Feature flags tied to billing plans
  • Usage limits and quotas
  • Tiered access levels
  • Dynamic feature unlocking

3. Flexible Billing Models ​

Support for various monetization strategies:

  • Per-seat billing with tiered pricing
  • Credit-based systems for AI/API usage
  • Metered billing for consumption-based pricing
  • Hybrid models combining multiple approaches

Architecture ​

The billing system is built on:

  • Stripe for payment processing
  • PostgreSQL for entitlement storage
  • Supabase RLS for secure access control
  • Server-side validation for security

Implementation ​

Checkout Addons ​

Configure addons in your billing configuration:

typescript
// config/billing.config.ts
export const billingConfig = {
  products: [
    {
      id: 'pro_plan',
      name: 'Pro Plan',
      variants: [
        {
          variantId: 'pro_monthly',
          price: 49,
          currency: 'USD',
          interval: 'month'
        }
      ],
      addons: [
        {
          id: 'onboarding_addon',
          name: 'Premium Onboarding',
          price: 299,
          type: 'one_time',
          description: '1-on-1 onboarding session with our team'
        },
        {
          id: 'extra_seats_addon',
          name: 'Additional Seats (5 pack)',
          price: 99,
          type: 'one_time',
          description: 'Add 5 more team members'
        }
      ]
    }
  ]
};

Add addons to checkout session:

typescript
// app/api/billing/checkout/route.ts
import { createCheckoutSession } from '@kit/billing/checkout';

export async function POST(request: Request) {
  const { variantId, addons, organizationId } = await request.json();
  
  const session = await createCheckoutSession({
    organizationId,
    variantId,
    addons: addons.map(addon => ({
      id: addon.id,
      quantity: addon.quantity || 1
    })),
    mode: 'subscription',
    successUrl: '/dashboard?checkout=success',
    cancelUrl: '/dashboard?checkout=cancel'
  });
  
  return Response.json({ sessionId: session.id });
}

Subscription Entitlements ​

Define entitlements in the database:

sql
-- Plan entitlements table
CREATE TABLE public.plan_entitlements (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  variant_id VARCHAR(255) NOT NULL,
  feature VARCHAR(255) NOT NULL,
  entitlement JSONB NOT NULL DEFAULT '{}',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  UNIQUE(variant_id, feature)
);

-- Example entitlements
INSERT INTO plan_entitlements (variant_id, feature, entitlement) VALUES
  ('pro_monthly', 'api_calls', '{"limit": 10000, "period": "month"}'::jsonb),
  ('pro_monthly', 'team_members', '{"limit": 10}'::jsonb),
  ('pro_monthly', 'projects', '{"limit": 5}'::jsonb),
  ('enterprise_monthly', 'api_calls', '{"limit": -1}'::jsonb), -- unlimited
  ('enterprise_monthly', 'team_members', '{"limit": -1}'::jsonb),
  ('enterprise_monthly', 'projects', '{"limit": -1}'::jsonb);

-- Function to check entitlements
CREATE OR REPLACE FUNCTION check_entitlement(
  p_organization_id UUID,
  p_feature VARCHAR,
  p_requested_value INTEGER DEFAULT 1
) RETURNS BOOLEAN AS $$
DECLARE
  v_entitlement JSONB;
  v_limit INTEGER;
  v_current_usage INTEGER;
BEGIN
  -- Get the entitlement for the organization's plan
  SELECT pe.entitlement INTO v_entitlement
  FROM plan_entitlements pe
  JOIN organizations o ON o.subscription_variant_id = pe.variant_id
  WHERE o.id = p_organization_id AND pe.feature = p_feature;
  
  IF v_entitlement IS NULL THEN
    RETURN FALSE;
  END IF;
  
  v_limit := (v_entitlement->>'limit')::INTEGER;
  
  -- -1 means unlimited
  IF v_limit = -1 THEN
    RETURN TRUE;
  END IF;
  
  -- Check current usage based on feature type
  CASE p_feature
    WHEN 'team_members' THEN
      SELECT COUNT(*) INTO v_current_usage
      FROM organization_members
      WHERE organization_id = p_organization_id;
    WHEN 'projects' THEN
      SELECT COUNT(*) INTO v_current_usage
      FROM projects
      WHERE organization_id = p_organization_id;
    WHEN 'api_calls' THEN
      -- This would check a usage tracking table
      SELECT COALESCE(SUM(usage), 0) INTO v_current_usage
      FROM api_usage
      WHERE organization_id = p_organization_id
        AND period_start >= date_trunc('month', CURRENT_DATE);
    ELSE
      v_current_usage := 0;
  END CASE;
  
  RETURN (v_current_usage + p_requested_value) <= v_limit;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

Use entitlements in your application:

typescript
// lib/entitlements/check-entitlement.ts
import { getSupabaseServerClient } from '@kit/supabase/server-client';

export async function checkEntitlement(
  organizationId: string,
  feature: string,
  requestedValue = 1
): Promise<boolean> {
  const client = getSupabaseServerClient();
  
  const { data, error } = await client.rpc('check_entitlement', {
    p_organization_id: organizationId,
    p_feature: feature,
    p_requested_value: requestedValue
  });
  
  if (error) {
    console.error('Error checking entitlement:', error);
    return false;
  }
  
  return data;
}

// Usage in server actions
export async function createProject(formData: FormData) {
  const session = await getSession();
  const organizationId = session.user.organizationId;
  
  // Check if the organization can create more projects
  const canCreate = await checkEntitlement(organizationId, 'projects');
  
  if (!canCreate) {
    throw new Error('Project limit reached. Please upgrade your plan.');
  }
  
  // Proceed with project creation
  // ...
}

React Hook for Client-Side Checks ​

typescript
// hooks/use-entitlement.ts
import { useQuery } from '@tanstack/react-query';

export function useEntitlement(feature: string, requestedValue = 1) {
  const { organizationId } = useAuth();
  
  return useQuery({
    queryKey: ['entitlement', organizationId, feature, requestedValue],
    queryFn: async () => {
      const response = await fetch('/api/entitlements/check', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ feature, requestedValue })
      });
      
      if (!response.ok) throw new Error('Failed to check entitlement');
      
      return response.json();
    },
    enabled: !!organizationId
  });
}

// Usage in components
export function CreateProjectButton() {
  const { data: canCreate, isLoading } = useEntitlement('projects');
  
  if (isLoading) return <Spinner />;
  
  if (!canCreate) {
    return (
      <Button disabled>
        Project limit reached
        <Link href="/settings/billing">Upgrade</Link>
      </Button>
    );
  }
  
  return <Button>Create Project</Button>;
}

Billing Models ​

Per-Seat Billing ​

Configure tiered seat pricing:

typescript
// config/billing.config.ts
export const seatTiers = [
  { min: 1, max: 5, pricePerSeat: 10 },
  { min: 6, max: 20, pricePerSeat: 8 },
  { min: 21, max: 50, pricePerSeat: 6 },
  { min: 51, max: null, pricePerSeat: 5 } // 51+
];

// Calculate seat pricing
export function calculateSeatPrice(seatCount: number): number {
  let totalPrice = 0;
  let remainingSeats = seatCount;
  
  for (const tier of seatTiers) {
    const tierSeats = tier.max 
      ? Math.min(remainingSeats, tier.max - tier.min + 1)
      : remainingSeats;
    
    totalPrice += tierSeats * tier.pricePerSeat;
    remainingSeats -= tierSeats;
    
    if (remainingSeats <= 0) break;
  }
  
  return totalPrice;
}

Credit-Based Billing ​

Implement token/credit system:

typescript
// Schema for credits
CREATE TABLE public.organization_credits (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
  credits INTEGER NOT NULL DEFAULT 0,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

// Credit transactions
CREATE TABLE public.credit_transactions (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
  amount INTEGER NOT NULL, -- positive for additions, negative for usage
  type VARCHAR(50) NOT NULL, -- 'purchase', 'usage', 'bonus', 'refund'
  description TEXT,
  metadata JSONB DEFAULT '{}',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

Metered Billing ​

Track and bill for usage:

typescript
// Track API usage
export async function trackApiUsage(
  organizationId: string,
  endpoint: string,
  tokens: number
) {
  const client = getSupabaseServerClient();
  
  await client.from('api_usage').insert({
    organization_id: organizationId,
    endpoint,
    tokens,
    timestamp: new Date().toISOString()
  });
  
  // Report to Stripe for metered billing
  await stripe.subscriptionItems.createUsageRecord(
    subscriptionItemId,
    {
      quantity: tokens,
      timestamp: Math.floor(Date.now() / 1000),
      action: 'increment'
    }
  );
}

Using Recipe Prompts ​

Orchestre provides dedicated prompts for each billing model:

bash
# For credit-based billing
/template recipe-credit-billing

# For per-seat billing
/template recipe-per-seat-billing

# For metered billing
/template recipe-metered-billing

Best Practices ​

  1. Cache Entitlements: Use Redis or in-memory caching for frequent checks
  2. Graceful Degradation: Show upgrade prompts instead of hard blocks
  3. Usage Warnings: Alert users before they hit limits
  4. Audit Trail: Log all billing-related actions
  5. Webhook Handling: Keep entitlements in sync with Stripe

Integration Points ​

  • Team Management: Enforce seat limits
  • Project Creation: Check project quotas
  • API Routes: Validate usage limits
  • UI Components: Show/hide features based on plan

Next Steps ​

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