Shipflash logoDocs

Billing

Billing uses one active provider, a local catalog, setup checks, checkout, portal access, webhooks, and local finance records.

On this page

Where to look

AreaPaths
Routessrc/app/(dashboard)/billing, /billing/history dedicated activity page, /billing/setup, /billing/customers, /billing/customers/[providerCustomerId]
Public APIsrc/app/api/billing/checkout, /api/billing/portal, /api/billing/status
Admin APIsrc/app/api/system/billing/export, customers/[providerCustomerId]/sync, refunds, subscriptions/[providerSubscriptionId]
Webhookssrc/app/api/webhooks/stripe/billing, src/app/api/webhooks/lemonsqueezy
Configsrc/features/billing/server/catalog/configuration.ts, pricing.ts, src/features/billing/server/runtime/provider.ts, enabled-models.ts, readiness.ts, status.ts
Providerssrc/features/billing/server/providers/stripe, src/features/billing/server/providers/lemonsqueezy
Persistencesrc/features/billing/server/persistence, src/features/billing/server/persistence/finance
Exportssrc/features/billing/server/export
UIsrc/features/billing/ui/hub, pricing, customer-detail, settings, setup

Code-backed billing map

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

Stripe and Lemon Squeezy logic stays behind provider-specific modules so app code works with app-owned billing concepts.

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

Webhook safety and projection

Webhook routes verify provider payloads, claim event rows, reject duplicate processed events, retry stale processing leases, then dispatch to provider handlers.

  • src/app/api/webhooks/stripe/billing/route.ts
  • src/features/billing/server/providers/stripe/webhooks/handler.ts
  • src/lib/security/webhooks/request.ts
  • src/lib/security/webhooks/processing.ts

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.

    • 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.

    • 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.

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

    Create the provider session

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

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

Webhook processing flow

  1. 1

    Verify before processing

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

    • src/app/api/webhooks/stripe/billing/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.

    • src/app/api/webhooks/stripe/billing/route.ts
    • src/lib/security/webhooks/processing.ts
  3. 3

    Dispatch by event type

    The Stripe handler routes checkout, customer, subscription, invoice, charge, refund, and dispute events to focused handlers.

    • src/features/billing/server/providers/stripe/webhooks/handler.ts
  4. 4

    Project locally and notify best-effort

    Billing handlers update local records as the dashboard source of truth, then queue related notifications without blocking billing projection writes.

    • src/features/billing/server/providers/stripe/webhooks/handler.ts
    • src/features/notifications/server/delivery/queue/notify-queue.ts

Active provider resolver

Source: src/features/billing/server/runtime/provider.ts

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;
  }
}

Recipe: add a billing product

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

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 enable prepaid credits with Lemon Squeezy.
  • 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 or price>. Inspect the active billing provider, catalog, enabled models, checkout request resolver, setup checks, and webhook projection code first. Keep the route entrypoints thin. Do not add fallback env names. Show which provider ids, env vars, tests, and dashboard surfaces changed.

Inspect first

  • src/features/billing/server/catalog/configuration.ts
  • src/features/billing/server/checkout/service.ts
  • src/app/api/webhooks/stripe/billing/route.ts

Verify

  • pnpm run test:features
  • pnpm run quality

Billing models

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

Billing rules

  • Validate webhook signatures before touching local records.
  • Make webhook writes safe for duplicate and concurrent delivery.
  • Keep provider ids separate from profile ids.
  • Use BILLING_PROVIDER, BILLING_ENABLED_MODELS, BILLING_PRESET, and BILLING_CATALOG_JSON as the billing source of configuration.
  • Use /billing/setup before accepting live payments.
  • Do not enable prepaid credits with Lemon Squeezy.