Billing in Tanstack Start Supabase Turbo
Complete guide to implementing billing in your Tanstack Start Supabase SaaS. Configure subscriptions, one-off payments, metered usage, and per-seat pricing with Stripe.
Makerkit's billing system lets you accept payments through Stripe with a unified API. You define your pricing once in a schema, and the billing gateway routes requests to the Stripe implementation. Stripe is the only provider shipped with this kit; the gateway abstraction lets you add your own provider if you need one.
Quick Start
Set your billing provider:
VITE_BILLING_PROVIDER=stripeUpdate the database configuration to match:
UPDATE public.config SET billing_provider = 'stripe';Then configure your billing schema with your products and pricing.
Payment Provider
The kit uses Stripe. It supports multiple line items per plan, flat, per-seat, and metered pricing, and one-off payments. Stripe is not a Merchant of Record, so you are responsible for tax collection (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)└─────────────────┘ └─────────────────┘ └─────────────────┘ │ ▼┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐│ 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
These live in packages/billing/core, packages/billing/gateway, and packages/billing/stripe. The billing schema defines what you sell, and the provider package handles the API specifics.
Database Schema
Billing data is stored in five 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/src/config/billing.config.ts:
import { BillingProviderSchema, createBillingSchema } from '@kit/billing';const provider = BillingProviderSchema.parse( import.meta.env.VITE_BILLING_PROVIDER,);export default createBillingSchema({ provider, products: [ { id: 'starter', name: 'Starter', description: 'For individuals', currency: 'USD', plans: [/* ... */], }, ],});Full billing schema documentation →
Environment Variables
Stripe requires these environment variables:
STRIPE_SECRET_KEY=sk_...STRIPE_WEBHOOK_SECRET=whsec_...VITE_STRIPE_PUBLISHABLE_KEY=pk_...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 = 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 Stripe as your payment provider
- 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