> ## 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.

# Request Lifecycle

> Four authentication modes, tenant scoping, idempotency — how a raw request becomes a safe, scoped operation.

Duro serves four very different kinds of caller, each with its own credential. The middleware chain's whole job is to turn any of them into the same thing the controllers expect: a request with a known tenant, a known mode, and a `RepositoryContext` already wired to the right schema.

## Four ways to authenticate

```mermaid theme={null}
flowchart TB
    subgraph who["Caller"]
        D["Dashboard user"]
        M["Merchant server"]
        O["OAuth app"]
        C["Customer"]
    end
    subgraph cred["Credential"]
        SES["Session cookie<br/>(10-min idle TTL)"]
        SK["Secret key<br/>sk_test_ / sk_live_"]
        OB["OAuth bearer<br/>(60-min token)"]
        IT["Identity token<br/>(phone + WhatsApp OTP)"]
    end
    subgraph svc["Where it's accepted"]
        CORE["core-api<br/>requireMember"]
        PUBV1["public-api /v1<br/>apiKeyAuth"]
        PUBOAUTH["public-api OAuth"]
        PUBID["public-api /identity, /portal"]
    end
    D --> SES --> CORE
    M --> SK --> PUBV1
    O --> OB --> PUBV1
    C --> IT --> PUBID
    M -.client_credentials.-> PUBOAUTH --> OB
```

| Credential         | Carries                         | Resolves to                  | Notes                                                                                                  |
| ------------------ | ------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| **Session**        | `httpOnly` cookie               | a `Member` (tenant + role)   | Dashboard only. 10-minute idle timeout, sliding.                                                       |
| **Secret key**     | `Authorization: Bearer sk_…`    | a tenant + mode + full scope | Server-to-server. Prefix picks the mode. Hashed at rest, `lastUsedAt` written fire-and-forget.         |
| **OAuth token**    | `Authorization: Bearer …` (JWT) | a tenant + mode + scope      | `client_credentials` grant from a `cid_`/`csec_` pair, 60-minute expiry.                               |
| **Identity token** | `Authorization: Bearer …` (JWT) | a `CustomerIdentity`         | Customer-facing. Issued after WhatsApp OTP. Marked `keyId: identity` so it can never pass tenant auth. |

## The middleware chain on `/v1`

The public merchant API is the most interesting chain because it does the most work before the controller sees anything:

```mermaid theme={null}
sequenceDiagram
    autonumber
    participant Req as Request
    participant AK as ApiKeyAuthMiddleware
    participant TS as TenantScopeMiddleware
    participant ID as IdempotencyMiddleware
    participant PG as PermissionGuard
    participant CT as Controller

    Req->>AK: Authorization: Bearer sk_live_…
    AK->>AK: hash key, look up, check not revoked
    AK->>AK: derive mode from prefix
    Note over AK: sets req.auth = { tenantId, mode, scope, keyId }
    AK->>TS: next()
    TS->>TS: new RepositoryContext(db, tenantId, mode, cache)
    Note over TS: sets req.repos — every repo pre-scoped
    TS->>ID: next()
    ID->>ID: if Idempotency-Key present, look up<br/>(tenantId, key) → replay or record
    ID->>PG: next()
    PG->>PG: route requires e.g. SUBSCRIPTION_WRITE?<br/>scope includes it? (sk_ has '*')
    PG->>CT: next() — runs with req.repos + req.auth
```

By the time `SubscriptionController.create` runs, it calls `this.repos(req).subscriptions.create(...)` and that repository is already bound to `(tenantId, mode)`. The controller never sees a raw database client, never sees the other tenants' data, and never constructs a query that could.

## Tenant scoping is the load-bearing wall

`RepositoryContext` is the object that makes cross-tenant leakage structurally hard:

```mermaid theme={null}
flowchart LR
    AUTH["req.auth<br/>tenantId + mode"] --> RC["new RepositoryContext(<br/>db, tenantId, mode, cache)"]
    RC --> R1["plans"]
    RC --> R2["customers"]
    RC --> R3["subscriptions"]
    RC --> R4["invoices · dunning ·<br/>webhooks · events · …"]
    R1 & R2 & R3 & R4 --> SCOPE["every method:<br/>where: { tenantId, … }<br/>on data(mode) client"]
```

Each repository is constructed with the `tenantId` baked in and every query it issues includes `tenantId` in the `where` clause. There is no repository method that omits it. A new resource added to the system gets a repository, gets added to `RepositoryContext`, and inherits the scoping for free.

## Idempotency, scoped to the tenant

Mutating endpoints accept an `Idempotency-Key`. The record is keyed `(tenantId, key)` — a compound unique. This matters: a global key namespace would let one merchant's key collide with another's, and an attacker could probe for replays. Per-tenant scoping closes that.

```mermaid theme={null}
flowchart TD
    R["POST with<br/>Idempotency-Key: abc"] --> Q{"(tenantId, abc)<br/>seen before?"}
    Q -->|"yes"| REPLAY["replay stored<br/>response + status"]
    Q -->|"no"| RUN["run handler"]
    RUN --> STORE["store response<br/>hashed by request"]
    STORE --> RESP["return"]
```

<Note>
  A real bug this design caught early: the idempotency table originally had a global-unique `key`. That's a cross-tenant replay vector — fixed to `@@unique([tenantId, key])`. The [security model](/security/security-model) page documents this and the other findings from the audit pass.
</Note>

## Mode is never optional, never a default

Four-fifths of the bugs in multi-mode billing systems come from `mode` defaulting silently. Duro never defaults it:

* **Keys carry it** in the prefix (`sk_test_` vs `sk_live_`).
* **Request-scoped code reads it** from `req.auth.mode` and passes it to `data(mode)`.
* **Public token lookups derive it** from the token itself — a checkout token is `cs_<mode>_…`, so the mode is parsed from the prefix, not guessed by scanning both schemas.
* **The customer portal takes it explicitly** — the dashboard's mode switcher sends the current mode; the portal never silently lists both.
* **Background scanners loop both** — and only background scanners, because a cron has no "current mode."

That discipline is a documented invariant, and it's the difference between a billing system you can trust and one you can't.

Next: a deeper look at [multi-tenancy](/architecture/multi-tenancy) and the cache that sits in front of it.
