Each example builds one Fetch handler with pipeline. Entries run in array order on the request. The handler runs last and reads what the entries contributed to ctx.
withCors runs first. It answers the CORS preflight (an OPTIONS request carrying Access-Control-Request-Method) with 204 before anything else runs. On the way out, it stamps Access-Control-* headers onto the response when the request's Origin is allowed.
withFeatureFlag runs second. evaluate receives the request and decides: true admits it, false rejects it. It can be async, and it can return a verdict object instead of a boolean. A rejected request gets a 404 and never reaches the handler. An admitted request reaches the handler with ctx.featureFlag set.
Both middleware ship in @supabase/middleware. The handler is plain Fetch, so the same stack runs on Node, Deno, Bun, and Cloudflare Workers; only the host entry point differs.
import { pipeline } from '@supabase/middleware'
import { withCors } from '@supabase/middleware/cors'
import { withFeatureFlag } from '@supabase/middleware/feature-flag'
export default {
fetch: pipeline(
[
withCors({ origin: ['https://app.example.com'], credentials: true }),
withFeatureFlag({
name: 'beta-checkout',
evaluate: (req) => req.headers.get('x-beta') === '1',
}),
],
async (_req, ctx) => Response.json({ feature: ctx.featureFlag.name }),
),
}
Middleware from @supabase/server drop into the same array. withCors runs first, so the preflight is answered before the auth gate. withSupabase runs second with cors: 'disabled', because withCors owns CORS here. It verifies the caller's JWT and puts an RLS-scoped client on ctx.supabase. A request without valid credentials gets a 401 and never reaches the flag or the handler.
The flag runs last. evaluate reads an environment variable through getEnv, so the endpoint returns 404 to every signed-in caller until BETA_CHECKOUT is set to on. Flip the variable to roll the endpoint out.
Without withCors, withSupabase answers every OPTIONS request itself with 204 and wildcard CORS headers (Access-Control-Allow-Origin: *). That is enough when you do not need an origin allowlist. A layer that owns CORS must sit before withSupabase in the array. Placed after it, the preflight reaches the auth gate and gets a 401.
The entry form of withSupabase is alpha. It needs @supabase/server 1.6.0 or later.
import { getEnv, pipeline } from '@supabase/middleware'
import { withCors } from '@supabase/middleware/cors'
import { withFeatureFlag } from '@supabase/middleware/feature-flag'
import { withSupabase } from '@supabase/server'
export default {
fetch: pipeline(
[
withCors({ origin: ['https://app.example.com'] }),
withSupabase({ auth: 'user', cors: 'disabled' }),
withFeatureFlag({
name: 'beta-checkout',
evaluate: () => getEnv('BETA_CHECKOUT') === 'on',
}),
],
async (_req, ctx) => {
const { data, error } = await ctx.supabase.from('carts').select()
if (error) return Response.json({ error: 'query_failed' }, { status: 500 })
return Response.json(data)
},
),
}