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 pro and plus plans.
  • Existing paid users are redirected to Stripe Billing Portal for plan updates, cancellation, and payment-method management.
  • Subscription provider reads GET /subscriptions/my-subscription and 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_id is empty.
  • Webhook processing is idempotent via billing_webhook_events (event_id unique).
  • Implemented webhook event handlers:
    • checkout.session.completed
    • invoice.payment_succeeded
    • invoice.payment_failed
    • invoice.payment_action_required
    • customer.subscription.updated
    • customer.subscription.deleted
  • Status mapping:
    • Stripe active / trialing -> local active
    • Stripe past_due / unpaid -> local past_due
    • Stripe canceled -> local canceled
    • Stripe incomplete / incomplete_expired -> local expired
  • PlanAccessGuard explicitly enforces status=active via subscription read model.

Current V2 Data Model (Post-Legacy Cleanup)

  • subscription_plans is 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_subscriptions and 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 IDDescriptionStepsInputExpected Result
FEAT-39-01Subscribe happy path (hosted checkout)Login -> open pricing -> choose Pro -> redirect to Stripe Checkout -> complete payment -> webhook deliveryValid authenticated user, valid Stripe test cardCheckout succeeds, webhook marks local subscription active, my-subscription reflects active status
FEAT-39-04Webhook signature validationCall webhook endpoint with tampered signatureValid payload, invalid Stripe-Signature headerRequest rejected; event not processed
FEAT-39-05Webhook idempotency replay safetySend same Stripe event twiceSame event_id payload twiceFirst event processed, second ignored without duplicate side effects
FEAT-39-083DS action-required renewal handlingTrigger invoice.payment_action_required webhook3DS-required invoice eventLocal 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_due state.

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.