Billing & Payments
Sell subscriptions, one-time products, and credits through one provider-agnostic billing layer.
Billing lives in @repo/payments and is gated by config.payment in packages/config/src/index.ts, wired into auth through the Better Auth billing plugin. One provider-agnostic layer covers recurring subscriptions, one-time products, prepaid credits, and per-organization billing - so checkout, webhooks, and entitlement checks stay identical whether you run Stripe or Polar.
The flag
config.payment is a discriminated union: { enabled: false } or { enabled: true; provider; bannedCountries? }.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Master switch for the billing surface. |
provider | "stripe" | "polar" | "stripe" | Selected at build time; inactive provider code is bundler-pruned. |
bannedCountries | string[] | ["BY","CU","IR","KP","RU","SY"] | ISO codes blocked from checkout. |
When enabled is false, createBillingPlugins() returns [], the provider barrel resolves to a no-op that throws on any charge, and checkout plus dashboard credits chrome disappear.
Disabling billing does not hide the marketing pricing page - those plan cards always render from pricingConfig. Remove or edit that page separately if you don't sell anything.
Capabilities
Each area maps to its own provider-agnostic module and dashboard view.
Plans and pricing
Recurring subscription tiers (free, starter, pro), intervals, and currency.
One-time products
Non-recurring purchases - add-ons, lifetime deals, credit packs - that grant ownership.
Credits
Prepaid, metered balance: grant, spend, and auto top-up usage-based units.
Organization billing
Switch the billing scope so the team - not the individual user - owns the subscription.
Webhooks & checkout guards
Webhooks reconcile state; authorizeReference runs server-side before any charge.
| Guard | Behavior |
|---|---|
| Webhooks | Keep subscription, payment, and credit state in sync. The active provider owns one endpoint and one signing secret - register it or events never reconcile. |
| Banned countries | Reject checkout when the CDN country header (cf-ipcountry, x-vercel-ip-country, cloudfront-viewer-country) or the user-profile country is in bannedCountries. |
| Lifetime plans | Block new subscription checkouts for any entity already on a lifetime plan. |
| Org role | When billingScope is organization, limit purchases to owner / admin members; otherwise the reference must equal the user's own ID. |
Organization billing requires config.tenancy.multiTenant: true and config.tenancy.billingScope: "organization". Both default to user-scoped (billingScope: "user"), so the team owns nothing until you opt in.
Frequently asked questions
Stripe or Polar? Pick Stripe for the broadest ecosystem and lowest fees when you handle your own tax. Pick Polar when you want a merchant-of-record to take on VAT/sales-tax remittance for you.
How do I disable billing entirely?
Set config.payment.enabled to false. Checkout, the active provider, and dashboard billing UI disappear; remember the marketing pricing page is separate content.
Where do entitlement checks happen?
Server-side, through Billing(...) and the Better Auth billing plugin - read the active subscription, owned products, or credit balance rather than trusting client state.