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.tsand export named HTTP method handlers such asGETorPOST. The handler receives theRequestdirectly. - Pages router API routes live at
pages/api/**and export named HTTP method handlers or adefaultfallback handler. The handler receives anAPIContextasctx; usectx.requestwhen a middleware expects aRequest.
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. Globalsecurity.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-localcors() 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
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
Usehandle() 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:
ctx.request:
security.cors policy after the handler returns. Root middleware can
subsequently change the response, so keep CORS ownership in one layer.
handle()vsexecute().execute()is a lower-level variant with no terminal handler: it returns the short-circuiting middleware’sResponse, or a synthesized 404 Not Found when the chain passes through. It always resolves to aResponse(neverundefined), soif (await pipeline.execute(request))is always truthy; useexecute()only when a middleware is always expected to produce the response. For the common “middleware, then my route handler” case, preferhandle().
In-memory state across requests
Middleware and route handlers created at module scope, for example arateLimit() 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).
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 anext function. Access the request via c.request:
Project-wide root middleware
Addmiddleware.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:
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: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 rootmiddleware.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.
BASIC_AUTH_USER and BASIC_AUTH_PASS in the project environment
(.env locally, the environment settings of your deployment in production)
and try it:
=== 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:Origin header and confirm the configured
Access-Control-Allow-Origin value is set on the response.