Subscription Lifecycle Reference
This guide explains every subscription status exposed by SubscriptionDto.status, the exact calculation rules, and how statuses can flow over time. These rules are enforced inside the Subscription domain entity, so every fetch reflects the latest lifecycle state without requiring callers to run manual cron jobs or SQL scripts.
Calculation Order
Statuses are evaluated in a strict priority order. The first rule that matches wins:
cancelledexpiredtrialcancellation_pendingsuspendedactivepending(fallback when activation has not happened yet)
If two conditions could apply simultaneously, whichever appears earlier in the list takes precedence.
Status Definitions
| Status | How it is calculated | Typical usage |
|---|---|---|
pending | Subscription exists but activationDate is missing or in the future. None of the other rules match yet. | Pre-provisioned subscriptions waiting for onboarding or payment confirmation. |
active | Default state once activationDate is in the past and there are no cancellations, expirations, suspensions, or trials in play. | Normal billing periods after trial completion. |
trial | trialEndDate exists and is greater than the current time. Overrides active as long as the trial is ongoing. | Limited-time access before billing starts. |
cancellation_pending | cancellationDate is set (indicating the subscriber asked to cancel) but the current period end or cancellation date is still in the future. | Grace period between a cancellation request and the final cut-off. |
cancelled | cancellationDate exists and is in the past (or equals now). Indicates the subscription has fully ended due to cancellation. | Final state after the cancellation effective date passes. |
expired | expirationDate exists and is in the past, and there is no cancellation. Used for time-bound subscriptions that simply reach their expiration. | Fixed-term offers or promotional subscriptions that lapse automatically. |
suspended | Subscription has been explicitly suspended via suspend() (e.g., payment failure or manual enforcement). This state only applies when none of the higher-priority rules match. | Temporary service pause until the issue is resolved. |
**Important
cancellation_pendingandcancelledalways win overtrial,active, andpending. This ensures cancellation intent is honored regardless of trial or activation timing.
Status Flow Diagram
flowchart LR
Pending["pending\n(no activation yet)"] -->|activationDate reached| Trial
Pending -->|activationDate reached| Active
Trial["trial\n(trialEndDate in future)"] -->|trialEndDate passed| Active
Active -->|set cancellationDate in future| CancelPending
Trial -->|set cancellationDate in future| CancelPending
CancelPending["cancellation_pending\n(cancellationDate in future)"] -->|cancellationDate reached| Cancelled
Active -->|set expirationDate in future| ActiveExp
ActiveExp["active\n(with expiration pending)"] -->|expirationDate reached| Expired
Pending -->|set expirationDate in future| Pending
Expired["expired\n(expirationDate passed)"]
Cancelled["cancelled\n(cancellationDate passed)"] The diagram illustrates common flows but does not represent every edge case (e.g., reactivation or plan transitions). Any state may move directly to cancelled or expired when the relevant date is set retroactively.
Subscription Transitions
When a subscription expires and its plan has an onExpireTransitionToBillingCycleKey configured, the subscription can be automatically transitioned to a new plan. This is handled by the transitionExpiredSubscriptions() method.
Transition Process
- Expired subscriptions with transition-enabled plans are identified
- Old subscription is marked as transitioned (archived with
transitioned_attimestamp) - New subscription is created to the transition billing cycle
- Subscription key is versioned:
original-key→original-key-v1→original-key-v2, etc.
Transition Tracking
transitioned_at: UTC timestamp set when a subscription is transitioned- Archived status: Transitioned subscriptions are archived (
isArchived = true) - Stripe IDs: Original Stripe subscription ID remains on the archived subscription for historical reference
- Feature overrides: Do not carry over to the new subscription
- Metadata: Carries over to the new subscription
Querying Transitioned Subscriptions
To find all subscriptions that were transitioned:
Or filter by transition date range for auditing purposes.
Best Practices for Provisioning Subscriptions with Trials
When creating subscriptions with trial periods, you need to decide what happens when the trial ends. There are three distinct scenarios, each requiring different configuration of trialEndDate, expirationDate, and currentPeriodStart/currentPeriodEnd.
Understanding the Key Fields
trialEndDate: Controls when the subscription is intrialstatus. IftrialEndDate > NOW(), status istrial; otherwise it'sactiveorexpired.expirationDate: Controls when the subscription becomesexpired. If set to the same value astrialEndDate, the subscription will expire when the trial ends.currentPeriodStartandcurrentPeriodEnd: Represent the billing period. Best practice: SetcurrentPeriodStartequal totrialEndDateso billing begins when the trial ends.
Trial End Scenarios
| Scenario | trialEndDate | expirationDate | Plan Transition? | After Trial Ends |
|---|---|---|---|---|
| A: Billing starts | Future date | null (not set) | No | Status: active, billing period begins |
| B: Migrate to free plan | Future date | = trialEndDate | Yes (onExpireTransitionToBillingCycleKey) | Status: expired → new subscription to free plan created |
| C: Lose access | Future date | = trialEndDate | No | Status: expired, no access granted |
Scenario A: Trial Ends → Billing Starts (Normal Paid Subscription)
This is the standard flow for paid subscriptions with a trial period. When the trial ends, billing automatically begins.
Setup
// Calculate trial end date (7 days from now)
const trialEndDate = new Date();
trialEndDate.setDate(trialEndDate.getDate() + 7);
// Create subscription
// Note: currentPeriodStart and currentPeriodEnd are optional
// If not provided, they will be calculated automatically
const subscription = await subscrio.subscriptions.createSubscription({
key: 'customer-123-pro-subscription',
customerKey: 'customer-123',
billingCycleKey: 'pro-monthly', // Monthly billing cycle
trialEndDate: trialEndDate.toISOString(),
// expirationDate is NOT set - billing will start after trial
// currentPeriodStart and currentPeriodEnd will be auto-calculated
// to start when trial ends
});
Record State on Creation
{
trialEndDate: "2025-01-27T00:00:00Z", // 7 days from now
expirationDate: null, // NOT set
currentPeriodStart: "2025-01-27T00:00:00Z", // Same as trialEndDate (billing starts when trial ends)
currentPeriodEnd: "2025-02-27T00:00:00Z", // currentPeriodStart + 1 month
activationDate: "2025-01-20T00:00:00Z", // Now
status: "trial" // Because trialEndDate > NOW()
}
After Trial Ends (2025-01-27)
{
trialEndDate: "2025-01-27T00:00:00Z", // Still set (historical record)
expirationDate: null, // Still not set
currentPeriodStart: "2025-01-27T00:00:00Z", // Billing period started
currentPeriodEnd: "2025-02-27T00:00:00Z", // First billing period ends
status: "active" // trialEndDate <= NOW(), no expiration
}
The subscription is now in its first paid billing period. The customer will be charged and has full access.
Scenario B: Trial Ends → Migrate to Free Plan
Use this when you want to automatically transition customers to a free plan after their trial expires. This requires setting up the transition on the plan.
Plan Setup (One-Time)
First, configure the paid plan to transition to a free plan when subscriptions expire:
// Create or update the paid plan to specify transition target
const paidPlan = await subscrio.plans.createPlan({
productKey: 'my-product',
key: 'pro-plan',
displayName: 'Pro Plan',
// ... other fields
});
// The plan should have onExpireTransitionToBillingCycleKey set
// This is typically set when creating the plan or via plan management
// For this example, assume 'free-monthly' billing cycle exists for the free plan
Note: The
onExpireTransitionToBillingCycleKeyfield is set on the plan entity. You'll need to ensure your plan has this configured, either through your plan creation process or by updating existing plans.
Subscription Creation
// Calculate trial end date (14 days from now)
const trialEndDate = new Date();
trialEndDate.setDate(trialEndDate.getDate() + 14);
// Create subscription with expiration set to trial end
const subscription = await subscrio.subscriptions.createSubscription({
key: 'customer-123-pro-trial',
customerKey: 'customer-123',
billingCycleKey: 'pro-monthly', // Paid plan billing cycle
trialEndDate: trialEndDate.toISOString(),
expirationDate: trialEndDate.toISOString(), // Same as trialEndDate - expires when trial ends
// currentPeriodStart will be set to trialEndDate automatically
});
Record State on Creation
{
trialEndDate: "2025-02-03T00:00:00Z", // 14 days from now
expirationDate: "2025-02-03T00:00:00Z", // Same as trialEndDate
currentPeriodStart: "2025-02-03T00:00:00Z", // Would start billing here, but expires instead
currentPeriodEnd: "2025-03-03T00:00:00Z",
status: "trial" // trialEndDate > NOW()
}
After Trial Ends (2025-02-03)
The subscription status becomes expired because expirationDate <= NOW(). To trigger the transition to the free plan, run the transition process:
// Run this periodically (e.g., via cron job) to process expired subscriptions
const report = await subscrio.subscriptions.transitionExpiredSubscriptions();
console.log(`Processed: ${report.processed}`);
console.log(`Transitioned: ${report.transitioned}`);
console.log(`Errors: ${report.errors.length}`);
What Happens During Transition
- Old subscription is archived and marked with
transitioned_attimestamp - New subscription is created to the free plan's billing cycle
- Subscription key is versioned:
customer-123-pro-trial→customer-123-pro-trial-v1 - Metadata carries over to the new subscription
- Feature overrides do NOT carry over
- Stripe subscription ID remains on the old archived subscription
The new subscription will have:
{
key: "customer-123-pro-trial-v1",
billingCycleKey: "free-monthly", // Transitioned to free plan
trialEndDate: null, // No trial on free plan
expirationDate: null, // Free plan doesn't expire
status: "active" // Immediately active
}
Scenario C: Trial Ends → Lose Access
Use this when you want customers to lose access completely after the trial ends, with no automatic transition to another plan.
Setup
// Calculate trial end date (7 days from now)
const trialEndDate = new Date();
trialEndDate.setDate(trialEndDate.getDate() + 7);
// Create subscription with expiration
const subscription = await subscrio.subscriptions.createSubscription({
key: 'customer-123-trial-only',
customerKey: 'customer-123',
billingCycleKey: 'premium-monthly',
trialEndDate: trialEndDate.toISOString(),
expirationDate: trialEndDate.toISOString(), // Same as trialEndDate - expires when trial ends
// Plan does NOT have onExpireTransitionToBillingCycleKey set
});
Record State on Creation
{
trialEndDate: "2025-01-27T00:00:00Z", // 7 days from now
expirationDate: "2025-01-27T00:00:00Z", // Same as trialEndDate
currentPeriodStart: "2025-01-27T00:00:00Z",
currentPeriodEnd: "2025-02-27T00:00:00Z",
status: "trial" // trialEndDate > NOW()
}
After Trial Ends (2025-01-27)
{
trialEndDate: "2025-01-27T00:00:00Z", // Historical record
expirationDate: "2025-01-27T00:00:00Z", // Now in the past
status: "expired" // expirationDate <= NOW()
}
The customer loses access. The subscription remains in the database as expired for historical purposes, but the feature checker will return no access.
Important Notes
-
currentPeriodStartBest Practice: WhentrialEndDateis provided, setcurrentPeriodStartequal totrialEndDateso the billing period begins when the trial ends. If you don't explicitly setcurrentPeriodStart, Subscrio will calculate it based on the billing cycle, but explicitly setting it totrialEndDatemakes the intent clear. -
Billing System Integration: When integrating with external billing systems (like Stripe), the billing system will typically reset all dates when a customer purchases during trial. In this case, you should update the subscription with the dates provided by the billing system webhook.
-
Running Transitions: For Scenario B (migrate to free plan), you must periodically call
transitionExpiredSubscriptions()to process expired subscriptions. This is typically done via a cron job or scheduled task. -
Status Priority: Remember that
expiredstatus (priority 2) takes precedence overtrialstatus (priority 3). If bothtrialEndDateandexpirationDateare set to the same future date, the subscription will betrialuntil that date, then immediately becomeexpired. -
Explicit vs Calculated Dates: You can either explicitly set
currentPeriodStartandcurrentPeriodEnd, or let Subscrio calculate them. If you settrialEndDatebut don't setcurrentPeriodStart, Subscrio will usenow()as the start, which may not be what you want. Recommendation: Always explicitly setcurrentPeriodStarttotrialEndDatewhen creating trial subscriptions.
Practical Tips
- Setting
trialEndDateautomatically enterstrialuntil the timestamp is reached. Remove or backdate the field to exit trial immediately. - To stage a future cancellation, set
cancellationDateto the end of the current period. The subscription becomescancellation_pendinguntil the date passes. - Removing
cancellationDate(e.g., a customer rescinds cancellation) returns the subscription toactiveortrial, depending on other fields. suspendedis only set via explicit service calls (e.g., billing failure automation). Once you callresume(), the entity recomputes to whichever status applies next (active,trial, etc.).
Refer back to subscriptions.md for lifecycle-related APIs (archiveSubscription, unarchiveSubscription, clearTemporaryOverrides, etc.), and to feature-checker.md for how these statuses affect runtime feature access.