Billing architecture
/pricing (RSC) → POST /billing/checkout → Stripe Checkout (hosted)
└→ billing_customers (the user's one Stripe customer)
/settings billing section → POST /billing/portal → Customer Portal (hosted)
Stripe → POST /webhooks/stripe (raw body, signature-verified)
└→ billing_subscriptions (guarded upsert)
└→ billing_webhook_events (audit log, one row per delivery)
└→ billing_payment_hook_claims (payment-hook and
refund-cancellation dedupe)
└→ billing_disputed_customers (chargebacks, so the
lock survives arriving before the row)
└→ BILLING_PAYMENT_HOOK (your app: grant on a
one-time payment, revoke on its reversal)
any feature → BillingService.getEntitlement(userId)
billing.config.ts: env parsing, enabled/partial detection, pinned
API version.
billing.service.ts: Stripe client, Checkout/Portal sessions,
getEntitlement.
billing-customer-store.ts: billing_customers, the Stripe customer
each user's checkouts run under.
billing-webhook.service.ts: event interpretation + dispatch.
billing-reversal.service.ts: refunds and chargebacks. A refund
cancels the subscription at period end in Stripe; a chargeback locks
the row; both run onPaymentReversed.
billing-subscription-store.ts: the ONLY writer of billing state -
billing_subscriptions plus the billing_disputed_customers record
that binds it. Idempotency cursor, disputed lock, race retry,
same-second tie resolution.
billing-events.service.ts: insert-only audit trail (swept after 30
days by session-cleanup.worker).
billing-payment-hook-claim-store.ts: BILLING_PAYMENT_HOOK dedupe -
claims the Stripe event id before the hook runs - and the once-per-charge
record of a refund's cancellation (swept after 30 days by
session-cleanup.worker).
billing-payment-hook.ts: the BILLING_PAYMENT_HOOK contract your app
implements. onPaymentCompleted is required, onPaymentReversed
optional.
billing-status.ts: pure status/field mapping, isLiveStatus, and
fieldsForEvent (the row shape an event type implies).