Billing

Billing uses one active provider edition, a local catalog, setup checks, attempt-safe checkout, portal access, webhooks, a durable inbox, and local finance records.

On this page

Where to look

AreaBuilder reference
Routes
Implementation referencesrc/app/(dashboard)/billing, /activity, /plans, /billing/history, /billing/setup, /billing/customers, /billing/customers/[providerCustomerId]
Public API
Implementation referencesrc/app/api/billing/checkout, /api/billing/portal, /api/billing/status
Admin API
Implementation referencesrc/app/api/system/billing/export, customers/[providerCustomerId]/sync, refunds, subscriptions/[providerSubscriptionId]
Webhooks
Implementation referencesrc/app/api/webhooks/stripe/billing, /api/webhooks/lemonsqueezy, /api/webhooks/dodo, /api/webhooks/polar, /api/webhooks/creem
Worker
Implementation referencesrc/app/api/cron/billing-webhooks, src/features/billing/server/webhooks/worker.ts
Config
Implementation referencesrc/config/product-distribution.ts, src/features/billing/server/catalog/configuration.ts, src/features/billing/server/runtime/provider.ts, enabled-models.ts, readiness.ts, status.ts
Providers
Implementation referencesrc/features/billing/server/providers/stripe, lemonsqueezy, dodo, polar, creem
Persistence
Implementation referencesrc/features/billing/server/persistence, src/features/billing/server/persistence/finance
Exports
Implementation referencesrc/features/billing/server/export
UI
Implementation referencesrc/features/billing/ui/hub, pricing, customer-detail, settings, setup
Implementation reference4 areas

These Product code locations explain how the documented behavior is implemented. Expand them when you are ready to customize or maintain this area.

Checkout request boundary

The API route stays thin and delegates validation, rate limiting, provider selection, session creation, logging, and analytics to the checkout service.

  • src/app/api/billing/checkout/route.ts
  • src/features/billing/server/checkout/service.ts

Runtime billing configuration

The local catalog, enabled billing models, active provider, presets, and setup checks decide what checkout can sell.

  • src/features/billing/server/catalog/configuration.ts
  • src/features/billing/server/runtime/enabled-models.ts
  • src/features/billing/server/runtime/provider.ts
  • src/features/billing/server/runtime/policy.ts
  • src/features/billing/server/runtime/readiness.ts

Provider adapters

Each provider implementation stays behind a provider-specific adapter so app code works with app-owned billing concepts. Buyer distributions include one provider; the staging source composes all adapters for verification.

  • src/features/billing/server/providers/stripe/checkout.ts
  • src/features/billing/server/providers/lemonsqueezy/checkout.ts
  • src/features/billing/server/providers/dodo/checkout.ts
  • src/features/billing/server/providers/polar/checkout.ts
  • src/features/billing/server/providers/creem/checkout.ts

Webhook safety and projection

Provider webhook routes verify raw payloads, claim event rows, reject duplicate processed events, and enqueue work for the billing worker to project with retry and dead-letter handling.

  • src/app/api/webhooks/stripe/billing/route.ts
  • src/app/api/webhooks/lemonsqueezy/route.ts
  • src/app/api/webhooks/dodo/route.ts
  • src/app/api/webhooks/polar/route.ts
  • src/app/api/webhooks/creem/route.ts
  • src/app/api/cron/billing-webhooks/route.ts
  • src/features/billing/server/webhooks/worker.ts
  • src/features/billing/server/providers/stripe/webhooks/handler.ts
  • src/lib/security/webhooks/request.ts
  • src/lib/security/webhooks/processing.ts

Reference paths are relative to the delivered product checkout.

