> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useduro.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Subscription Lifecycle

> Nine states, an explicit transition table, and a guard that makes illegal transitions impossible.

A subscription is a small state machine. Getting the state machine right is the difference between a billing system you can reason about and a pile of boolean flags that drift out of sync. Duro encodes it explicitly in `SubscriptionStateMachine` — a pure class in `@duro/billing` with a transition table and no IO.

## The states

```mermaid theme={null}
stateDiagram-v2
    [*] --> incomplete: created
    incomplete --> trialing: start_trial
    incomplete --> active: activate
    incomplete --> incomplete_expired: expire_incomplete
    trialing --> active: activate (first charge)
    trialing --> past_due: renewal_failed
    active --> past_due: renewal_failed
    active --> paused: pause
    active --> expired: reach_limit (maxCycles)
    paused --> active: resume
    past_due --> active: recover (dunning win)
    past_due --> unpaid: exhaust_dunning
    incomplete --> canceled: cancel
    trialing --> canceled: cancel
    active --> canceled: cancel
    past_due --> canceled: cancel
    paused --> canceled: cancel
    unpaid --> canceled: cancel
    canceled --> [*]
    expired --> [*]
    unpaid --> [*]
```

Each arrow is one row in a transition table. A transition has a `from` state, an `action`, a `to` state, and the **event it emits**. That last column is why the state machine is also the source of truth for webhooks — the event name is not chosen at the call site, it's a property of the transition.

| From                  | Action            | To        | Emits                    |
| --------------------- | ----------------- | --------- | ------------------------ |
| incomplete            | `start_trial`     | trialing  | `subscription_created`   |
| trialing / incomplete | `activate`        | active    | `subscription_activated` |
| active / trialing     | `renewal_failed`  | past\_due | `subscription_past_due`  |
| past\_due             | `recover`         | active    | `subscription_recovered` |
| past\_due             | `exhaust_dunning` | unpaid    | `subscription_unpaid`    |
| active                | `reach_limit`     | expired   | `subscription_expired`   |
| active                | `pause`           | paused    | `subscription_paused`    |
| paused                | `resume`          | active    | `subscription_resumed`   |
| (any cancelable)      | `cancel`          | canceled  | `subscription_cancelled` |

## Illegal transitions are impossible, loudly

`transition(from, action)` looks up the table. If there's no matching row, it throws a `ConflictError` — it does not silently no-op:

```mermaid theme={null}
flowchart LR
    CALL["transition('canceled', 'pause')"] --> FIND{"row where<br/>from=canceled,<br/>action=pause?"}
    FIND -->|"found"| OK["return { to, emit }"]
    FIND -->|"none"| ERR["throw ConflictError:<br/>'cannot pause a<br/>canceled subscription'"]
```

So "pause a canceled subscription" or "resume an active one" can't corrupt state — the attempt is rejected at the boundary. Callers that want to *check first* use `can(from, action)`, a boolean; callers that *intend* the transition use `transition()` and accept the throw. The [dunning service](/billing/dunning) uses `can()` precisely so a recovery attempt on a subscription the merchant already canceled is a quiet skip, not a crash.

## Terminal states stop the world

`isTerminal(status)` is true for `canceled`, `expired`, and `incomplete_expired`. The [renewal scanner](/billing/renewal-engine) only ever picks up `active` and `trialing` subscriptions, so terminal subscriptions are never billed again — they fall out of every sweep by construction.

## Why this is a separate package

`SubscriptionStateMachine` has **zero dependencies on the database, HTTP, or the queue.** It's pure functions over enums. That means:

* It's tested exhaustively in isolation — every legal transition and a sample of illegal ones, in milliseconds.
* The same class drives transitions whether the caller is a dashboard request (`SubscriptionService`), a recovery win (`DunningService`), or the renewal worker (`BillingService`). One definition of "what's legal," three call sites.
* A judge reading `@duro/billing/state-machine.ts` sees the entire lifecycle in one screen, with no Prisma noise.

Next: the [renewal engine](/billing/renewal-engine) — the loop that drives subscriptions through this machine on a timer.
