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:
// 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:
// 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:
-- 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:
// 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
// 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:
// 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:
// 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:
// 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:
# For credit-based billing
/template recipe-credit-billing
# For per-seat billing
/template recipe-per-seat-billing
# For metered billing
/template recipe-metered-billingBest Practices
- Cache Entitlements: Use Redis or in-memory caching for frequent checks
- Graceful Degradation: Show upgrade prompts instead of hard blocks
- Usage Warnings: Alert users before they hit limits
- Audit Trail: Log all billing-related actions
- 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
- Explore billing recipe prompts
- Review MakerKit template commands
- Check integration patterns
