โ† Pattern library

Commerce

Subscription Billing

Recurring payments on a schedule (monthly, yearly), with self-serve plan changes, cancellations, and dunning when payments fail.

๐ŸŒณ

When to use this

For SaaS, content subscriptions, memberships, or any recurring-revenue model. Always use a billing provider (Stripe Billing, Paddle), never roll your own.

commercesubscriptionrecurringbillingstripe-billing
โœจ Built using these library patterns:
subscription

What I assumed

I made these guesses to fill gaps. Let me know if any are wrong.

    Flow diagram

    Step-by-step recipe

    Copy this and paste into Cursor, Claude Code, or v0.

    PATTERN: Subscription Billing
    INPUT: user, plan_id (e.g., "pro_monthly"), payment_method
    OUTPUT: active_subscription | billing_error
    
    SUBSCRIBE_STEPS:
      1. User picks a plan from pricing page
      2. Show summary: plan, billing interval, first charge amount, trial details if any
      3. Collect payment method (composable_with: checkout)
      4. Create customer + subscription in billing provider (Stripe Subscription)
      5. IF trial โ†’ mark active with trial_end date, no charge yet
      6. IF no trial โ†’ first charge happens immediately
      7. Provider sends webhook: subscription.created
      8. Server updates user record: tier, status, current_period_end
      9. Grant access to features for this tier
      10. Send welcome email + invoice
    
    BILLING_CYCLE_STEPS (recurring, automatic):
      1. Provider attempts charge on current_period_end
      2. IF succeeds โ†’ extend current_period_end, send invoice
      3. IF fails โ†’ enter dunning (retry 3x over 14 days)
      4. IF dunning exhausted โ†’ cancel subscription, downgrade access
      5. Send appropriate email at each step (success, retry, final-warning)
    
    SELF_SERVICE_STEPS:
      1. User opens billing portal
      2. Can: change plan, update card, cancel, view invoices
      3. Plan changes: prorate immediately or at period end (your choice)
      4. Cancellation: keep access until current_period_end, then downgrade
    
    ERROR_HANDLING:
      - Charge fails (insufficient funds, expired card) โ†’ enter dunning, email user
      - User updates card mid-dunning โ†’ retry immediately
      - Webhook missed โ†’ daily reconciliation job syncs subscription state
      - Plan change race condition โ†’ use idempotency keys
    
    EXTENSION_POINTS:
      - Coupon for first month/year (composable_with: ["coupon"])
      - Refund partial period on cancel (composable_with: ["refund"])
      - Dunning emails sequence (composable_with: ["dunning"])
      - Annual upgrade prompt for monthly users
    

    States โ€” how things change

    StateDescriptionTransitions
    TrialActive without charge
    • Trial endsโ†’Active
    ActivePaid and current
    • Charge failsโ†’Past due
    • User cancelsโ†’Cancel pending
    Past dueIn dunning retry cycle
    • Recoveredโ†’Active
    • Dunning exhaustedโ†’Cancelled
    Cancel pendingWill end at period_end, access kept until then
    • Period endsโ†’Cancelled
    CancelledAccess removedterminal

    Easy-to-miss situations

    The kinds of edge cases that break demos.

    • What if a user cancels but wants to come back later?

      medium

      Lose data and history if you delete on cancel.

      Suggested handling: Soft-cancel โ€” keep account and data. Re-subscribing restores access seamlessly. Show "Welcome back!" message. Keep data 12+ months before hard-delete.

    • What if a user upgrades mid-cycle?

      medium

      How much to charge for the prorated period?

      Suggested handling: Use Stripe's automatic proration. Charge difference for remaining days. Email immediate invoice showing the calculation transparently.

    • What if billing fails on a paying customer's expired card?

      high

      Lose paying customer if you don't recover gracefully.

      Suggested handling: Email 7 days BEFORE card expires. On failure, send dunning emails over 14 days (3 retries). Provide one-click "Update card" link. Don't immediately cancel.

    • What if a user's currency changes (moved countries)?

      low

      Pricing might be off, payment method invalid in new country.

      Suggested handling: Detect via IP + payment method country. Offer to switch billing currency on next renewal. Don't force change mid-period.

    • What if a user is double-billed due to webhook race?

      high

      Trust catastrophe, immediate refund + apology required.

      Suggested handling: Use idempotency keys on every billing operation. Daily reconciliation job catches doubles. If detected, auto-refund + email apology before user notices.

    Composes well with

    Combine these patterns when you need a richer flow.

    Build a flow starting from this pattern โ†’