Billing & Subscriptions

Learn how to implement and manage billing with Stripe or Polar via Better Auth

The SaaS Kit includes a complete billing and subscription management system with multi-provider support. This section covers everything you need to monetize your application with subscription-based billing.

Overview

The billing system provides:

  • Multi-Provider Billing - Switch between Stripe and Polar via environment variable
  • Subscription Management - Create, upgrade, cancel, and restore subscriptions
  • Multi-Tenant Billing - Support for both personal accounts and organizations
  • Flexible Pricing - Configure multiple products, plans, and billing intervals
  • Per-seat billing - Quantity-based subscriptions using seat count
  • Trial Periods - Built-in support for free trials with automatic conversion
  • Customer Portal - Provider-hosted portal for self-service billing management
  • Lifecycle Hooks - Extensible hooks for subscription events
  • Permission-Based - Role-based authorization for billing operations

Provider Selection

Switch billing providers via environment variable:

# Use Stripe (default)
NEXT_PUBLIC_BILLING_PROVIDER=stripe
# Use Polar
NEXT_PUBLIC_BILLING_PROVIDER=polar

The same application code works with either provider - only configuration changes.

Key Concepts (Context, IDs, Permissions)

Billing in Makerkit is contextual: the same UI and APIs work for personal accounts and organizations. Understanding the identifiers below makes it much easier to debug and extend billing safely.

Context: referenceId + customerType

  • referenceId: The owner of the subscription. It is either:
    • A user ID for personal billing
    • An organization ID for organization billing
  • customerType: Which billing “entity” the provider should use:
    • 'user' for personal billing
    • 'organization' for organization billing (not all providers support true org customers)

In the web app, the billing page resolves this from the current account context:

  • Personal: referenceId = session.user.id, customerType = 'user'
  • Organization: referenceId = session.session.activeOrganizationId, customerType = 'organization'

IDs: customerId, subscriptionId, providerSubscriptionId

NameMeaningTypical examplesNotes
customerIdProvider customer IDStripe: cus_…Used for portals, entitlements, usage meters. Stripe supports separate user and org customers.
subscriptionIdSubscription identifier used in API calls (cancel/restore/upgrade)Implementation-definedImportant: this is delegated to the active provider implementation. In the kit UI, it is sourced from subscription.providerSubscriptionId.
providerSubscriptionIdProvider’s subscription ID (when available)Stripe: sub_…Exposed on the unified Subscription returned by billing.listSubscriptions.

Permissions: personal vs organization billing

  • Personal billing: the current user can manage their own billing.
  • Organization billing: operations are gated by Better Auth org permissions on the billing resource.

Common mapping used across the kit:

PermissionEnables
billing:readView billing page and list subscriptions
billing:createStart checkout / upgrade subscription
billing:updateOpen customer portal, restore subscription
billing:deleteCancel subscription