Checkout flow

  1. 1

    Validate the request boundary

    The checkout route creates a request id, reads the trusted origin, and passes request metadata into the billing checkout service.

    Relevant Product code
    • src/app/api/billing/checkout/route.ts
  2. 2

    Resolve runtime policy

    The service reads billing policy, allowed origins, enabled models, provider selection, and setup status before accepting a checkout request.

    Relevant Product code
    • src/features/billing/server/checkout/service.ts
    • src/features/billing/server/runtime/policy.ts
  3. 3

    Resolve catalog selection

    The request body is matched to a product, price, billing model, and provider-compatible checkout selection.

    Relevant Product code
    • src/features/billing/server/checkout/request.ts
    • src/features/billing/server/catalog/configuration.ts
  4. 4

    Create the provider session

    The selected provider adapter creates the checkout session and returns an app response without leaking provider SDK details across the boundary.

    Relevant Product code
    • src/features/billing/server/providers/stripe/checkout.ts
    • src/features/billing/server/providers/lemonsqueezy/checkout.ts
    • src/features/billing/server/providers/dodo/checkout.ts
    • src/features/billing/server/providers/polar/checkout.ts
    • src/features/billing/server/providers/creem/checkout.ts

Webhook processing flow

  1. 1

    Verify before processing

    The selected route rate-limits the webhook, validates provider config, reads the raw text body, and verifies the provider signature before touching local billing records.

    Relevant Product code
    • src/app/api/webhooks/stripe/billing/route.ts
    • src/app/api/webhooks/lemonsqueezy/route.ts
    • src/app/api/webhooks/dodo/route.ts
    • src/app/api/webhooks/polar/route.ts
    • src/app/api/webhooks/creem/route.ts
    • src/lib/security/webhooks/request.ts
  2. 2

    Claim the event row

    Existing processed events are returned as duplicates, stale processing leases can be reclaimed, and new events are inserted before dispatch.

    Relevant Product code
    • src/app/api/webhooks/stripe/billing/route.ts
    • src/app/api/webhooks/lemonsqueezy/route.ts
    • src/app/api/webhooks/dodo/route.ts
    • src/app/api/webhooks/polar/route.ts
    • src/app/api/webhooks/creem/route.ts
    • src/lib/security/webhooks/processing.ts
  3. 3

    Dispatch by event type

    The selected provider handler routes checkout, customer, subscription, invoice, charge, refund, dispute, usage, or credit events to the supported projection handlers.

    Relevant Product code
    • src/features/billing/server/providers/stripe/webhooks/handler.ts
    • src/features/billing/server/providers/lemonsqueezy/webhooks/handler.ts
    • src/features/billing/server/providers/dodo/webhooks.ts
    • src/features/billing/server/providers/polar/webhooks.ts
    • src/features/billing/server/providers/creem/webhooks.ts
  4. 4

    Project locally and notify best-effort

    The worker updates local records as the dashboard source of truth, then queues related notifications without blocking billing projection writes.

    Relevant Product code
    • src/features/billing/server/webhooks/worker.ts
    • src/features/notifications/server/delivery/queue/notify-queue.ts

Active provider resolver

Use this example as a starting point, then adapt it to your product's rules and configuration.

Environment remains the default source of truth. Settings can override it only when the provider is not locked to env.

ts
export async function getBillingProvider(): Promise<BillingProvider> {
  const envProvider = getBillingProviderFromEnv();
  if (isBillingProviderLockedToEnv()) {
    return envProvider;
  }

  try {
    const { getAppSettings } = await import("@/features/settings/server/queries/settings");
    const settings = await getAppSettings();
    const fromSettings = normalizeProvider(settings.billing?.provider);
    return fromSettings ?? envProvider;
  } catch {
    return envProvider;
  }
}
Source reference
  • src/features/billing/server/runtime/provider.ts

Recipe: add a billing product

Add a product without breaking provider compatibility, checkout, setup checks, or dashboard projections.

Implementation locations

Inspect

  • src/features/billing/server/catalog/configuration.ts
  • src/features/billing/server/runtime/enabled-models.ts
  • src/features/billing/server/runtime/provider.ts
  • src/features/billing/server/runtime/readiness.ts
  • src/features/billing/server/checkout/request.ts

Edit

  • BILLING_CATALOG_JSON
  • BILLING_ENABLED_MODELS
  • BILLING_PRESET
  • src/features/billing/server/catalog/configuration.ts

Steps

  • Add the provider product and price ids first.
  • Add the catalog entry with the billing model that matches the provider price type.
  • Keep product lookup, checkout metadata, and dashboard copy aligned.
  • Run billing setup checks before accepting live payments.

Verify

  • pnpm run test:features
  • pnpm run quality
  • Open /billing/setup and run a test checkout in provider test mode.

