Enable providers deliberately
All providers start off. Set BILLING_PROVIDERS to the required comma-separated names: stripe, polar. Each listed provider must have its complete valid configuration. BILLING_ENVIRONMENT distinguishes SANDBOX from PRODUCTION; Stripe key mode must agree.
Stripe and Polar sell on the web. If both web providers are enabled, BILLING_CHECKOUT_PROVIDER chooses new checkouts; existing customers retain their provider portal. Visibility uses billing.web.
Entitlements and webhooks
Paid Access belongs to the User, not an Organization. The server's entitlement read model projects enabled-provider rows in the current environment. Leftover store rows (for example provider = 'revenuecat') stay in the table and are ignored. Stripe and Polar apply signed, deduplicated events in transactions.
| Provider | Webhook path | Server secrets |
|---|---|---|
| Stripe | /api/billing/stripe | STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_PRICE_IDS |
| Polar | /api/billing/polar | POLAR_ACCESS_TOKEN, POLAR_WEBHOOK_SECRET, POLAR_PRODUCT_IDS |
Raw webhook verification precedes payload parsing. A replay is a no-op, and a failed write rolls back its dedupe record so the provider can retry.
Extend and test
Add plans to the leaf billing-plans.ts, map provider product IDs in env, and add name/description translations. Checkout return URLs come from the server, never a client-supplied redirect. Test purchases in sandbox and verify billing.status, cancellation and webhook replay before enabling production visibility.
Stripe and Polar currently rely on webhooks plus the grace window; no scheduled pull reconciles their subscriptions. billing.sync re-reads that projection. See ADR-0004.