Enable providers deliberately
All providers start off. Set BILLING_PROVIDERS to the required comma-separated names: stripe, polar, revenuecat. 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. RevenueCat connects store purchases on mobile. If both web providers are enabled, BILLING_CHECKOUT_PROVIDER chooses new checkouts; existing customers retain their provider portal. Visibility separately uses billing.web and billing.mobile.
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. Device-reported purchases alone grant nothing. RevenueCat reconciliation replaces only its provider rows; 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 |
| RevenueCat | /api/billing/revenuecat | REVENUECAT_SECRET_API_KEY, REVENUECAT_WEBHOOK_AUTHORIZATION, REVENUECAT_WEBHOOK_SIGNING_SECRET |
Configure RevenueCat app and entitlement IDs too. Public platform SDK keys belong in the mobile environment; server keys do not. 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. RevenueCat verification has a freshness bound. Native purchases require a development build with the SDK, not just a successful TypeScript check. See ADR-0004.