Billing in Next.js Supabase Turbo
Complete guide to implementing billing in your Next.js Supabase SaaS. Configure subscriptions, one-off payments, metered usage, and per-seat pricing with Stripe, Lemon Squeezy, or Paddle.
Makerkit's billing system lets you accept payments through Stripe, Lemon Squeezy, or Paddle with a unified API. You define your pricing once in a schema, and the gateway routes requests to your chosen provider. Switching providers requires changing one environment variable.
Quick Start
Set your billing provider:
NEXT_PUBLIC_BILLING_PROVIDER=stripe # or lemon-squeezy, paddleUpdate the database configuration to match:
UPDATE public.config SET billing_provider = 'stripe';Then configure your billing schema with your products and pricing.
Choose Your Provider
| Provider | Best For | Tax Handling | Multi-line Items |
|---|---|---|---|
| Stripe | Maximum flexibility, global reach | You handle (or use Stripe Tax) | Yes |
| Lemon Squeezy | Simplicity, automatic tax compliance | Merchant of Record | No (1 per plan) |
| Paddle | B2B SaaS, automatic tax compliance | Merchant of Record | No (flat + per-seat only) |
Merchant of Record means Lemon Squeezy and Paddle handle VAT, sales tax, and compliance globally. With Stripe, you're responsible for tax collection (though Stripe Tax can help).
Supported Pricing Models
Makerkit supports four billing models out of the box:
Flat Subscriptions
Fixed monthly or annual pricing. The most common SaaS model.
{ id: 'price_xxx', name: 'Pro Plan', cost: 29, type: 'flat',}Learn more about configuring flat subscriptions →
Per-Seat Billing
Charge based on team size. Makerkit automatically updates seat counts when members join or leave.
{ id: 'price_xxx', name: 'Team', cost: 0, type: 'per_seat', tiers: [ { upTo: 3, cost: 0 }, // First 3 seats free { upTo: 10, cost: 12 }, // $12/seat up to 10 { upTo: 'unlimited', cost: 10 }, ]}Metered Usage
Charge based on consumption (API calls, storage, tokens). Report usage through the billing API.
{ id: 'price_xxx', name: 'API Requests', cost: 0, type: 'metered', unit: 'requests', tiers: [ { upTo: 1000, cost: 0 }, { upTo: 'unlimited', cost: 0.001 }, ]}One-Off Payments
Lifetime deals, add-ons, or credits. Stored in the orders table instead of subscriptions.
{ paymentType: 'one-time', lineItems: [{ id: 'price_xxx', name: 'Lifetime Access', cost: 299, type: 'flat', }]}Credit-Based Billing
For AI SaaS and token-based systems. Combine subscriptions with a credits table for consumption tracking.
Implement credit-based billing →
Architecture Overview
The billing system uses a provider-agnostic architecture:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│ Your App │────▶│ Gateway │────▶│ Provider ││ (billing.config) │ (routes requests) │ (Stripe/LS/Paddle)└─────────────────┘ └─────────────────┘ └─────────────────┘ │ ▼┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│ Database │◀────│ Webhook Handler│◀────│ Webhook ││ (subscriptions) │ (processes events) │ (payment events)└─────────────────┘ └─────────────────┘ └─────────────────┘Package structure:
@kit/billing(core): Schema validation, interfaces, types@kit/billing-gateway: Provider routing, unified API@kit/stripe: Stripe-specific implementation@kit/lemon-squeezy: Lemon Squeezy-specific implementation@kit/paddle: Paddle-specific implementation (plugin)
This abstraction means your application code stays the same regardless of provider. The billing schema defines what you sell, and each provider package handles the API specifics.
Database Schema
Billing data is stored in four main tables:
| Table | Purpose |
|---|---|
billing_customers | Links accounts to provider customer IDs |
subscriptions | Active and historical subscription records |
subscription_items | Line items within subscriptions (for per-seat, metered) |
orders | One-off payment records |
order_items | Items within one-off orders |
All tables have Row Level Security (RLS) enabled. Users can only read their own billing data.
Configuration Files
billing.config.ts
Your pricing schema lives at apps/web/config/billing.config.ts:
import { createBillingSchema } from '@kit/billing';export default createBillingSchema({ provider: process.env.NEXT_PUBLIC_BILLING_PROVIDER, products: [ { id: 'starter', name: 'Starter', description: 'For individuals', currency: 'USD', plans: [/* ... */], }, ],});Full billing schema documentation →
Environment Variables
Each provider requires specific environment variables:
Stripe:
STRIPE_SECRET_KEY=sk_...STRIPE_WEBHOOK_SECRET=whsec_...NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_...Lemon Squeezy:
LEMON_SQUEEZY_SECRET_KEY=...LEMON_SQUEEZY_SIGNING_SECRET=...LEMON_SQUEEZY_STORE_ID=...Paddle:
PADDLE_API_KEY=...PADDLE_WEBHOOK_SECRET_KEY=...NEXT_PUBLIC_PADDLE_CLIENT_TOKEN=...Common Tasks
Check if an account has a subscription
import { createAccountsApi } from '@kit/accounts/api';const api = createAccountsApi(supabaseClient);const subscription = await api.getSubscription(accountId);if (subscription?.status === 'active') { // User has active subscription}Create a checkout session
import { createBillingGatewayService } from '@kit/billing-gateway';const service = await createBillingGatewayService(provider);const { checkoutToken } = await service.createCheckoutSession({ accountId, plan, returnUrl: `${origin}/billing/return`, customerEmail: user.email,});Handle billing webhooks
Webhooks are processed at /api/billing/webhook. Extend the handler for custom logic:
await service.handleWebhookEvent(request, { onCheckoutSessionCompleted: async (subscription) => { // Send welcome email, provision resources, etc. }, onSubscriptionDeleted: async (subscriptionId) => { // Clean up, send cancellation email, etc. },});Next Steps
- Configure your billing schema to define your products and pricing
- Set up your payment provider: Stripe, Lemon Squeezy, or Paddle
- Handle webhooks for payment events
- Use the billing API to manage subscriptions programmatically
For advanced use cases:
- Per-seat billing for team-based pricing
- Metered usage for consumption-based billing
- Credit-based billing for AI/token systems
- Custom integrations for other payment providers