Defines a middleware.
run sees the inbound Request and the upstream context. It either short-circuits with a Response, or contributes a value by returning { [key]: contribution }; the framework merges result[key] into the context and calls the inner handler.
run is request-side by default: it runs before the handler and never observes the handler's Response. Response-shaped concerns (CORS, envelopes) belong in the handler or a .then() on the stack instead.
Response seam. When a middleware needs to see the way out (stamp headers, time the request, run finally cleanup), write run as an async function* instead of async. Code before yield is the request phase; yielding hands back the contribution, and the yield expression resolves to the downstream Response for the response phase, the only place a middleware observes the handler's response. Short-circuit the same way as the request-side path, with a plain return new Response(...), and yield at most once.
Composition. withFoo(config, handler) produces a single (req, ctx) => Response function. Middleware nest directly, and the outermost one is used as the runtime's fetch handler with no wrapper: export default { fetch: withFoo(config, handler) }. The host's second argument is a platform value (a Workers env, a Deno ServeHandlerInfo), not an upstream context. isContext detects this and seeds a fresh context instead of merging it, so platform arguments never leak into ctx. They are only captured as the module-scoped platform env behind getEnv.
import { defineMiddleware } from '@supabase/middleware'
export const withFeatureFlag = defineMiddleware<
'featureFlag',
{ name: string; evaluate: (req: Request) => boolean },
{},
{ name: string; enabled: true }
>({
key: 'featureFlag',
run: (config) => async (req) => {
if (!config.evaluate(req)) {
return Response.json({ error: 'feature_disabled' }, { status: 404 })
}
return { featureFlag: { name: config.name, enabled: true } }
},
})