---
title: "Request lifecycle"
description: "What happens inside your app on one MainSDK call, why we are never in the request path, and how the app fails static."
section: "architecture"
status: "Planned for 0.4"
spec: "fail-static"
canonical: "https://mainsdk.dev/docs/architecture/request-lifecycle"
updated: "2026-10-28"
---

# Request lifecycle

> What happens inside your app on one MainSDK call, why we are never in the request path, and how the app fails static.

Status: Planned for 0.4 · Canonical: https://mainsdk.dev/docs/architecture/request-lifecycle · Updated: 2026-10-28

This page follows one request through a Next.js app that calls `pay.refund()`. Everything below happens in your process. No step waits on MainSDK Cloud (ADR-0003).

## One call, step by step

```mermaid
sequenceDiagram
  autonumber
  participant B as Browser or agent
  participant A as Adapter (mainsdk/next)
  participant H as Your handler
  participant P as Policy engine (in process)
  participant D as Driver (stripe)
  participant N as http chokepoint
  participant L as Log and audit
  participant Q as Telemetry buffer
  B->>A: POST /api/orders/8812/refund (traceparent)
  A->>A: build RequestContext, run context and before hooks
  A->>H: handler(req, ctx)
  H->>P: policy.decide(subject, action, resource, context)
  P-->>H: allow (policyVersion, reasons)
  P->>L: decision record with policy version
  H->>D: pay.refund(paymentId, idempotency key)
  D->>N: http.fetch(api.stripe.com)
  N->>N: check host against network.allow
  N-->>D: response
  D-->>H: result
  H-->>A: response
  A->>A: after and finally hooks
  A-)Q: spans and logs, async
  A-->>B: 200, or RFC 9457 problem details
```

## What each step does

1. **Context.** The adapter creates a `RequestContext` per request or job: the W3C `traceparent`, user, organisation, country, age band, consent, purpose, market, segment and locale. It travels to every interface call through `AsyncLocalStorage`, like tracing context.
2. **Hooks.** They run in a fixed order in every adapter: `context` → `before` → handler → `after` → `error` (only on throw) → `finally`. A throwing `before` hook skips the handler and runs `error` and `finally`.
3. **Decision.** The interface asks `policy.decide()`. It is deterministic, in process and has no network round trip. With no matching permit, the answer is deny.
4. **Guarantees.** The contract applies its own rules, for example an idempotency key generated from the action and its inputs, so a retry never refunds twice.
5. **Outbound call.** The driver calls the provider through the SDK's `http` client. The host is checked against `network.allow` plus the driver's declared hosts; anything else is blocked and reported as `MAIN-NET-0002`.
6. **Records.** The decision is written to the audit log with its policy version. Logs and spans go to a bounded buffer and are exported in the background.
7. **Errors.** A thrown error becomes `application/problem+json` (RFC 9457) with a stable code such as `MAIN-DATA-0007`, a fix and the trace id. In production the body to the end user carries no file, line or stack.

## Never in the request path

The rules are on [fail static](https://mainsdk.dev/docs/concepts/fail-static); this is how each remote dependency is kept off the path.

| Remote dependency | When we talk to it | If it is down |
| --- | --- | --- |
| MainSDK Cloud (phase 2) | Background export, after `mainsdk login` and opt-in | Telemetry is buffered, then dropped oldest first, with a counter |
| TUF delivery (0.4) | `mainsdk sync`, startup from the local cache, a background timer | The app keeps its last good packs |
| Your OTLP endpoint | Background export | Telemetry is dropped with a counter |

The telemetry buffer is bounded. Our proposed default is 10,000 records or 8 MB, whichever comes first; calls to the Cloud use short timeouts (proposed 2 s) with exponential backoff and jitter. Errors, decisions and audit records are never sampled; the audit log buffers to local disk instead of dropping (`MAIN-LOG-0010`).

## Fail static

```mermaid
stateDiagram-v2
  [*] --> Verified: startup loads local cache
  Verified --> Verified: background refresh verifies newer set
  Verified --> Stale: mirror unreachable or metadata expired
  Stale --> Verified: refresh succeeds
  Verified --> Verified: new set fails to parse or verify (keep last good)
  Stale --> Stale: keep deciding with last good policy
```

When cached policy or packs expire, or a new set fails to load, the app keeps the last good set. It never falls back to no policy and never blocks traffic because of expiry. The last good set survives a restart and is verified again at load. Startup never waits on a TUF refresh or a Cloud call.

Driver outages are different: a provider failure still surfaces as a typed error with a retry hint, and `fallback:` in `main.yaml` handles failover.

## See the state of every dependency

```ts
import { health } from "mainsdk";
health(); // [{ name: "mainsdk-cloud", kind: "mainsdk", state: "down", inRequestPath: false, … }]
```

```text
$ npx mainsdk status
mainsdk-cloud   down      since 10:02Z   telemetry buffered (4,120 queued, 0 dropped)
tuf             stale     since 09:40Z   last good: baseline 27.0.3, market/in 3
email:resend    ok
db:neon         degraded  p99 840 ms
```

`inRequestPath` is `false` for every MainSDK service, always. `health()` and `mainsdk status` ship with milestone 0.4.

## Next

- [Performance budgets](https://mainsdk.dev/docs/architecture/performance-budgets): the limits this path must stay within, also in degraded mode.
- [Policy engine](https://mainsdk.dev/docs/architecture/policy-engine): how decisions are made and explained.
- [Telemetry and signals](https://mainsdk.dev/docs/architecture/telemetry-and-signals): what the buffer holds and where it goes.
