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