Every request is validated against one or more auth modes before your handler runs. The allow config determines which modes are accepted.
| Mode | Credential required | Typical use case |
|---|---|---|
'user' |
Valid JWT in Authorization: Bearer <token> |
Authenticated user endpoints |
'public' |
Valid publishable key in apikey header |
Client-facing, key-validated endpoints |
'secret' |
Valid secret key in apikey header |
Server-to-server, internal calls |
'always' |
None | Open endpoints, custom auth wrappers |
Supabase Edge Functions: By default, the platform requires a valid JWT on every request same as
'user'. If your function uses'public','secret'or'always', disable the platform-level JWT check insupabase/config.toml:[functions.my-function] verify_jwt = false
The default. Verifies the JWT using your project's JWKS (JSON Web Key Set).
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
// ctx.userClaims has the caller's identity
console.log(ctx.userClaims!.id) // "d0f1a2b3-..."
console.log(ctx.userClaims!.email) // "user@example.com"
console.log(ctx.userClaims!.role) // "authenticated"
// ctx.claims has the raw JWT payload
console.log(ctx.claims!.sub) // same as userClaims.id
console.log(ctx.claims!.exp) // token expiration (epoch seconds)
// ctx.supabase is scoped to this user — RLS applies
const { data } = await ctx.supabase.from('todos').select()
return Response.json(data)
}),
}
The caller must send:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
userClaims vs supabase.auth.getUser(): userClaims is extracted from the JWT and is available instantly — no network call. It includes id, email, role, appMetadata, and userMetadata. For the full Supabase User object (email confirmation status, providers, linked identities), call ctx.supabase.auth.getUser(), which makes a request to the auth server.
Validates that the apikey header contains a recognized publishable key. Uses timing-safe comparison to prevent timing attacks. See security.md for details.
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ allow: 'public' }, async (_req, ctx) => {
// ctx.userClaims is null — no JWT involved
// ctx.supabase is initialized as anonymous (RLS anon role)
const { data } = await ctx.supabase.from('products').select()
return Response.json(data)
}),
}
The caller must send:
apikey: sb_publishable_abc123...
By default, public mode validates against the "default" key in SUPABASE_PUBLISHABLE_KEYS. Use named key syntax to target a specific key (see below).
Validates that the apikey header contains a recognized secret key. Same timing-safe comparison as public mode. See security.md for details.
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ allow: 'secret' }, async (_req, ctx) => {
// ctx.supabaseAdmin bypasses RLS — use for privileged operations
const { data } = await ctx.supabaseAdmin.from('config').select()
return Response.json(data)
}),
}
The caller must send:
apikey: sb_secret_xyz789...
No credentials required. Every request is accepted.
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ allow: 'always' }, async (_req, ctx) => {
// ctx.authType is 'always'
// ctx.userClaims is null
// ctx.supabase is anonymous (RLS anon role)
return Response.json({ status: 'healthy' })
}),
}
Use always for health checks, public APIs, or when you handle auth yourself inside the handler.
Accept multiple auth methods. Modes are tried in order — the first match wins.
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ allow: ['user', 'secret'] }, async (req, ctx) => {
// ctx.authType tells you which mode matched
if (ctx.authType === 'user') {
// Called by an authenticated user
const { data } = await ctx.supabase.from('reports').select()
return Response.json(data)
}
// Called by another service with a secret key
const { user_id } = await req.json()
const { data } = await ctx.supabaseAdmin
.from('reports')
.select()
.eq('user_id', user_id)
return Response.json(data)
}),
}
A request with a valid JWT matches 'user'. A request with a valid secret key matches 'secret'. A request with neither is rejected.
Fallthrough vs rejection. A mode is only "tried" when its credential is actually present. A request with no Authorization header moves on to the next mode. But if a JWT is present and fails verification (malformed, expired, wrong signature, or missing a sub claim), the request is rejected immediately with InvalidCredentialsError — it will not silently fall through to 'public', 'secret', or 'always'. The same rule applies on the API-key side: 'public' and 'secret' fall through only when no apikey header is sent. This prevents a bad credential from being downgraded to a less-privileged auth mode.
When your project has multiple API keys (e.g., separate keys for web, mobile, and internal services), use the colon syntax to validate against a specific named key.
Keys are stored as a JSON object in SUPABASE_PUBLISHABLE_KEYS or SUPABASE_SECRET_KEYS:
{
"default": "sb_publishable_123...",
"web": "sb_publishable_abc...",
"mobile": "sb_publishable_a1b2..."
}
// Only accept the "web" publishable key
withSupabase({ allow: 'public:web' }, handler)
// Only accept the "internal" secret key
withSupabase({ allow: 'secret:internal' }, handler)
// Accept any publishable key
withSupabase({ allow: 'public:*' }, handler)
// Accept any secret key
withSupabase({ allow: 'secret:*' }, handler)
When using named keys, ctx.authType tells you the mode and keyName on the AuthResult (from core primitives) tells you which key matched. In the high-level withSupabase wrapper, the matched key is used internally for client creation.
withSupabase({ allow: ['user', 'public:web'] }, async (_req, ctx) => {
// Accepts either a valid JWT or the "web" publishable key
return Response.json({ authType: ctx.authType })
})
extractCredentials(request) reads Authorization: Bearer <token> and apikey from headersallow is tried in order against the extracted credentialsAuthResult with authType, token, userClaims, claims, and keyName. A mode falls through to the next only when its credential is absent; a credential that is present but invalid terminates the chain with InvalidCredentialsError.supabase with the user's token, supabaseAdmin with the secret key)SupabaseContext and passed to your handler