Billing
Billing uses one active provider, a local catalog, setup checks, attempt-safe checkout, portal access, webhooks, and local finance records.
On this page
Where to look
| Area | Maintainer 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, src/app/api/webhooks/lemonsqueezy |
| Config | Implementation referencesrc/features/billing/server/catalog/configuration.ts, pricing.ts, src/features/billing/server/runtime/provider.ts, enabled-models.ts, readiness.ts, status.ts |
| Providers | Implementation referencesrc/features/billing/server/providers/stripe, src/features/billing/server/providers/lemonsqueezy |
| 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.tssrc/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.tssrc/features/billing/server/runtime/enabled-models.tssrc/features/billing/server/runtime/provider.tssrc/features/billing/server/runtime/policy.tssrc/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.tssrc/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.tssrc/features/billing/server/providers/stripe/webhooks/handler.tssrc/lib/security/webhooks/request.tssrc/lib/security/webhooks/processing.ts
Reference paths are relative to the Shipflash-Product checkout.
Checkout flow
- 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
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.tssrc/features/billing/server/runtime/policy.ts
- 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.tssrc/features/billing/server/catalog/configuration.ts
- 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.
Relevant Product code
src/features/billing/server/providers/stripe/checkout.tssrc/features/billing/server/providers/lemonsqueezy/checkout.ts
Webhook processing flow
- 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.
Relevant Product code
src/app/api/webhooks/stripe/billing/route.tssrc/lib/security/webhooks/request.ts
- 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.tssrc/lib/security/webhooks/processing.ts
- 3
Dispatch by event type
The Stripe handler routes checkout, customer, subscription, invoice, charge, refund, and dispute events to focused handlers.
Relevant Product code
src/features/billing/server/providers/stripe/webhooks/handler.ts
- 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.
Relevant Product code
src/features/billing/server/providers/stripe/webhooks/handler.tssrc/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.
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
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.tssrc/features/billing/server/checkout/service.tssrc/app/api/webhooks/stripe/billing/route.ts
Verify
- pnpm run test:features
- pnpm run quality
Billing models
| Model | Use it for | Provider support |
|---|---|---|
| licensed_recurring | Fixed monthly or yearly plans. | Stripe and Lemon Squeezy |
| licensed_one_time | Lifetime access or one-time purchases. | Stripe and Lemon Squeezy |
| metered_usage | Recurring plans where usage affects price. | Stripe and Lemon Squeezy |
| base_plus_usage | Base recurring plans with metered add-ons. | Stripe and Lemon Squeezy |
| prepaid_credits | Repeatable one-time credit packs tracked atomically in the local ledger. | Stripe and Lemon Squeezy |
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 BILLING_PROVIDER, BILLING_ENABLED_MODELS, BILLING_PRESET, and BILLING_CATALOG_JSON as the billing source of configuration.
- 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 for both providers.
- Keep single-purchase checkout claims attempt-scoped so a recovered or duplicate request cannot create two paid sessions for the same plan.