What we're enabling

Supabase Auth ships with a full OAuth 2.1 server that supports exactly the pieces the MCP authorization spec requires:

  • OAuth 2.1 with PKCE for the authorization code flow
  • RFC 8414 authorization server metadata at a well-known URI
  • RFC 7591 dynamic client registration — so Claude can self-register without you pre-creating a client
  • JWT access tokens that include a client_id claim and survive normal RLS
  • Refresh tokens with rotation

This is recent enough (2026 GA) that most existing MCP+Supabase tutorials predate it and recommend hand-rolled auth instead. We're using the supported, on-spec path.

1. Enable the OAuth server in the dashboard

Supabase project → AuthenticationSign in / ProvidersOAuth 2.1 server (left sidebar, "Authorization" section).

Toggle Enable OAuth 2.1 server on. The page will reveal:

  • The base URL for the auth server: https://<ref>.supabase.co/auth/v1
  • A switch labelled Dynamic Client Registration — turn it on
  • A switch labelled Client ID Metadata Documents — leave off for now; we're using DCR

2. Configure allowed redirect URIs

Still on the OAuth 2.1 server page, find Allowed redirect URIs and add at minimum:

  • http://127.0.0.1/* — local Claude Code uses ephemeral localhost ports during the flow
  • http://localhost/* — same, different name
  • (Optional) claude://callback — Claude Desktop's custom-scheme redirect

These are the URIs the authorization code can be returned to. Anything not on this list will be rejected (per OAuth 2.1's strict redirect URI matching rules).

3. Confirm the well-known endpoints

Hit them with curl — they're public:

curl https://<ref>.supabase.co/.well-known/oauth-authorization-server/auth/v1 | jq

You should see something like:

{
  "issuer": "https://<ref>.supabase.co/auth/v1",
  "authorization_endpoint": "https://<ref>.supabase.co/auth/v1/oauth/authorize",
  "token_endpoint": "https://<ref>.supabase.co/auth/v1/oauth/token",
  "registration_endpoint": "https://<ref>.supabase.co/auth/v1/oauth/register",
  "jwks_uri": "https://<ref>.supabase.co/auth/v1/.well-known/jwks.json",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none", "client_secret_basic", "client_secret_post"],
  "scopes_supported": ["openid", "email", "profile"]
}

Three things to notice:

  1. registration_endpoint — Claude will POST a small JSON document here to register itself.
  2. code_challenge_methods_supported: ["S256"] — PKCE is supported, MCP requires it.
  3. token_endpoint_auth_methods_supported: ["none", ...]none means public clients (no client secret) work, which is what MCP clients are.

If any of these are missing, double-check the dashboard toggles in step 1.

4. Also fetch the JWKS

curl https://<ref>.supabase.co/auth/v1/.well-known/jwks.json | jq

Returns a JWKS document with a single RSA public key. We'll use this in step 6 to verify Supabase-issued JWTs in the Edge Function. Copy the kid and alg for sanity — they should be RS256.

5. Choose how users sign in

Supabase OAuth's job is "obtain an access token after the user authenticates somehow." The "somehow" is whatever Supabase Auth providers you have configured. Common choices:

  • Magic link (no provider config needed; works out of the box)
  • Email + password (toggle on in Authentication → Sign-in Method)
  • Google / GitHub / etc. (configure each provider with its credentials)

For a team blueprint, email + password or a social provider like Google is friendliest. Magic link is fine for solo testing but a hassle if you want to log in repeatedly during development.

Enable one or more under Authentication → Sign-in Method in the dashboard.

6. Configure access-token claims

By default, Supabase JWTs include sub (the user ID), email, role: "authenticated", and a few standard OIDC claims. For RLS that's all we need — auth.uid() reads sub.

If you want richer claims (workspace IDs baked into the token, custom roles, etc.), you can add an access-token hook. For this blueprint we don't need one — RLS reads workspace membership from the database on every query, which is the correct level of indirection.

7. Sanity check the issuer URL — it matters

The MCP server (our Edge Function) is going to validate tokens by checking the iss claim. Supabase issues tokens with iss = https://<ref>.supabase.co/auth/v1. Note: with a trailing path component, no trailing slash.

In step 6 we'll hard-code https://<ref>.supabase.co/auth/v1 as the expected issuer and reject anything else. Worth confirming now by signing in once and inspecting the token:

# Get a token via the password grant (requires Email auth enabled)
curl -X POST "https://<ref>.supabase.co/auth/v1/token?grant_type=password" \
  -H "apikey: <your-anon-key>" \
  -H "Content-Type: application/json" \
  -d '{"email":"you@example.com","password":"your-password"}'

Take the access_token, paste it into jwt.io, and verify the iss claim matches what you expect.


The auth server is alive. Step 5 builds the MCP server skeleton on top of Hono and implements the Protected Resource Metadata document Claude will use to find this auth server.