What's new#
@supabase/middleware 1.0.0 is on npm and JSR under the latest tag. From 1.0 the package follows semantic versioning: breaking changes ship only in a new major.
It is a small, MIT-licensed engine for per-request logic on Web Fetch handlers:
defineMiddlewarewrites one piece of per-request logic. It contributes one typed key to a sharedctx.pipeline([...], handler)runs several in order and returns the fetch handler. Middleware also nest directly, sopipelineis optional.- The handler sees every upstream key, correctly typed. Duplicate keys fail to compile, and so does a middleware placed before its prerequisite.
- Any middleware can return a
Responseand stop the chain, or read and change the response on the way out. - Two middleware ship with the engine:
withCorsandwithFeatureFlag. Everything else is yours, or comes from a package.
The engine runs wherever fetch runs: Supabase Edge Functions, Vercel Functions, Cloudflare Workers, Deno, Bun, and Node 22 or newer. Inside Hono, H3, Elysia, NestJS, or TanStack Start, the same middleware runs through a short, typechecked bridge you copy into your app. No adapter package, no new dependency. See the frameworks guide.
@supabase/server is built on the engine and ships the Supabase middleware. withSupabase(config) with no handler is a pipeline entry; withSupabase(config, handler) wraps a single handler. Entries before it in the array run ahead of the auth gate, and entries after it receive the Supabase context. The @supabase/server/middleware/* subpaths export the pieces on their own: withClaims, withRequiredClaims, withSupabaseClient, withSupabaseAdminClient, withPostgresClient, and withPostgresAdminClient. withOAuthProtectedResource answers OAuth discovery for MCP clients. All of them are stable as of @supabase/server 1.9.0.
How to use it#
_10npm install @supabase/middleware
On Edge Functions, import it directly. This example answers OAuth discovery for MCP clients, verifies the caller's token, and hands the handler a Supabase client scoped to that user. The Bring your own MCP guide builds a full MCP server on the same two entries:
_14import { pipeline } from 'npm:@supabase/middleware@1'_14import { withOAuthProtectedResource, withSupabase } from 'npm:@supabase/server@1'_14_14Deno.serve(_14 pipeline(_14 // 1. OAuth discovery for MCP clients, 2. verify the user's token and scope a client to them_14 [withOAuthProtectedResource(), withSupabase({ auth: 'user' })],_14 async (req, { supabase }) => {_14 // RLS scopes this query to the signed-in user_14 const { data } = await supabase.from('tasks').select('id, title')_14 return Response.json(data)_14 },_14 ),_14)
Writing your own is one call:
_10import { defineMiddleware } from '@supabase/middleware'_10_10const withRequestId = defineMiddleware<'requestId', void, Record<never, never>, string>({_10 key: 'requestId',_10 run: () => async (req) => ({_10 requestId: req.headers.get('x-request-id') ?? crypto.randomUUID(),_10 }),_10})
The authoring guide covers prerequisites, the response seam, bundling several middleware into one, and publishing to npm and JSR.
Why we built this#
Per-request logic is stuck where you wrote it. Move to a different runtime and you write it again. Adopt a framework and you rewrite it as that framework's middleware, which cannot leave that framework. Start a second project and you copy the file across. This is the code least worth rewriting: verifying a caller, scoping a database connection to them, handling CORS, checking a flag. It is fiddly, security-sensitive, and easy to get subtly wrong the second time.
@supabase/server has run on this engine since 1.5.1, released on August 31, 2026. withSupabase is a composite of single-key parts: the auth gate, claims, CORS, the Supabase client, the admin client. withOAuthProtectedResource handles the whole OAuth protected-resource flow an MCP client expects, and it is one entry in the pipeline. None of it uses a private API. What you get is the same primitive we use.