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].
Extra keys on the returned object are ignored at runtime and not caught at
compile time, since run's return is contextually typed against a
Response | { [key]: … } union via a generic mapped type, a position
where TypeScript suppresses excess-property checks. The contributionOf
guard is the runtime backstop: it throws only when the key is missing
entirely (e.g. a computed or typo'd key the types could not see). Annotate
run's inner return type explicitly to catch excess keys at compile time
instead.
Typing:
fetch export on their own.
Their produced handler has an optional ctx, so it satisfies a bare
(req) => Response export and self-seeds a fresh context.In prerequisites require ctx. They can only nest
inside a wrapper (not necessarily the immediate one) that supplies those
keys, never as the bare fetch export, since that would make the
prerequisite a lie. An unmet prerequisite propagates outward until
something supplies it; if nothing does, the stack keeps a required
ctx, which fails only where it is checked against FetchHandler.
Annotate the outermost call to catch this at build time; an untyped
export default { fetch: … } will not catch it.Base resolves to its
constraint (the empty upstream, same as satisfies FetchHandler would
seed), and the cascade proceeds inward.satisfies FetchHandler on the outermost call: the produced type records what a
stack requires, not what it contributes, so an unannotated call has
nothing to check a duplicate key against, and it compiles silently. One
annotation covers any depth. pipeline has no such gap, since it
validates from its entries array; a stack that cannot carry the
annotation is better written flat.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.
runsees the inboundRequestand the upstream context. It either short-circuits with aResponse, or contributes a value by returning{ [key]: contribution }; the framework mergesresult[key]into the context and calls the inner handler.runis request-side by default: it runs before the handler and never observes the handler'sResponse. 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
finallycleanup), writerunas anasync function*instead ofasync. Code beforeyieldis the request phase; yielding hands back the contribution, and theyieldexpression resolves to the downstreamResponsefor 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 plainreturn new Response(...), and yield at most once.Composition.
withFoo(config, handler)produces a single(req, ctx) => Responsefunction. Middleware nest directly, and the outermost one is used as the runtime'sfetchhandler with no wrapper:export default { fetch: withFoo(config, handler) }. The host's second argument is a platform value (a Workersenv, a DenoServeHandlerInfo), not an upstream context. isContext detects this 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 behindgetEnv.