Billing & Payments
Sell subscriptions, one-time products, and credits through one provider-agnostic billing layer where every balance and plan belongs to a user.
Billing lives in @repo/payments, is gated by config.payment in packages/config/src/index.ts, and is wired into auth through the Better Auth billing plugin. One provider-agnostic layer covers recurring subscriptions, one-time products, and prepaid credits, so checkout, webhooks, and entitlement checks stay identical whether you run Stripe or Polar.
Money always lives on a user. There is no organization balance, plan, or customer record. When an organization is active its owner is the payer, so members share one funded workspace; otherwise the session user pays for themselves. Billing(session) resolves it - see Organizations.
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. |
With enabled: false, createBillingPlugins() returns [], the provider barrel resolves to a no-op that throws on any charge, and checkout plus the dashboard credit UI disappear.
Disabling billing does not hide the landing page's pricing section - plan cards always render from pricingConfig. Drop <Pricing> from app/[locale]/(marketing)/page.tsx if you sell nothing.
Choosing a provider
- Stripe for the broadest ecosystem and the lowest fees when you handle your own tax. Auto top-up and chargeback suspension are Stripe-only.
- Polar when you want a merchant of record to take on VAT/sales-tax remittance and disputes for you.
Capabilities
Plans and pricing
Recurring subscription tiers (free, starter, pro), intervals, currency, and how revenue is recorded.
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.
Organizations
Shared workspaces funded by their owner - who pays, who may check out, what members see.
Webhooks and checkout guards
Webhooks reconcile subscription, payment, and credit state: the active provider owns one endpoint and one signing secret, and nothing reconciles until you register it. authorizeReference runs server-side before any charge:
| Guard | Behavior |
|---|---|
| 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 anyone already on a lifetime plan. |
| Payer identity | The billing reference must equal the caller's own user id (canUserPurchase). Inside an organization the reference is the owner, so only the owner can buy. |
Read entitlements server-side through Billing(...) and the Better Auth billing plugin - the active subscription, owned products, or credit balance - never from client state.
On localhost the provider cannot reach your webhook endpoint, so a test checkout reaches the success page but grants nothing until the event arrives. Forward webhooks in dev with stripe listen --forward-to localhost:3000/api/auth/stripe/webhook (set STRIPE_WEBHOOK_SECRET to the secret it prints). Polar has an equivalent CLI forwarder.
There is no organization-billing mode to turn on: funding follows the owner, so transferring the organization is the only way to change who pays. Providers retry webhooks for days, so a checkout started before per-organization billing was removed can still deliver an event carrying organization metadata; getBillingFromMetadata resolves that organization to its owner and credits the payment there, so no in-flight payment is lost.