Feature: Stripe Subscription Billing Foundation (FEAT-39)
Metadata
- Issue ID: FEAT-39
- Status: In Progress
- Owner: Codex
- Related PRs: ticket branch
39
Overview
Integrates Stripe as the subscription billing provider using hosted Checkout for subscription initiation and Stripe Billing Portal for lifecycle management. Webhooks are the authoritative source of subscription state and entitlement synchronization.
Frontend Behavior
- Pricing page creates Stripe Checkout sessions for
proandplusplans. - Existing paid users are redirected to Stripe Billing Portal for plan updates, cancellation, and payment-method management.
- Subscription provider reads
GET /subscriptions/my-subscriptionand hydrates entitlement state (status,requiresPaymentAction, limits, usage). - When
requiresPaymentAction=true(3DS required on renewal/off-session), pricing page displays a CTA that opens Billing Portal for payment authentication. - Sidebar billing action routes free users to pricing and paid users to Billing Portal.
Backend Behavior
- New endpoints:
POST /subscriptions/checkout-session(auth required)POST /subscriptions/billing-portal-session(auth required)POST /billing/stripe/webhook(signature-verified public endpoint)
- Stripe customer creation is automatic on first checkout if
users.stripe_customer_idis empty. - Webhook processing is idempotent via
billing_webhook_events(event_idunique). - Implemented webhook event handlers:
checkout.session.completedinvoice.payment_succeededinvoice.payment_failedinvoice.payment_action_requiredcustomer.subscription.updatedcustomer.subscription.deleted
- Status mapping:
- Stripe
active/trialing-> localactive - Stripe
past_due/unpaid-> localpast_due - Stripe
canceled-> localcanceled - Stripe
incomplete/incomplete_expired-> localexpired
- Stripe
PlanAccessGuardexplicitly enforcesstatus=activevia subscription read model.
Current V2 Data Model (Post-Legacy Cleanup)
subscription_plansis removed from runtime flow.- Plan catalog + Stripe price mapping is sourced from
subscription_plan_prices. - User entitlement state is sourced from
user_subscriptions(status, plan slug/name, limits, Stripe refs). - Per-cycle consumption is sourced from
subscription_usages.
Is this standard?
- Yes. This follows standard Stripe subscription architecture:
- Stripe webhooks are source of truth.
- Checkout + Billing Portal own billing lifecycle UX.
- Local entitlement/usage read model powers authorization and fast UI hydration.
Production Hardening Checklist
- Add/verify DB indexes for
user_subscriptionsand Stripe ID lookup fields. - Enforce one-active-subscription-per-user invariant at DB level.
- Keep webhook retry + alerting path for persistent failures.
- Monitor status-transition anomalies and webhook replay volume.
QA Test Scenarios
| Scenario ID | Description | Steps | Input | Expected Result |
|---|---|---|---|---|
| FEAT-39-01 | Subscribe happy path (hosted checkout) | Login -> open pricing -> choose Pro -> redirect to Stripe Checkout -> complete payment -> webhook delivery | Valid authenticated user, valid Stripe test card | Checkout succeeds, webhook marks local subscription active, my-subscription reflects active status |
| FEAT-39-04 | Webhook signature validation | Call webhook endpoint with tampered signature | Valid payload, invalid Stripe-Signature header | Request rejected; event not processed |
| FEAT-39-05 | Webhook idempotency replay safety | Send same Stripe event twice | Same event_id payload twice | First event processed, second ignored without duplicate side effects |
| FEAT-39-08 | 3DS action-required renewal handling | Trigger invoice.payment_action_required webhook | 3DS-required invoice event | Local status past_due, requiresPaymentAction=true, UI shows portal CTA |
Edge Cases
- Stripe webhook arrives before checkout redirect returns to frontend.
- Duplicate or out-of-order webhook delivery from Stripe retries.
- Plan price mapping missing for requested interval.
- 3DS required on off-session renewals causing temporary
past_duestate.
Notes
- Canonical implementation doc file is maintained at:
docs/features/FEAT-39-stripe-subscription-billing.md
- This docs-site page mirrors FEAT-39 for in-app navigation visibility.