Skip to content
OTFotf
All posts

What runs before withSupabase in an MCP pipeline?

D
DaveAuthor
6 min read
What runs before withSupabase in an MCP pipeline?

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 a WWW-Authenticate challenge 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.

Two people at a desk with a wooden toy track and a rounded arch, one sliding a cube toward the arch while the other points at it.

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.

Same component. Web and mobile. One codebase.

The free, open-source SDK gives you components that work the same on web and mobile — one codebase. github.com/otf-kit/sdk

Get the free SDK

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. withSupabase checks 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 = false on the function. The function verifies tokens itself, and the gateway would otherwise turn away the tokenless discovery request before withOAuthProtectedResource can 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.

Two people at a desk with a small two-drawer cabinet, one sliding the top drawer open to show blank cards while the other holds a small round token.

Run these checks first

The first two come from the guide's local test. The rest are ours.

  1. Send a request with no token. The guide shows a 401 with a WWW-Authenticate header naming the metadata document.
  2. Fetch that metadata document. Its authorization_servers should be your Auth server, and on a custom domain it must match the issuer from Auth's discovery document.
  3. 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.
  4. Read the array aloud in review. Anything left of withSupabase should 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.

Sources

supabasemcpoauth
OTF SDK + Kits

Buy once, own the code. Ship with the agent you already use.

  • Free, open-source SDK — same component, web and mobile
  • Paid kits include AI configs + 40+ tested prompts — your agent reads the whole project
  • $99/kit or $149 for everything. No subscription, no sandbox limit.
Need more than components?

Full-stack kits.
Pay once, own the code.

Auth, database, and payments already connected — so you ship product, not setup. Or take the delivered kits in the Bundle.

Everything Bundle — $149See full pricing

Get the free AI configs pack

Pre-tuned AI configs for Cursor, Claude, and Lovable — drop them in and your AI tool instantly understands your project.

No spam. Unsubscribe any time.

Prefer the free SDK? Star it on GitHub →