Required environment variables (high level)

  • NEXT_PUBLIC_BILLING_PROVIDER: stripe (default) or polar
  • NEXT_PUBLIC_SITE_URL: required for server-side return URLs (must be a valid absolute URL, e.g. https://yourdomain.com)

See Stripe Setup and Polar Setup for provider-specific variables.

Architecture

The billing implementation consists of several key packages:

packages/
├── billing/
│ ├── api/ # @kit/billing-api - Unified BillingClient
│ ├── core/ # @kit/billing - Core types and registry
│ ├── config/ # @kit/web-billing-config - App config
│ ├── stripe/ # @kit/billing-stripe - Stripe provider
│ ├── polar/ # @kit/billing-polar - Polar provider
│ └── ui/ # @kit/billing-ui - UI components
├── better-auth/
│ └── src/plugins/
│ └── billing.ts # Better Auth billing plugin (provider factory)

System Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│ UI LAYER │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ pricing-table │ │ checkout-button │ │ subscription- │ │
│ │ │ │ │ │ card │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ ┌────────┴────────┐ ┌────────┴────────┐ ┌────────┴────────┐ │
│ │ plan-picker │ │ billing-portal- │ │ cancel-dialog │ │
│ │ │ │ button │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ @kit/billing-ui │
└───────────────────────────────────┬─────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ SERVER ACTIONS │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ billing-server-actions.ts │ │
│ │ • createCheckoutSessionAction │ │
│ │ • createBillingPortalSessionAction │ │
│ │ • cancelSubscriptionAction │ │
│ │ • restoreSubscriptionAction │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────┬─────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ BILLING CLIENT │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ getBilling(auth) → BillingClient │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Methods: │ Utilities: │ │ │
│ │ │ • checkout() │ • checkPlanLimit() │ │ │
│ │ │ • portal() │ • checkEntitlement() │ │ │
│ │ │ • listSubscriptions() │ • recordUsage() │ │ │
│ │ │ • cancelSubscription() │ • capabilities │ │ │
│ │ │ • restoreSubscription() │ • getProvider() │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ @kit/billing-api │
└───────────────────────────────────┬─────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ PROVIDER REGISTRY │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ NEXT_PUBLIC_BILLING_PROVIDER env var → Provider Factory (lazy) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────┬─────────────────────────────────────────┘
┌───────────────┴───────────────┐
▼ ▼
┌──────────────────────────────────┐ ┌──────────────────────────────────┐
│ STRIPE PROVIDER │ │ POLAR PROVIDER │
│ ┌────────────────────────────┐ │ │ ┌────────────────────────────┐ │
│ │ Capabilities: │ │ │ │ Capabilities: │ │
│ │ ✓ checkout │ │ │ │ ✓ checkout │ │
│ │ ✓ portal │ │ │ │ ✓ portal │ │
│ │ ✓ cancel │ │ │ │ ✗ cancel (via portal) │ │
│ │ ✓ restore │ │ │ │ ✗ restore (via portal) │ │
│ │ ✓ entitlements │ │ │ │ ✗ entitlements │ │
│ │ ✓ usage meters │ │ │ │ ✓ usage meters │ │
│ └────────────────────────────┘ │ │ └────────────────────────────┘ │
│ @kit/billing-stripe │ │ @kit/billing-polar │
└──────────────────────────────────┘ └──────────────────────────────────┘

Checkout Flow

User UI Server Action Provider
│ │ │ │
│ Click Checkout │ │ │
│────────────────────>│ │ │
│ │ createCheckoutSession │ │
│ │────────────────────────>│ │
│ │ │ billing.checkout() │
│ │ │───────────────────────>│
│ │ │ │
│ │ │ Checkout URL │
│ │ │<───────────────────────│
│ │ redirect(url) │ │
│ │<────────────────────────│ │
│ Redirect to │ │ │
│ Provider Checkout │ │ │
│<────────────────────│ │ │
│ │ │ │
│ Complete Payment │ │ │
│─────────────────────────────────────────────────────────────────────── >│
│ │ │ Webhook: subscription │
│ │ │<───────────────────────│
│ │ │ Update DB via │
│ │ │ Better Auth Plugin │
│ │ │ Execute lifecycle │
│ │ │ hooks │
│ Redirect to │ │ │
│ Success Page │ │ │
│<──────────────────────────────────────────────────────────────────────-│

Key Concept: BillingClient

The @kit/billing-api package exports a unified BillingClient that provides:

  • Core operations: checkout, portal, subscriptions, cancel/restore
  • Plan limits: check and enforce quotas (seats, projects, storage)
  • Entitlements: boolean feature gating (returns defaults if unsupported)
  • Usage meters: usage-based billing (returns defaults if unsupported)
import { getBilling } from '@kit/billing-api';
const billing = await getBilling(auth);
// All methods available on single object
await billing.checkout({ ... });
await billing.checkPlanLimit({ referenceId, limitKey, currentUsage });
await billing.checkEntitlement(customerId, 'feature-key');

Key Features

Subscription Lifecycle

The system handles the complete subscription lifecycle:

  1. Checkout - Stripe Checkout sessions for new subscriptions
  2. Trials - Automatic trial period management
  3. Active Subscriptions - Ongoing subscription monitoring
  4. Upgrades/Downgrades - Plan changes with prorated billing
  5. Cancellations - Cancel at period end with reactivation option
  6. Payment Failures - Automatic retry and notification handling

Multi-Tenant Support

Billing works seamlessly across account types:

// Personal account billing
const referenceId = user.id;
// Organization billing
const referenceId = organizationId;

The same billing code handles both scenarios through Better Auth's polymorphic subscription system.

Quick Start

1. Choose Your Provider

Set your billing provider in apps/web/.env.development:

# For Stripe (default)
NEXT_PUBLIC_BILLING_PROVIDER=stripe
STRIPE_SECRET_KEY=sk_test_...
STRIPE_WEBHOOK_SECRET=whsec_...
# For Polar
NEXT_PUBLIC_BILLING_PROVIDER=polar
POLAR_ACCESS_TOKEN=polar_at_...
POLAR_WEBHOOK_SECRET=whsec_...
POLAR_ENVIRONMENT=sandbox

See Stripe Setup or Polar Setup for detailed configuration.

2. Configure Plans

Create products in your provider's dashboard, then configure them:

import { BillingConfig } from '@kit/billing';
export const billingConfig: BillingConfig = {
products: [
{
id: 'starter',
name: 'Starter',
description: 'For individuals and small teams',
currency: 'USD',
features: ['Core features', 'Email support'],
plans: [
{
name: 'starter-monthly',
// Configure provider-specific IDs
planId: process.env.STRIPE_PRICE_STARTER_MONTHLY!,
displayName: 'Starter Monthly',
interval: 'month',
cost: 9.99,
},
],
},
],
};

Use environment variables for IDs so you can swap them in different environments. Only the active provider's configuration is used at runtime.

3. Test the Flow

  1. Run the development server: pnpm dev
  2. Navigate to /settings/billing
  3. Select a plan and proceed to checkout
  4. For Stripe: Use test card 4242 4242 4242 4242
  5. For Polar: Use the sandbox environment for testing

What's Next

Explore the billing documentation:

  • Billing Configuration - Set up products and pricing plans
  • Providers - Understand the unified billing API
  • Stripe Setup - Configure Stripe account and webhooks
  • Polar Setup - Configure Polar account and webhooks
  • Lifecycle Hooks - Extend billing with custom logic
  • Customization - Tailor billing to your business needs