A Subscription represents a customer's commitment to a plan. It tracks the lifecycle from trial through active billing to cancellation, managing state transitions, billing periods, and usage entitlements.
Structure01
type Subscription struct {
ledger.Entity
ID id.SubscriptionID
CustomerID id.CustomerID
PlanID id.PlanID
TenantID string
AppID string
Status SubscriptionStatus
CurrentPeriodStart time.Time
CurrentPeriodEnd time.Time
TrialStart *time.Time
TrialEnd *time.Time
CanceledAt *time.Time
CancellationReason string
Metadata map[string]any
}Lifecycle states02
Subscriptions transition through well-defined states:
type SubscriptionStatus string
const (
StatusTrialing SubscriptionStatus = "trialing"
StatusActive SubscriptionStatus = "active"
StatusPastDue SubscriptionStatus = "past_due"
StatusCanceled SubscriptionStatus = "canceled"
StatusUnpaid SubscriptionStatus = "unpaid"
StatusPaused SubscriptionStatus = "paused"
)State diagram
trialing → active → past_due → unpaid
↓ ↓ ↓ ↓
canceled ← ← ← ← ← ← ← ←
↕
paused| Status | Description |
trialing | In trial period, no billing |
active | Actively billed, entitlements enabled |
past_due | Payment failed, grace period active |
unpaid | Payment collection failed, limited access |
paused | Temporarily suspended by customer |
canceled | Terminated, no further billing |
Billing periods03
Each subscription tracks its current billing cycle:
CurrentPeriodStart: time.Date(2024, 1, 15, 0, 0, 0, 0, time.UTC)
CurrentPeriodEnd: time.Date(2024, 2, 15, 0, 0, 0, 0, time.UTC)When an invoice is generated, the billing period advances:
Monthly:
CurrentPeriodEndmoves forward 1 monthYearly:
CurrentPeriodEndmoves forward 1 yearUsage-based: Period defined by billing schedule (e.g., monthly)
Trial periods04
Trials are tracked separately from billing periods:
TrialStart: &time.Time{...} // Trial started
TrialEnd: &time.Time{...} // Trial ends (becomes active or canceled)When time.Now() is before TrialEnd:
Status is
trialingNo invoices are generated
Full plan entitlements are granted
When trial expires:
Status transitions to
active(if payment method on file) orunpaidFirst invoice is generated
Cancellation05
Subscriptions can be canceled immediately or at period end:
Immediate cancellation
subscription.Status = subscription.StatusCanceled
subscription.CanceledAt = &now
subscription.CancellationReason = "customer_request"Entitlements are revoked immediately.
Cancel at period end
subscription.CancelAtPeriodEnd = trueThe subscription remains active until CurrentPeriodEnd, then transitions to canceled.
Plan changes (upgrades/downgrades)06
When a customer changes plans:
Immediate change
subscription.PlanID = newPlan.ID
subscription.CurrentPeriodStart = time.Now()
subscription.CurrentPeriodEnd = time.Now().AddDate(0, 1, 0) // restart billing cycleA prorated invoice is generated for the unused portion of the old plan.
At period end
subscription.ScheduledPlanChange = &ScheduledChange{
NewPlanID: newPlan.ID,
EffectiveDate: subscription.CurrentPeriodEnd,
}The plan change applies at the end of the current billing period.
Usage tracking07
Subscriptions accumulate usage events for metered features:
// Usage is stored separately and aggregated during invoice generation
meter.Record(ctx, &meter.Event{
SubscriptionID: subscription.ID,
Meter: "api-calls",
Value: 1,
Timestamp: time.Now(),
})See Usage Metering for details.
Complete example: Creating a subscription08
subscription := &subscription.Subscription{
ID: id.NewSubscriptionID(),
CustomerID: customer.ID,
PlanID: starterPlan.ID,
TenantID: "acme-corp",
AppID: "saas-platform",
Status: subscription.StatusTrialing,
CurrentPeriodStart: time.Now(),
CurrentPeriodEnd: time.Now().AddDate(0, 1, 0), // 1 month
TrialStart: ptrTime(time.Now()),
TrialEnd: ptrTime(time.Now().AddDate(0, 0, 14)), // 14 days
}
if err := store.CreateSubscription(ctx, subscription); err != nil {
log.Fatal(err)
}Scheduled jobs09
Ledger runs background jobs to manage subscription lifecycle:
| Job | Frequency | Action |
| Trial expiration | Hourly | Transition trialing → active or unpaid |
| Period rollover | Daily | Advance billing periods, generate invoices |
| Dunning management | Daily | Retry failed payments, transition past_due → unpaid |
| Scheduled changes | Daily | Apply plan changes at EffectiveDate |
Store interface10
type Store interface {
CreateSubscription(ctx context.Context, sub *Subscription) error
GetSubscription(ctx context.Context, subID id.SubscriptionID) (*Subscription, error)
GetSubscriptionsByCustomer(ctx context.Context, customerID id.CustomerID) ([]*Subscription, error)
UpdateSubscription(ctx context.Context, sub *Subscription) error
CancelSubscription(ctx context.Context, subID id.SubscriptionID, reason string) error
ListSubscriptions(ctx context.Context, filter *ListFilter) ([]*Subscription, error)
}API routes11
| Method | Path | Description |
POST | /ledger/subscriptions | Create a subscription |
GET | /ledger/subscriptions | List subscriptions |
GET | /ledger/subscriptions/{id} | Get subscription by ID |
PUT | /ledger/subscriptions/{id} | Update subscription |
POST | /ledger/subscriptions/{id}/cancel | Cancel subscription |
POST | /ledger/subscriptions/{id}/pause | Pause subscription |
POST | /ledger/subscriptions/{id}/resume | Resume subscription |
POST | /ledger/subscriptions/{id}/change-plan | Upgrade/downgrade plan |