Stripe’s docs are excellent until your first enterprise buyer asks for annual invoicing, seat changes mid-cycle, and audit logs. These patterns keep billing logic maintainable.
One source of truth in your database
Stripe holds payment state; your app holds product state. We mirror subscription IDs, plan tiers, and seat counts locally and reconcile via webhooks — never by trusting client-side checkout success alone.
Webhooks are idempotent
The same invoice.paid event may arrive twice. Handlers check event IDs or use idempotency keys before mutating seats or unlocking features. We log unhandled event types instead of silently ignoring them.
Proration rules are product decisions
Whether upgrades bill immediately or at period end is not a Stripe setting — it is a product rule. We document it in the PRD and encode it once in a billing service, not scattered across Route Handlers.
Failed payments need UX, not just emails
Dunning emails help, but the portal should show payment status, offer card update, and gracefully degrade features according to policy — before support tickets pile up.
Test clocks in staging
Stripe test clocks let us simulate renewals and failures without waiting thirty days. We run them before every major billing change.
Billing integrations are integration projects, not checkbox tasks. A thin, well-tested layer around Stripe saves years of spreadsheet reconciliation later.
