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 Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.
Stability The promise an interface ships with.
- Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
- Beta Usable in production; may change before it becomes stable.
- Experimental May change or go away; needs an explicit opt-in in main.yaml.
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
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
- Context. The adapter creates a
RequestContextper request or job: the W3Ctraceparent, user, organisation, country, age band, consent, purpose, market, segment and locale. It travels to every interface call throughAsyncLocalStorage, like tracing context. - Hooks. They run in a fixed order in every adapter:
context→before→ handler →after→error(only on throw) →finally. A throwingbeforehook skips the handler and runserrorandfinally. - 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. - 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.
- Outbound call. The driver calls the provider through the SDK's
httpclient. The host is checked againstnetwork.allowplus the driver's declared hosts; anything else is blocked and reported asMAIN-NET-0002. - 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.
- Errors. A thrown error becomes
application/problem+json(RFC 9457) with a stable code such asMAIN-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; 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
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
import { health } from "mainsdk";
health(); // [{ name: "mainsdk-cloud", kind: "mainsdk", state: "down", inRequestPath: false, … }]$ 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 msinRequestPath is false for every MainSDK service, always. health() and mainsdk status ship with milestone 0.4.
Next
- Performance budgets: the limits this path must stay within, also in degraded mode.
- Policy engine: how decisions are made and explained.
- Telemetry and signals: what the buffer holds and where it goes.
Was this page helpful?