Subscription Architecture
Current module structure, service responsibilities, entitlement flow, and access integration for subscriptions.
Subscription Architecture
Audience: backend developers Scope: current architecture in
apps/api/src/modules/subscription,apps/api/src/modules/access, andpackages/db/src/schema/subscription.ts
High-Level Shape
The current architecture has two related layers:
- subscription lifecycle
- access entitlement
Lifecycle lives in the subscription module. Runtime authorization lives in the access module.
Core Modules
SubscriptionModule
File: apps/api/src/modules/subscription/subscription.module.ts
Responsibilities:
- provides
SubscriptionService - provides
AccessExpirationTask - exports
SubscriptionService - imports
ActivityModule
This module combines core subscription write logic with the scheduled expiry task.
SubscriptionMobileModule
File: apps/api/src/modules/subscription/mobile/subscription-mobile.module.ts
Responsibilities:
- imports
SubscriptionModule - exposes mobile subscription read endpoints
- delegates trial start and detailed subscription lookup to
SubscriptionService
Important current fact:
- it does not import a payment module
- it does not expose purchase or payment-verification routes
SubscriptionAdminAggregateModule
File: apps/api/src/modules/subscription/admin/subscription-admin-aggregate.module.ts
Responsibilities:
- imports and re-exports all admin subscription submodules
- wires controllers for:
- modules
- tiers
- plans
- plan prices
- plan features
- subscriptions
AccessModule
File: apps/api/src/modules/access/access.module.ts
Responsibilities:
- globally exports
AccessService - globally exports
FeatureService
This is the read-side entitlement module used by route guards and other consumers.
Service Responsibilities
SubscriptionService
Primary write-side coordinator.
Key methods:
createPendingPurchase()activateSubscription()handlePaymentFailure()startTrial()adminGrant()adminExtend()cancel()getActiveSubscriptions()getSubscriptionById()upsertUserAccess()
The most important architectural detail is upsertUserAccess(). Every grant path converges on it so that user_access remains the current entitlement source of truth.
AccessService
Primary module-level authorization service.
- checks access by module slug
- resolves the module first
- grants access only when a non-revoked, non-expired
user_accessrow exists
FeatureService
Primary feature-level entitlement service.
- loads feature keys from active
user_access -> plan -> features - supports boolean checks and value lookup
AccessExpirationTask
Scheduled cleanup.
- runs every 5 minutes
- revokes expired
user_access - marks expired
subscriptionrows asexpired - invalidates related caches
Current Flow Boundaries
What the Subscription Module Does
- manages catalog-backed subscription lifecycle
- starts trials
- grants and extends subscriptions
- updates entitlement rows
- exposes mobile read APIs for modules, plans, subscriptions, and access
What the Subscription Module Does Not Currently Expose
- a public purchase initiation endpoint
- a payment verification endpoint
The service layer still contains pending purchase and activation methods, but they are not exposed by the current mobile subscription controller.
Access Integration
The old SubscriptionAccessGuard naming is obsolete.
Current access integration uses:
AccessGuard+@RequiresAccess()FeatureGuard+@RequiresFeature()
These guards depend on AccessService and FeatureService, not on a dedicated subscription guard.
Current File Layout
apps/api/src/
├── common/authorization/
│ ├── access.guard.ts
│ ├── feature.guard.ts
│ ├── requires-access.decorator.ts
│ └── requires-feature.decorator.ts
│
├── modules/
│ ├── access/
│ │ ├── access.module.ts
│ │ ├── access.service.ts
│ │ └── feature.service.ts
│ │
│ └── subscription/
│ ├── subscription.module.ts
│ ├── subscription.service.ts
│ ├── tasks/
│ │ └── access-expiration.task.ts
│ ├── admin/
│ │ ├── subscription-admin-aggregate.module.ts
│ │ ├── module/
│ │ ├── tier/
│ │ ├── plan/
│ │ ├── plan-price/
│ │ ├── plan-feature/
│ │ └── subscription/
│ └── mobile/
│ ├── subscription-mobile.module.ts
│ ├── subscription-mobile.controller.ts
│ └── subscription-mobile.service.ts
│
packages/db/src/schema/
└── subscription.tsArchitectural Summary
The current design is built around one core idea:
subscriptiontracks lifecycleuser_accesstracks entitlement
That separation is why trials, admin grants, and paid subscriptions can all share the same authorization path while still keeping business history in subscription records.