Request lifecycle
Every request flows through a middleware chain registered in app.ts, in
this order:
requestLogger → checkOrigin → secureHeaders → compress → inertiaMiddleware→ routes → onError / notFoundrequestLogger— generates the correlation id (x-request-id), times the request, batches the log line (no syscall per request; flushed every 50ms to stdout, errors go to stderr immediately).checkOrigin— CSRF defense: unsafe methods (POST/PUT/PATCH/DELETE) with anOriginheader whose host mismatches get 403. Non-browser clients that omitOriginare allowed.secureHeaders— hardening headers (CSP, nosniff, frame denial, referrer policy, permissions policy, HSTS). For/uploadsresponses the CSPscript-srcbecomes'none'(attacker-controlled bytes can never execute scripts).compress— gzip for/assets/*and SSR HTML only. JSON/API responses pass through untouched (they are small and must never be transformed).inertiaMiddleware— resolves the session from the cookie and sets typed context variables:user,flash,sessionToken,inertia(AppEnv.Variables). Runs for every request, including unmatched ones, so error handlers can rely on it.
Then the route: guards run (requireAuth, guestOnly, requireRole),
TypeBox validation runs, and the handler renders the Inertia page — full
SSR HTML for browser visits, JSON for X-Inertia requests.
Errors (app.onError): validation failures map to 422 Inertia page
payloads with friendly field messages; /uploads errors stay JSON with tus
headers; unknown errors → 500. Not found (app.notFound): Inertia
NotFound page, except /uploads which stays JSON.
Middleware rules of thumb
Section titled “Middleware rules of thumb”- Return a
Responseto short-circuit the chain; callawait next()to continue. - Never read a response body unless you rebuild the response afterwards — bodies are one-shot streams (this bit the gzip middleware once).