Request lifecycle
Every request follows the same path. Understanding this chain is the key to debugging and extending Kilat.
The chain
Section titled “The chain”Workers runtime │ ▼ fetch(request, env)worker.ts │ ├── initConfig(env) → config singleton ├── initDb(env.DB) → db singleton ├── initSessionCache() → sessionKv singleton (no-op without binding) ▼ app.fetch(request, env)┌─────────────────────────────────────────────────┐│ 1. requestLogger → correlation ID + JSON log ││ 2. checkOrigin → CSRF (Origin header) ││ 3. secureHeaders → nosniff, DENY, HSTS ││ 4. CSP nonce (post-resp) → script-src 'nonce-...' ││ 5. inertiaMiddleware → resolve session → c.var ││ 6. globalLimiter → KV rate limit (200/60s) │└─────────────────────────────────────────────────┘ │ ▼guards + handler │ ├── requireAuth ── fail ──→ redirect /login ├── requireRole ── fail ──→ redirect /dashboard ├── guestOnly ── fail ──→ redirect /dashboard │ ▼ passroute handler │ ├── c.var.inertia.render(component, props) │ ├── browser visit → SSR HTML + data-page JSON │ ├── X-Inertia XHR → JSON page payload │ └── version mismatch → 409 + X-Inertia-Location │ ├── ValidationFailed ──→ onError → 422 Inertia ├── other error ──→ onError → 500 └── no route ──→ notFound → 404 InertiaStep by step
Section titled “Step by step”1. Workers fetch → worker.ts
Section titled “1. Workers fetch → worker.ts”The Workers runtime calls fetch(request, env) per request. env carries the
D1 + ASSETS bindings and environment variables:
export default { async fetch(request: Request, env: Env): Promise<Response> { initConfig(env); // validate env → module-level config singleton initDb(env.DB); // bind D1 → module-level db singleton initSessionCache(env.SESSION_KV); // bind KV → module-level sessionKv singleton return app.fetch(request, env); },} satisfies ExportedHandler<Env>;initConfig and initDb run per-request — cheap pointer assignments to
module-level singletons. Workers isolates are stateless; do not cache state
across requests.
2. requestLogger — correlation ID
Section titled “2. requestLogger — correlation ID”src/server/logger.ts assigns a random correlation ID to every request and
logs the method + path as structured JSON. This is the first middleware —
every subsequent log line can reference the ID.
3. checkOrigin — CSRF
Section titled “3. checkOrigin — CSRF”src/server/security.ts checks the Origin header on state-changing requests
(POST, PUT, DELETE, PATCH). If the origin doesn’t match the request host, the
request is rejected. This is the CSRF defense — no token dance, just the Origin
check.
4. secureHeaders — security headers
Section titled “4. secureHeaders — security headers”Hono’s secureHeaders middleware sets X-Content-Type-Options: nosniff,
X-Frame-Options: DENY, referrer policy, permissions policy, and HSTS.
A per-request CSP nonce is generated in inertiaMiddleware and applied
after the response — inline scripts (theme boot, page payload) and inline
styles carry the nonce, so script-src 'unsafe-inline' is not needed.
5. inertiaMiddleware — session resolve
Section titled “5. inertiaMiddleware — session resolve”src/server/inertia-middleware.ts reads the session cookie, resolves the
user and flash from D1 via resolveSession (or KV cache when enabled), and
stores the Inertia adapter + user on c.var (the AppEnv context). Every
downstream handler reads c.var.inertia and c.var.user — no per-route
session plumbing.
6. Guards + handler
Section titled “6. Guards + handler”Guards are Hono middleware that short-circuit the chain by returning a
Response, or call next() to continue:
requireAuth— redirect to/loginif no session.guestOnly— redirect to/dashboardif already logged in.requireRole('admin')— redirect non-admins to/dashboard.
app.get("/dashboard", requireAuth, async (c) => { return c.var.inertia.render("Dashboard", { user: c.var.user });});Guards MUST call next() to continue — returning undefined without
next() errors with “Context is not finalized”.
7. Inertia render
Section titled “7. Inertia render”The handler calls c.var.inertia.render(component, props). The Inertia
adapter (src/server/inertia.ts) decides the response shape:
- Browser visit (no
X-Inertiaheader): full HTML with SSR markup +data-pageJSON embedded.renderToStringfromreact-dom/serverruns inside the Worker bundle. X-InertiaXHR: JSON page payload only — SPA navigation.- Asset-version mismatch:
409 + X-Inertia-Location— the client reloads.
8. onError / notFound
Section titled “8. onError / notFound”app.onError handles two cases:
ValidationFailed(TypeBox) → 422 with field errors, mapped back to the Inertia page component that owns the URL.- Other errors → 500
Internal Server Error, logged with the correlation ID.
app.notFound renders the NotFound component as a 404 Inertia page.
Workers-specific notes
Section titled “Workers-specific notes”- All DB calls are async. D1 is async — this cascades to every handler, auth function, and middleware.
Response.redirect()returns immutable headers on Workers. Hono’ssecureHeaderscrashes trying to append to a frozen Response. All redirects usenew Response(null, { status, headers: { location } }).- No custom compression. Wrangler/Miniflare auto-compresses with gzip/br. A custom compress middleware causes double-compression.
- Middleware runs in registration order. Global
app.use()middleware must precede the routes they cover. c.header()-queued headers are dropped when a handler returns a customResponse— cookie helpers append toc.res.headersinstead.
Next steps
Section titled “Next steps”- Conventions — the rules that keep handlers consistent.
- Sessions & guards — auth middleware in depth.
- Architecture overview — the full
src/layout.