Review verdict (2026-05-27)

Accept. Closes the "where does the request id come from?" gap that spec 10 already implicitly opened by typing requestId: string on AiContext and reaching for crypto.randomUUID(). The framework should generate the id once per request at the outermost middleware boundary and expose it on PattiesContext; downstream consumers (AiContext, log lines, plugin middlewares, the dev-error overlay) read it instead of minting their own.

Scope pins:

  • Single source of truth. ctx.requestId is generated by the framework in the outer composer (the same place that constructs PattiesContext), before the user middleware runs. User middleware and plugins MUST NOT re-generate it.
  • Inbound override. If the request carries an X-Request-Id header that matches ^[A-Za-z0-9._-]{8,128}$, that value is adopted verbatim and Bun.randomUUIDv7() is not called. This keeps trace ids stable across an upstream proxy / gateway. Header values that don't match the shape are ignored (not echoed back) — generate fresh instead.
  • Outbound header. The response finalizer (same place that flushes Set-Cookie) sets X-Request-Id: <ctx.requestId> on every response, including the 404 fallback and error responses. Already-set values on the user's Response win — we do not overwrite.
  • Non-Bun targets. Adapters under src/adapters/ that target a runtime without Bun.randomUUIDv7 fall through to crypto.randomUUID() (UUIDv4). The id is still a string of the same shape; the time-ordering property is best-effort.
  • AiContext. createAiContext no longer mints its own id — it accepts the framework's requestId from PattiesContext. The crypto.randomUUID() call in spec 10 is replaced by a read of ctx.requestId.

Out of scope for this RFC:

  • W3C traceparent / OpenTelemetry propagation. Separate concern; the request id is a single opaque string, not a trace context. Revisit under a future observability RFC.
  • Signing or HMAC'ing the id. The id is informational, not a security primitive.

Summary

Bun.randomUUIDv7() returns a UUID v7: time-ordered (millisecond-precision prefix) and lexicographically sortable by issuance. Patties should generate one per request at the framework boundary and surface it as ctx.requestId on PattiesContext, with an X-Request-Id echo on the response.

Motivation

Spec 07 (framework/phase-1/07-middleware) defines PattiesContext but gives users no way to correlate a request across middleware, handler, log lines, and (when present) the AI subsystem. Spec 10 (framework/phase-3/10-agents-and-tools) already needs a requestId on AiContext and currently reaches for crypto.randomUUID() inline — a local fix that leaves the framework's main request lifecycle without an id of its own. The two surfaces drift.

UUID v7 is the right shape:

  • Sortable. ORDER BY request_id in a log table = chronological order without a separate timestamp column.
  • Time-encoded prefix. Eyeballing a log file you can see which requests came first.
  • Still a string. No new type for users to learn; no breaking change to AiContext.requestId: string.

Proposal

07-middleware

Extend PattiesContext:

export interface PattiesContext {
  // ...existing fields
  requestId: string  // populated once per request by the framework
}

The outer composer (the place that today builds ctx.url, ctx.cookies, etc. before invoking user middleware) populates ctx.requestId:

const inbound = req.headers.get("x-request-id")
const requestId = inbound && /^[A-Za-z0-9._-]{8,128}$/.test(inbound)
  ? inbound
  : Bun.randomUUIDv7()

The response finalizer sets X-Request-Id: <requestId> on the outgoing Response if the user/handler didn't set it themselves.

User middleware example (replacing the existing logging snippet):

export default defineMiddleware(async (req, ctx, next) => {
  const start = performance.now()
  const res = await next()
  console.log(ctx.requestId, req.method, ctx.url.pathname, res.status,
    `${(performance.now() - start) | 0}ms`)
  return res
})

10-agents-and-tools

createAiContext accepts the framework's requestId rather than generating one. The example in spec 10 changes from:

const ctx = createAiContext({ requestId: crypto.randomUUID() })

to:

const ctx = createAiContext({ requestId: pctx.requestId })

AiContext.requestId: string is unchanged on the type level.

13-conventions

Note that all framework-generated ids use Bun.randomUUIDv7() so they're sortable in log output. Non-Bun adapters fall through to crypto.randomUUID(); this is documented but not a guarantee.

Adapters

src/adapters/bun/ uses Bun.randomUUIDv7() directly. src/adapters/edge/ uses crypto.randomUUID() (UUIDv4) — the W3C-edge platforms don't expose a v7 helper yet. The framework core depends only on the PattiesContext.requestId: string field, not on the v7-ness, so the adapter swap is local.

Trade-offs

  • One more field on PattiesContext. The spec is mildly heavier; worth it because today every team writes the same five lines of middleware to mint and log a correlation id.
  • Inbound header trust. Adopting X-Request-Id verbatim means a malicious client can pin a known id and make their requests appear alongside someone else's in logs. Acceptable for an informational field; the regex stops the obviously-bad shapes (control chars, excessive length). Production deployments that don't want this can strip the header at the edge.
  • UUIDv4 fallback on non-Bun. Time-ordering is best-effort across targets. Code that relies on sortable ids (e.g. an admin log view) must tolerate v4 mixed with v7. Both sort lexicographically as strings; v4s simply scatter.

Open questions

  • Should ctx.requestId be readable inside app/server.ts boot code or only at request scope? Decision: request-scope only (boot has no request). Boot code that needs an id for its own logging mints its own via Bun.randomUUIDv7().
  • Trim header allowlist regex once we have a real corpus of upstream proxies in use. The current [A-Za-z0-9._-]{8,128} accepts common trace-id encodings (hex, base32, base64url-without-padding) but not bare UUIDs with : separators. Revisit if a real deployment trips it.