# Middleware

> Compose request-time concerns — auth, migrations, custom logic — in src/middleware.ts

Package: Koze
Canonical: https://kuratchi.dev/docs/koze/middleware
Markdown: https://kuratchi.dev/docs/koze/middleware.md

`src/middleware.ts` is where cross-cutting request-time policy composes. It's a single file that exports a default `defineMiddleware({...})` object; each property is a named **step** with up to four lifecycle hooks. The framework runs them in declaration order around matched pages and API routes.

Middleware is not the application router. Keep endpoint-specific HTTP behavior in [API Routes](/docs/koze/api-routes) under `src/routes/api/`; use middleware for policy that should wrap many routes, such as auth, migrations, route guards, response headers, tenant lookup, or forwarding a whole protocol to a Durable Object.

## Minimum file

```ts
// src/middleware.ts
import { defineMiddleware } from 'koze:middleware';

export default defineMiddleware({
  // empty — the framework runs without any custom middleware
});
```

A real app layers steps on top:

```ts
// src/middleware.ts
import { defineMiddleware } from 'koze:middleware';
import { autoMigrate } from '@kuratchi/kunii';
import { kyzenAuthMiddleware } from '@kuratchi/kyzen/middleware';
import { adminSchema } from './server/schemas/admin';
import { authConfig } from './server/auth-config';

export default defineMiddleware({
  // 1. Apply pending D1 migrations on the first request per worker isolate.
  migrate: autoMigrate({ DB: adminSchema }),

  // 2. Sessions, guards, OAuth, rate-limit, turnstile.
  auth: kyzenAuthMiddleware(authConfig),

  // 3. Your own steps.
  logging: {
    async request(ctx, next) {
      const start = Date.now();
      const res = await next();
      console.log(`${ctx.request.method} ${ctx.url.pathname} ${Date.now() - start}ms`);
      return res;
    },
  },
});
```

That's the entire integration shape. `autoMigrate` and `kyzenAuthMiddleware` are not framework features — they're ordinary middleware steps. Swap them for any third-party auth/ORM/observability tool by replacing the step.

## The four hooks

Each named step can declare up to four async hooks. Every hook is optional; declare only the ones you need.

| Hook | When it runs | Receives | Returns |
| --- | --- | --- | --- |
| `request` | Before routing | `(ctx, next)` | A `Response` (short-circuits) or `await next()` |
| `route` | After routing, before render | `(ctx, next)` | A `Response` or `await next()` |
| `response` | After render | `(ctx, response)` | A `Response` (the final one to send) |
| `error` | When any prior phase throws | `(ctx, error)` | A `Response` (custom error page) or null |

### `request` — gate, redirect, forward broad protocols

Runs before the framework matches a route. Common uses:

- Auth gates (redirect unauthenticated users)
- Multi-tenant routing (look up a hostname → tenant)
- Forwarding to a Durable Object (agents, sites)
- Rate limiting

Do not build a large API switchboard here. If the path is an endpoint such as
`/api/v1/platform/sites/:id/files`, put it in
`src/routes/api/v1/platform/sites/[id]/files.ts` and export method handlers
there.

```ts
auth: {
  async request(ctx, next) {
    if (ctx.url.pathname.startsWith('/admin/') && !ctx.locals.user) {
      return new Response('Unauthorized', { status: 401 });
    }
    return next();
  },
},
```

Returning a `Response` short-circuits — the framework stops here and skips routing/rendering. Calling `next()` continues the pipeline.

### `route` — augment the matched route

Runs after the framework has matched a route (`ctx.params` is populated) but before render. Useful for:

- Per-route logging that needs the matched pattern
- Adding common props to `ctx.locals`
- Final auth checks where the route pattern matters

```ts
audit: {
  async route(ctx, next) {
    ctx.locals.routePattern = ctx.params; // available to load() and actions
    return next();
  },
},
```

### `response` — modify the outgoing response

Runs after the route has rendered. Receives the final `Response`; return a (possibly modified) one.

```ts
headers: {
  async response(ctx, response) {
    // Headers from `Response.redirect()` and similar are immutable. Clone
    // to a mutable shape before setting custom headers.
    const out = new Response(response.body, response);
    out.headers.set('X-App-Version', '2.4.1');
    return out;
  },
},
```

### `error` — custom error pages

Runs when any earlier phase throws. Return a `Response` to override the framework's default 500 page; return `null`/`undefined` to let the framework handle it.

```ts
errors: {
  async error(ctx, error) {
    if (error instanceof MyDomainError) {
      return new Response(error.userMessage, { status: 400 });
    }
    return null; // let the framework render the default 500
  },
},
```

## The middleware context

Every hook receives a `MiddlewareContext`:

