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 PolarNEXT_PUBLIC_BILLING_PROVIDER=polarThe 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
| Name | Meaning | Typical examples | Notes |
|---|---|---|---|
customerId | Provider customer ID | Stripe: cus_… | Used for portals, entitlements, usage meters. Stripe supports separate user and org customers. |
subscriptionId | Subscription identifier used in API calls (cancel/restore/upgrade) | Implementation-defined | Important: this is delegated to the active provider implementation. In the kit UI, it is sourced from subscription.providerSubscriptionId. |
providerSubscriptionId | Provider’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
billingresource.
Common mapping used across the kit:
| Permission | Enables |
|---|---|
billing:read | View billing page and list subscriptions |
billing:create | Start checkout / upgrade subscription |
billing:update | Open customer portal, restore subscription |
billing:delete | Cancel subscription |
Required environment variables (high level)
NEXT_PUBLIC_BILLING_PROVIDER:stripe(default) orpolarNEXT_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 objectawait 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:
- Checkout - Stripe Checkout sessions for new subscriptions
- Trials - Automatic trial period management
- Active Subscriptions - Ongoing subscription monitoring
- Upgrades/Downgrades - Plan changes with prorated billing
- Cancellations - Cancel at period end with reactivation option
- Payment Failures - Automatic retry and notification handling
Multi-Tenant Support
Billing works seamlessly across account types:
// Personal account billingconst referenceId = user.id;// Organization billingconst 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=stripeSTRIPE_SECRET_KEY=sk_test_...STRIPE_WEBHOOK_SECRET=whsec_...# For PolarNEXT_PUBLIC_BILLING_PROVIDER=polarPOLAR_ACCESS_TOKEN=polar_at_...POLAR_WEBHOOK_SECRET=whsec_...POLAR_ENVIRONMENT=sandboxSee 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
- Run the development server:
pnpm dev - Navigate to
/settings/billing - Select a plan and proceed to checkout
- For Stripe: Use test card
4242 4242 4242 4242 - 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