Skip to main content
Middleware runs before your route handler. Use it for rate limits, logging, timeouts, and auth checks. A MiddlewarePipeline chains middleware together and short-circuits to a Response when one rejects the request. The pipeline works in both router styles. The route module wrapper changes:
  • App router API routes live at app/api/**/route.ts and export named HTTP method handlers such as GET or POST. The handler receives the Request directly.
  • Pages router API routes live at pages/api/** and export named HTTP method handlers or a default fallback handler. The handler receives an APIContext as ctx; use ctx.request when a middleware expects a Request.

Prerequisites

  • At least one API route in your project (see API routes).
  • The dev server running so you can hit the routes with curl.

Built-in middleware

CORS

For App and Pages API routes, configure CORS globally. Global security.cors is authoritative for CORS headers on automatic preflight, explicit OPTIONS, and actual route responses. This authority applies at the API router boundary:

Ownership rules

Method-local cors() middleware runs only inside the handler that calls it. It does not configure the framework-generated automatic OPTIONS response and cannot override the global policy after route dispatch. Use method-local cors() only in a lower-level pipeline whose response does not return through the Veryfront API router, such as a custom server adapter. Do not use it to configure CORS for an App or Pages API route. Root middleware resumes after the route returns and can change the final wire response after this boundary. Do not set policy-owned CORS headers in root middleware unless it deliberately owns the final CORS policy for that request. An explicit OPTIONS export is authoritative for the response status, body, and non-CORS headers. Veryfront uses automatic preflight only when the matched route has no executable OPTIONS handler. A callable default Pages route is also authoritative for OPTIONS and must branch on ctx.request.method when it owns multiple methods. Veryfront replaces policy-owned CORS headers on both forms with the validated global security.cors policy. An unauthenticated preflight for an auth-protected route is the exception. Veryfront returns automatic preflight without executing the explicit handler, so the browser can evaluate the global CORS policy before the actual request performs application authentication.

Rate limiting

Forwarded client-address headers are ignored by default. If the app is not behind a trusted reverse proxy, use keyGenerator with a trusted client or account identifier.

Logging

Timeout

Pipeline composition

Combine middleware into a pipeline:

Route-specific middleware

Apply middleware only to matching URL patterns:

Run the pipeline

Use handle() to run the middleware chain and then your route handler. If a middleware short-circuits (returns a Response, for example a rate-limit rejection), handle() returns that response and your handler never runs; otherwise your handler runs as the terminal step:
The same pipeline runs in a pages router handler via ctx.request:
Try it with the dev server running:
The response includes non-CORS headers added by the middleware that matched the request. The API router replaces policy-owned CORS headers with the global security.cors policy after the handler returns. Root middleware can subsequently change the response, so keep CORS ownership in one layer.
handle() vs execute(). execute() is a lower-level variant with no terminal handler: it returns the short-circuiting middleware’s Response, or a synthesized 404 Not Found when the chain passes through. It always resolves to a Response (never undefined), so if (await pipeline.execute(request)) is always truthy; use execute() only when a middleware is always expected to produce the response. For the common “middleware, then my route handler” case, prefer handle().

In-memory state across requests

Middleware and route handlers created at module scope, for example a rateLimit() store or a module-level counter, behave differently by environment:
  • In development, the dev server re-evaluates each route module on every request so edits hot-reload. A fresh module scope means module-level variables and default in-memory stores are re-created per request: a counter always reads back its initial value, and the default in-memory rate-limit store never accumulates across requests. To exercise threshold behavior in dev, drive the pipeline multiple times within a single request, or use an external store.
  • In production, the compiled route module is cached per release, so module-scoped state persists across requests within one server process and one release. It is still not shared across multiple instances, and it resets on every redeploy (and under memory-pressure eviction).
For anything that must be correct across requests, instances, and deploys, such as rate limiting, counters, and sessions, use an external store (see the RateLimitStore interface and the Redis example in the rate-limit reference) rather than module-scoped memory.

Cleanup callbacks

Register teardown logic that runs once per request, after the response body has finished, been canceled, or errored:
onTeardown callbacks run for every handle() and execute() call, so a module-scoped route pipeline fires them on each request. For streamed responses, cleanup waits until the body reaches EOF, is canceled by the consumer, or errors. Bodyless, locked, or already-read responses and handler or middleware exceptions clean up before the handle()/execute() promise resolves. Callback errors are logged and swallowed, never surfaced to the client. For long-lived pipelines that need one-shot cleanup on shutdown rather than per request, call pipeline.teardown() explicitly. Unlike the per-request run, teardown() drains and discards the callbacks so they never fire again.

Custom middleware

A middleware is a function that receives a context object and a next function. Access the request via c.request:
Add it to a pipeline:

Project-wide root middleware

Add middleware.ts, middleware.js, or middleware.mjs at the project root to run middleware before every project route. Export one middleware function or an array of functions:
Root middleware has the same ordering and short-circuit contract in local development, dedicated production servers, and the shared hosted runtime. The shared runtime resolves and compiles the file only after it has authenticated the project and selected its release or preview branch. Middleware receives only that request’s project environment through c.env. Production middleware is cached by project, environment, and immutable release or preview branch. Preview cache invalidation reloads the file after source changes, and the cache has a fixed entry limit. A missing file passes through normally. Root middleware runs in front of your project’s routes, not in front of the platform’s. A control-plane dispatch is the signed request the platform sends to your runtime to build a release asset manifest for a deploy, or to start, resume, or cancel a run. It bypasses root middleware and goes straight to the handler that verifies its signature. Middleware could not authorize one of these requests in any case. Infrastructure headers, including the dispatch signature, are withheld from project code, so middleware that gates on a credential rejects the platform’s request to build your own deploy. The signature-keyed bypass is narrow. It applies only to a request that both addresses one of those platform routes and carries the signature header the receiving handler verifies. An unsigned request to POST /api/control-plane/runs/{runId}/execute, an unsigned request to the agents list route, and any other path under /api/control-plane/, including your own routes in that namespace, still run your middleware. Three run-lifecycle routes are a longstanding exception and bypass middleware whether or not they are signed: POST /api/control-plane/runs/{runId}/stream, POST /api/control-plane/runs/{runId}/resume, and DELETE /api/control-plane/runs/{runId}. Do not rely on middleware to gate those three paths. A signed channel dispatch bypasses root middleware on the same terms. This is the request the platform sends to POST /channels/invoke to run one of your agents on a message from Slack or Discord, and it carries its own envelope under its own header, verified by the channel handler rather than by the control-plane signature check. Your middleware does not run for your project’s channel traffic. An unsigned request to POST /channels/invoke still runs your middleware. Production loading is fail-closed. If a declared middleware file cannot be read, compiled, or validated as a middleware export, a dedicated server does not start and a shared server returns an error only for the affected project request. Failed shared loads are not cached, so a corrected deployment can recover without restarting unrelated projects. Development loading remains nonfatal and reports the loading error in the server log.

Application authorization after login

Use Application authentication when the whole app needs a framework-owned login boundary. Veryfront admits the request before middleware runs, then middleware can apply application authorization using the normalized identity. Keep per-route policy in middleware or route code.

Example: site-wide HTTP Basic Auth

A common use of root middleware is password-gating an entire site: a staging environment, a preview, an internal tool.

Prefer the built-in gate

Before writing middleware, know that the runtime ships this as configuration. Set the operator environment variables in the deployment environment:
or configure it per project:
Read config secrets through getEnv from veryfront, not process.env: the hosted declarative config evaluator rejects process.env access as a forbidden capability, while getEnv works in local, dedicated, and shared runtimes. The built-in gate compares credentials in constant time and keeps the platform’s health probes and signed control-plane traffic working, so prefer it whenever “one username and password for the whole site” is all you need. (security.auth.bearer is the token-header equivalent; configure one or the other, not both.)

Custom Basic Auth middleware

Write it yourself when you need logic the built-in gate does not have, say, exempting a public path or accepting several credential pairs. This is the root middleware.ts file described above, which every runtime, including the shared hosted runtime, compiles and runs. Do not confuse it with the middleware.custom config option: config-declared middleware functions are rejected by hosted runtimes and work only when you run or self-host the project yourself.
Set BASIC_AUTH_USER and BASIC_AUTH_PASS in the project environment (.env locally, the environment settings of your deployment in production) and try it:
Two things the hand-rolled version gives up relative to the built-in gate: the === comparisons are not constant-time, and the exemptions described above still apply: signed platform dispatches bypass root middleware, so this gates your visitors, not the platform’s own traffic.

Verify it worked

Hit a route with and without the headers the middleware expects:
For global CORS, include an Origin header and confirm the configured Access-Control-Allow-Origin value is set on the response.