```ts
interface MiddlewareContext {
  request: Request;              // the incoming request
  env: Env;                      // worker bindings (D1, KV, R2, DO, …)
  ctx: ExecutionContext;         // Cloudflare execution context
  url: URL;                      // parsed URL
  params: Record<string, string>; // route params (after `route` phase)
  locals: Record<string, any>;   // shared scratchpad across hooks
}
```

`ctx.locals` is the scratchpad — set values in one hook, read them in another, then read the same values in `load()` / actions / `$server/*` modules via `locals`.

```ts
auth: {
  async request(ctx, next) {
    ctx.locals.user = await loadUser(ctx);
    return next();
  },
},
```

```ts
// any $server/*.ts module or route action
import { locals } from 'koze:request';

export async function load() {
  const { user } = locals as App.Locals & { user: { id: string; email: string } | null };
  return { user };
}
```

Type `App.Locals` in `src/app.d.ts` to get IDE autocomplete on `ctx.locals`:

```ts
declare global {
  namespace App {
    interface Locals {
      user: { id: string; email: string } | null;
    }
  }
}
export {};
```

## Execution order

Steps run in **declaration order** for `request` and `route` phases, **reverse declaration order** for the `response` phase (so the first `request` hook is the last to see the response — like Express middleware).

```ts
defineMiddleware({
  outer: { /* request → route → response → error */ },
  inner: { /* request → route → response → error */ },
});
```

Per-request flow:

```
outer.request → inner.request → routing → inner.route → outer.route
                              → render
                ← inner.response ← outer.response  (reverse order)
```

If `inner.request` throws, `outer.error` runs first (then `inner.error`).

If `outer.request` short-circuits with a `Response`, no other steps run; that response goes back through the (empty) response chain.

## Real example: kuratchi.cloud

`apps/web` is the framework's reference dashboard. Its `src/middleware.ts` composes four steps:

```ts
export default defineMiddleware({
  // 1. Cold-start ORM migrations (idempotent, runs once per isolate).
  migrate: autoMigrate({ DB: adminSchema }),

  // 2. Auth — sessions, guards, rate-limit, turnstile, OAuth.
  auth: kyzenAuthMiddleware(authConfig),

  // 3. Per-tenant site rendering — short-circuits when the hostname maps to a site.
  sites: {
    async request(ctx, next) {
      const siteResponse = await resolveSiteRequest(ctx.request);
      if (siteResponse) return siteResponse;
      return next();
    },
  },

  // 4. AI agent routing — forwards /agents/* to per-session DOs.
  agents: {
    async request(ctx, next) {
      if (!ctx.url.pathname.startsWith('/agents/')) return next();
      // verify token, forward to the right DO
      // …
    },
  },
});
```

Read `apps/web/src/middleware.ts` for the full source.

## Patterns

### Conditional middleware (dev vs prod)

```ts
import { dev } from 'koze:environment';

defineMiddleware({
  auth: dev
    ? { async request(_ctx, next) { return next(); } } // bypass in dev
    : kyzenAuthMiddleware(authConfig),
});
```

`dev` is a compile-time constant (`true` during `vite dev`, `false` during `vite build`), so the unused branch tree-shakes out of the production worker.

### Sharing data without globals

`ctx.locals` is per-request and reset between requests. Don't reach for module-level variables to share state — they leak across requests in long-lived isolates. The `locals` object is the right scratchpad.

### Returning early without a `Response`

Hooks must return either a `Response` (short-circuit) or the result of `await next()` (continue). Returning `undefined`/`null` from `request`/`route` is treated as "did nothing, continue" by the framework, but explicit `return next()` is clearer.

### Handling `Response.redirect()` and immutable headers

`Response.redirect(...)` and similar constructors produce responses with frozen headers. The framework already copies these to a mutable shape before passing them to your `response` hooks, so you can call `response.headers.set(...)` directly. If you build your own response in code (e.g. inside an action), wrap it with `new Response(body, response)` before mutating headers.

## Where things live

| Use case | Where |
| --- | --- |
| Auth, ORM auto-migration, route guards, custom request logic | `src/middleware.ts` |
| JSON APIs, webhooks, uploads, health checks, agent-visible HTTP endpoints | `src/routes/api/**/*.ts` |
| Build-time options (security headers, directory overrides) | `koze({...})` in `vite.config.ts` |
| Cloudflare deployment (secrets, routes, custom bindings) | `wrangler.jsonc` |
| ORM schema definitions | `src/server/schemas/*.ts` (imported by middleware) |

See [Configuration](/docs/koze/configuration) for the full split.

## Read next

- [Request APIs](/docs/koze/request-apis) — the request context shape, redirect helpers, env helpers
- [API Routes](/docs/koze/api-routes) — file-based HTTP endpoints under `src/routes/api`
- [Auth](/docs/kyzen/getting-started) — wire `kyzenAuthMiddleware` into your project
- [ORM Migrations](/docs/kunii/migrations) — `autoMigrate` as a middleware step
