The literal-string key contributed to ctx.
Configuration object the middleware accepts.
Upstream prerequisites besides BaseContext. Defaults to none.
Shape of the value placed at ctx[Key].
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 } }
},
})
Defines a middleware.
A middleware runs against an inbound
Requestand the upstream context. It either short-circuits by returning aResponse, or contributes a typed value atctx[key]by returning a single-key object{ [key]: contribution }— the framework picksresult[key], merges it into the context, and calls the inner handler. Extra keys on the returned object are ignored at runtime. Note they are not caught at compile time:run's return is contextually typed against aResponse | { [key]: … }union via a generic mapped type, a position where TypeScript suppresses excess-property checks — so a stray sibling key slips past the types (harmlessly). The runtime contributionOf guard is the backstop, and it throws only when the key is missing entirely (e.g. a computed/typo'd key the types couldn't see). To opt into the excess check on a given middleware, annotaterun's inner return type explicitly.runis request-side by default (the common case): it runs before the handler and never observes the handler'sResponse. Response-shaped concerns (CORS, envelopes) normally belong in the handler or a.then()on the stack.Response seam (escape hatch). When a middleware genuinely needs to see the way out — stamp headers, time the request, run
finallycleanup — writerunas anasync function*instead ofasync.yieldis the seam: code before it is the request phase; youyieldthe contribution and theyieldexpression resolves to the downstreamResponsefor the response phase.yieldalways means "run downstream and hand me the response" — short-circuit with a plainreturn new Response(...), exactly as the request-side path does, and yield at most once. This is the one place a middleware observes the handler's response, and writingfunction*is the visible, opt-in signal.withFoo(config, handler)produces a single(req, ctx) => Responsefunction. Middleware nest directly, and the outermost is used as the runtime'sfetchhandler with no wrapper —export default { fetch: withFoo(config, handler) }. When the host invokes it, the second argument is a platform value (a Workersenv, a DenoServeHandlerInfo), not an upstream context; the wrapper detects this via isContext and seeds a fresh context instead of merging it, so platform arguments never leak intoctx— they are only captured as the module-scoped platform env behind the importablegetEnv.Typing:
fetchexport on their own. Their produced handler has an optionalctx, so it satisfies a bare(req) => Responseexport and self-seeds a fresh context.Inprerequisites requirectx. They can only be nested inside a wrapper that supplies those keys — never the barefetchexport — which keeps the prerequisite from being a type-lie at the top level. The supplying wrapper need not be the immediately enclosing one, and no annotation is needed: an unmet prerequisite is republished by each layer that doesn't contribute it, travelling outward until one does. If none does, the stack keeps a requiredctxand fails wherever it is checked against FetchHandler — but an untypedexport default { fetch: … }is no such check, so annotate the outermost call to catch it at build time.Baseresolves to its constraint — the empty upstream, which is exactly what asatisfies FetchHandlerwould seed — and the cascade proceeds inward from there.satisfies FetchHandleron the outermost call. The produced handler type records the upstream a stack requires, never the keys it contributes, so an unannotated enclosing call has nothing to check its own key against and a duplicate compiles silently. One annotation covers any depth. pipeline has no such gap — it validates from its entries array — so a stack that cannot carry the annotation is better written flat.