If an MCP client calls your Supabase function, which checks run before withSupabase({ auth: 'user' }), and which run after it? The Supabase changelog for @supabase/middleware 1.0 (30 September 2026) answers it in one line: entries before withSupabase in the array run ahead of the auth gate, and entries after it receive the Supabase context. One detail matters here. The MCP entry in the example, withOAuthProtectedResource(), is not the gate. It answers OAuth discovery. The gate is withSupabase itself.
The facts below come from that changelog and from the "Deploy MCP servers" guide, both re-read on 4 October 2026. Where a sentence is our opinion, it is flagged "in our view" or "our suggested practice".
Entries before and after the gate
Supabase's Edge Functions example composes two entries and a handler:
pipeline(
[withOAuthProtectedResource(), withSupabase({ auth: 'user' })],
handler,
)
pipeline([...], handler) runs the entries in order. The changelog describes the pair as OAuth discovery for MCP clients, then verifying the caller's token and scoping a Supabase client to that user. The guide adds the detail:
-
withOAuthProtectedResource()runs before the auth gate. It serves the OAuth Protected Resource Metadata document so clients can find your authorization server, and it adds aWWW-Authenticatechallenge to responses for callers with no token. -
withSupabase({ auth: 'user' })rejects requests without a valid user token and gives your handler a Supabase client scoped to that user. - Your handler runs last and, per the guide, queries through Row Level Security.
So the answer to "which checks run before" is a discovery step, not an authorization step. Nothing before withSupabase has checked the token. A request that reaches an entry on its left may still be anonymous.
The image below shows a wooden cube being slid toward an arch on a toy track, with more cubes already past it. Where a piece sits on the track decides what it can know.
What belongs on each side
The order gives a simple rule, in our reading. Treat anything left of withSupabase as code that runs for an unknown caller, and keep it to work that needs no user, such as answering discovery. Treat anything right of it as code that may use the Supabase context. Per the changelog, any entry can also return a Response and stop the chain, and the guide says withOAuthProtectedResource answers the unauthenticated discovery request. Our reading: a request it answers never reaches the gate.
The changelog lists pieces that @supabase/server exports on their own, such as withClaims and withRequiredClaims. They are the natural next step if you want to check a claim beyond "has a valid token", but we have not tested how they compose in an MCP server, so check the reference before you rely on a specific order.
What the compiler does and does not check
Per the changelog, the handler sees every upstream key, correctly typed. Duplicate keys fail to compile, and so does an entry placed before its prerequisite. That catches a wiring mistake in the array. We expect it cannot know what your row policies say, because they live in your database.
The guide is explicit about where the data boundary sits: tool calls run as the user, and "your RLS policies decide what each client can see". The compiler cannot tell you a table has no policy, so the test that matters is behavioural, covered below. For the policy side, see our row-level security production checklist. For the same discovery-then-token shape on another stack, see our post on OAuth-protected MCP with Vercel Connect.
Setup the guide requires
-
ES256 or RS256 signing keys.
withSupabasechecks user tokens against the project's JWKS and rejects legacy HS256 tokens. If your project still uses the legacy secret, the guide tells you to switch in JWT Signing Keys. -
verify_jwt = falseon the function. The function verifies tokens itself, and the gateway would otherwise turn away the tokenless discovery request beforewithOAuthProtectedResourcecan answer it. - A sign-in frontend. The OAuth consent screen is hosted in your app, not by Supabase.
- Dynamic client registration. MCP clients register themselves, and the guide notes that this lets any compatible client register. It advises reviewing registered clients and letting users revoke grants.
The changelog says the engine runs anywhere fetch does, from Edge Functions to Node 22 or newer. Off Edge Functions, the guide notes there are no forwarded headers to derive public URLs from, so you pass resourceServer and authorizationServer to withOAuthProtectedResource yourself. With a custom domain, the guide says the entry builds the Auth URL from the domain the request arrived on, which does not match the issuer, so set authorizationServer explicitly.
The image below shows a small cabinet with two drawers on a desk, with only the top one pulled open to show blank cards. That is what a user-scoped client should look like to the data: one caller, one set of rows.
Run these checks first
The first two come from the guide's local test. The rest are ours.
- Send a request with no token. The guide shows a
401with aWWW-Authenticateheader naming the metadata document. - Fetch that metadata document. Its
authorization_serversshould be your Auth server, and on a custom domain it must match the issuer from Auth's discovery document. - Call a tool with a token for user A and a token for user B. Each should see only its own rows. This tests your policies, not the pipeline.
- Read the array aloud in review. Anything left of
withSupabaseshould be written for an anonymous caller.
If the MCP server needs an app around it
Arcade is not an MCP server and does not use @supabase/middleware. Its kit page lists a Hono API over Postgres and Drizzle, with no mention of Supabase or MCP. What you get is a $99 Expo codebase to read first, live today, as editable source in a repository you control, with a CLAUDE.md in the repo. The free demo opens before you buy. The SaaS Dashboard and Fitness kits are the other two live options.
FAQ
Is withOAuthProtectedResource the check that blocks bad tokens?
No. Supabase's guide places it before the auth gate, serving discovery metadata plus a WWW-Authenticate challenge. withSupabase({ auth: 'user' }) rejects requests without a valid user token.
Can I nest the entries instead of using pipeline?
Yes. The guide notes that withOAuthProtectedResource(withSupabase({ auth: 'user' }, handler)) behaves the same, and that both forms need @supabase/server 1.6.0 or later.
Will a breaking change land in a minor release?
From 1.0 the package follows semantic versioning, so breaking changes ship only in a new major, per the release note.
Does this only work on Edge Functions?
No. The guide shows the same pipeline in any runtime that takes a Request and returns a Response, such as a Next.js route handler or a Node server. It adds that you then pass the public URLs explicitly.


Top comments (0)