Common mistakes

  • Do not treat app-local prepaid credits as a provider balance.
  • Do not store provider customer ids as profile ids.
  • Do not bypass webhook projection tables for dashboard state.

AI prompt: billing change

AI prompt
Change the billing module to add <product, price, meter, credit pack, portal action, or reconciliation behavior> for <customer or operator>. Before editing, identify the provider edition and branch, provider mode, supported billing model, catalog identifier, customer outcome, non-goals, and whether the change affects checkout, portal, usage, refunds, entitlements, webhooks, the billing worker, notifications, schema, or operations. Inspect the provider/distribution lock, product selection/configurator contract, catalog schema and runtime, enabled-model policy, readiness checks, checkout request and service, claim/idempotency state, the selected provider adapter, matching webhook route, raw-body signature verification, durable event inbox, billing worker, local projection tables, generated types, environment template, and nearby tests. Trace one event from signed receipt through claim, projection, entitlement/credit update, and customer-visible history before changing code. Preserve one-provider-per-buyer-branch isolation, provider/catalog/model compatibility, server-owned pricing, canonical env names, secret separation, signature verification, duplicate and out-of-order handling, bounded retry/concurrency, request-id/provider-event logging, and customer-safe errors. Never add an unrelated provider SDK, switch the provider by changing buyer runtime settings, trust UI-only entitlements, accept unverified payloads, log secrets or raw payloads, or change migration history to select modules. Add regression coverage for invalid provider/model/catalog values, duplicate and stale events, retries, partial projection, unauthorized actions, and unsupported provider capabilities. Return the plan, changed files, preserved invariants, schema/env/provider/route/Edge/worker impact, exact verification commands and results, provider test-mode steps, operational rollout, repair or rollback plan, and remaining risks. Stop if a provider, commercial model, catalog resource, or data-repair decision is missing.
Inspect first
  • src/config/product-distribution.ts
  • src/config/product-selection.ts
  • src/features/billing/server/catalog/configuration.ts
  • src/features/billing/server/runtime/enabled-models.ts
  • src/features/billing/server/runtime/readiness.ts
  • src/features/billing/server/checkout/service.ts
  • src/features/billing/server/providers/runtime/selected.ts
  • src/app/api/webhooks/stripe/billing/route.ts
  • src/app/api/webhooks/lemonsqueezy/route.ts
  • src/app/api/webhooks/dodo/route.ts
  • src/app/api/webhooks/polar/route.ts
  • src/app/api/webhooks/creem/route.ts
  • src/app/api/cron/billing-webhooks/route.ts
  • src/features/billing/server/webhooks/worker.ts

Verify

  • pnpm run test:features
  • pnpm run quality

Billing models

ModelUse it forProvider support
licensed_recurringFixed monthly or yearly plans.Stripe, Lemon Squeezy, Dodo, Polar, and Creem
licensed_one_timeLifetime access or one-time purchases.Stripe, Lemon Squeezy, Dodo, Polar, and Creem
metered_usageRecurring plans where usage affects price.Stripe, Lemon Squeezy, Dodo, and Polar
base_plus_usageBase recurring plans with metered add-ons.Stripe, Lemon Squeezy, Dodo, and Polar
prepaid_creditsRepeatable one-time credit packs tracked atomically in the local ledger.Stripe, Lemon Squeezy, Dodo, Polar, and Creem

Billing rules

  • Validate webhook signatures before touching local records.
  • Make checkout and webhook writes safe for duplicate and concurrent delivery.
  • Keep provider ids separate from profile ids.
  • Use the provider branch, BILLING_ENABLED_MODELS, BILLING_PRESET, and BILLING_CATALOG_JSON as the billing source of configuration.
  • Do not switch providers by changing a buyer branch's runtime env value.
  • Omit trial and credit metadata when it does not apply instead of sending blank provider values.
  • Use /billing/setup before accepting live payments.
  • Treat prepaid credits as an app-local ledger.
  • Schedule /api/cron/billing-webhooks once and monitor retries and dead letters.
  • Keep single-purchase checkout claims attempt-scoped so a recovered or duplicate request cannot create two paid sessions for the same plan.