MainSDK
Search⌘K
Architecture

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.

Planned for 0.4

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.
Stability levels
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

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
Diagram

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; this is how each remote dependency is kept off the path.

Remote dependencyWhen we talk to itIf it is down
MainSDK Cloud (phase 2)Background export, after mainsdk login and opt-inTelemetry is buffered, then dropped oldest first, with a counter
TUF delivery (0.4)mainsdk sync, startup from the local cache, a background timerThe app keeps its last good packs
Your OTLP endpointBackground exportTelemetry 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
Diagram

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

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet