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 entry.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:
ctx, so it satisfies a bare(req) => Responsefetch entry and self-seeds a fresh context.Inprerequisites requirectx. They can only be nested inside a wrapper that supplies those keys — never a bare entry — which keeps the prerequisite from being a type-lie at the top level.Baseto aConflict<Key>sentinel; the stack fails to typecheck.Intype with no ceremony. For the innermost handler to ambiently see every upstream key, annotate the outermost withsatisfies FetchHandler(a type-only anchor).