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=stripe

Update 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 },
]
}

Configure per-seat billing →

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 },
]
}

Set up metered billing →

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',
}]
}

Configure one-off payments →

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:

TablePurpose
billing_customersLinks accounts to provider customer IDs
subscriptionsActive and historical subscription records
subscription_itemsLine items within subscriptions (for per-seat, metered)
ordersOne-off payment records
order_itemsItems 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.
},
});

Full webhook documentation →

Next Steps

  1. Configure your billing schema to define your products and pricing
  2. Set up Stripe as your payment provider
  3. Handle webhooks for payment events
  4. Use the billing API to manage subscriptions programmatically

For advanced use cases: