Subscription Schema
Current database schema for subscription catalog, lifecycle, entitlement, and history tables.
Subscription Schema
Audience: backend developers Scope:
packages/db/src/schema/subscription.ts
Enum Strategy
The schema uses TypeScript enums mapped to varchar columns with .$type<>().
That means:
- code gets enum typing
- the database does not enforce enum values natively
Current enums:
SubscriptionStatusSubscriptionHistoryActionAccessGrantTypeAccessHistoryChangeType
Table Groups
The schema is organized into three groups.
Catalog Tables
subscription_modulesubscription_tiersubscription_plansubscription_plan_pricesubscription_plan_feature
Lifecycle / Runtime Tables
subscriptionuser_trialuser_access
History Tables
subscription_historyuser_access_history
Catalog Tables
subscription_module
Represents a subscribable product area.
Key fields:
iduuidnameuniquesluguniquedescriptioniconisActive
subscription_tier
Represents a tier within a module.
Key fields:
moduleIdnameslugdisplayOrderparentTierIdisActive
Important rule:
- tier slug uniqueness is scoped to a module
subscription_plan
Represents the core plan attached to a tier.
Key fields:
tierIdnamedescriptiontrialDaystrialRequiresCcisActivedisplayOrder
Important rule:
tierIdis unique, so each tier has exactly one plan
subscription_plan_price
Represents purchasable durations and prices for a plan.
Key fields:
planIdlabeldurationDaysmrpNprspNprisBaseisFeaturedisActivedisplayOrder
Important rules:
- unique on
(planId, durationDays) - check constraint enforces
spNpr <= mrpNpr
subscription_plan_feature
Represents plan features used by the feature entitlement layer and UI.
Key fields:
planIdmoduleIdfeatureKeyfeatureTextfeatureDetailsvalueicondisplayOrderenabled
Important rule:
- unique on
(planId, featureKey)
Runtime Tables
subscription
Represents a user's subscription lifecycle for a module.
Key fields:
userIdmoduleIdplanIdplanPriceIdstatusstartDateendDatecancelledAtcancelsAtpriceSnapshotNpr
Important observations:
planIdis required in the current schemaplanPriceIdis optionalstatuscan bepending_payment,active,cancelled,expired, ortrial- indexes exist on
userId,status,endDate, andmoduleId
user_trial
Tracks one-time trial consumption per user and module.
Key fields:
userIdmoduleIdsubscriptionIdstartedAtconvertedAt
Important rule:
- unique on
(userId, moduleId)
user_access
Represents the current access decision source of truth.
Key fields:
userIdmoduleIdplanIdgrantTypegrantedBysubscriptionIdexpiresAtrevokedAt
This table is what AccessService and FeatureService read.
History Tables
subscription_history
Audit log for lifecycle changes.
Key fields:
subscriptionIduserIdactionperformedBydetailscreatedAt
user_access_history
Audit log for entitlement changes.
Key fields:
userAccessIduserIdmoduleIdplanIdgrantTypechangeTypechangedBypreviousExpiresAtnewExpiresAtnotecreatedAt
Current Relations
Important relation paths:
- module -> tiers
- tier -> plan
- plan -> prices
- plan -> features
- subscription -> module / plan / planPrice / history
- user_access -> module / plan / subscription / history
- user_trial -> module / subscription
Important Modeling Decisions
Lifecycle and Entitlement Are Separate
This is the most important design choice.
subscriptionstores lifecycle and reporting stateuser_accessstores current entitlement
That is why cancellation, trial, paid activation, and admin grant can all share one authorization model.
No DB-Level Invariant for Some Business Rules
Some business rules are enforced in service code rather than the database:
- one current access row per
userId + moduleId - consistency between
subscription.moduleIdand the module implied byplanId - start/end date ordering
The service layer assumes those rules hold.