Core
Billing
Subscription Lifecycle

Subscription Lifecycle

Trial → active → expired, with manual re-subscribe. Renewal is user-driven, not webhook-driven. A 5-minute cron expires stale trials/subs and (conditionally) unpublishes the catalog.

1. Overview

Every User may have at most one UserSubscription. A new OWNER is auto-assigned a 14-day trial on signup. The subscription-cron.ts scheduler runs every 5 minutes; update-expired-trials.ts flips trial/active/cancelled rows to expired when trialEndsAt or currentPeriodEnd is past. Razorpay in this codebase is for the seller's buyer checkout — there is no Razorpay subscription webhook. A user renews by manually re-subscribing, which creates a new row.

2. Architecture

3. Data model

status enum at schema.prisma:L1352. past_due is defined but unused. SubscriptionPayment is written but only read for admin revenue reporting.

4. Key flows

4.1 Signup → trial → subscribe

4.2 Cron expiry

5. Lifecycle

6. Key files

7. Env vars & config

VarRequiredPurpose
RAZORPAY_KEY_ID, RAZORPAY_KEY_SECRETyesOrder + signature verify
TZrecommendedCron timezone (defaults UTC)
SMTP_* / mail provideryes (for reminders)Trial reminder emails

8. Gotchas & troubleshooting

  • No Razorpay subscription webhook → Renewal = user manually re-subscribes. If they don't, the cron expires them. Don't look for /webhooks/razorpay/subscription; it doesn't exist.
  • Cancelled + in-period users are LOCKED OUT of gated routesrequireSubscription only accepts active|trial. UI may promise grace-after-cancel; backend disagrees. Real UX gotcha.
  • Trial reminders not wired into the schedulersend-trial-reminders.ts exists but nothing calls it on a cron. Users get no heads-up before expiry. Wire into subscription-cron.ts.
  • Two incrementUsage implsmiddleware/subscription.ts:L118 uses raw SQL, race-safe. lib/subscription-limits.ts does read-modify-write, NOT race-safe. Always prefer the middleware.
  • Race between cron and re-subscribe → A re-subscribe exactly at the cron tick can see the row flipped mid-flow. Defense at subscriptions.ts:L123-L138 blocks new subs while a cancelled row still has currentPeriodEnd > now.
  • Trial eligibility checksubscriptions.ts:L153 checks for any prior sub with non-null trialEndsAt. No user flag. Deleting old sub rows = trial abuse.
  • Catalog auto-unpublishes on expiry → If the plan included Shareable Catalog, update-expired-trials.ts flips Store.published=false. Shopper gets 404.
  • past_due is dead → Enum value exists, no code sets it.

9. Extension points

  • Recurring auto-renewal → integrate Razorpay Subscriptions API (separate from Orders API). Add webhook handler.
  • Grace period → relax requireSubscription to allow cancelled with currentPeriodEnd > now. One-line; mind UX consistency.
  • Wire trial reminders → call send-trial-reminders.ts from subscription-cron.ts daily.
  • Prorated downgrades → not supported today; users expire and re-subscribe at new plan.

10. Related docs