9.2 KiB
Auth Modes
Overview
Every request is validated against one or more auth modes before your handler runs. The auth config determines which modes are accepted.
allowis deprecated. Theauthoption replaces the legacyallowoption.allowstill works (with a one-timeconsole.warn) but will be removed in a future major release. Migration is a find-and-replace:allow:→auth:.
Breaking — auth API renamed.
'always'is now'none'and'public'is now'publishable'(including the colon variants'public:<name>'→'publishable:<name>'). The field onAuthResultandSupabaseContextwas also renamed fromauthTypetoauthModeso it matches theAuthModetype. The old names no longer work — update the option values you pass in and any runtime checks onctx.authType(nowctx.authMode).
| Mode | Credential required | Typical use case |
|---|---|---|
'user' |
Valid JWT in Authorization: Bearer <token> |
Authenticated user endpoints |
'publishable' |
Valid default publishable key in apikey header |
Client-facing, key-validated endpoints |
'secret' |
Valid default secret key in apikey header |
Server-to-server, internal calls |
'none' |
None | Open endpoints, custom auth wrappers |
Bare
'publishable'/'secret'match only thedefaultkey inSUPABASE_PUBLISHABLE_KEYS/SUPABASE_SECRET_KEYS. Usesecret:<name>for a specific key orsecret:*to accept any key in the set — see Named key syntax.
Supabase Edge Functions: By default, the platform requires a valid JWT on every request same as
'user'. If your function uses'publishable','secret'or'none', disable the platform-level JWT check insupabase/config.toml:[functions.my-function] verify_jwt = false
User mode
The default. Verifies the JWT using your project's JWKS (JSON Web Key Set).
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ auth: '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.jwtClaims has the raw JWT payload
console.log(ctx.jwtClaims!.sub) // same as userClaims.id
console.log(ctx.jwtClaims!.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.
Publishable mode
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({ auth: 'publishable' }, 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...
Bare
publishablematches only thedefaultkey.auth: 'publishable'validates only against the key nameddefaultinSUPABASE_PUBLISHABLE_KEYS. It does not fall back to your named keys, so if every key in the set is named, barepublishablenever matches. Usepublishable:<name>to target a named key, orpublishable:*to accept any key in the set — see Named key syntax.
Secret mode
Validates that the apikey header contains a recognized secret key. Same timing-safe comparison as publishable mode. See security.md for details.
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ auth: '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...
Bare
secretmatches only thedefaultkey.auth: 'secret'validates only against the key nameddefaultinSUPABASE_SECRET_KEYS. It does not fall back to your named keys, so if every key in the set is named, baresecretnever matches. Usesecret:<name>to target a named key, orsecret:*to accept any key in the set — see Named key syntax.
None mode
No credentials required. Every request is accepted.
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ auth: 'none' }, async (_req, ctx) => {
// ctx.authMode is 'none'
// ctx.userClaims is null
// ctx.supabase is anonymous (RLS anon role)
return Response.json({ status: 'healthy' })
}),
}
Use none for health checks, public APIs, or when you handle auth yourself inside the handler.
Array syntax (multiple modes)
Accept multiple auth methods. Modes are tried in order — the first match wins.
import { withSupabase } from '@supabase/server'
export default {
fetch: withSupabase({ auth: ['user', 'secret'] }, async (req, ctx) => {
// ctx.authMode tells you which mode matched
if (ctx.authMode === '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 'publishable', 'secret', or 'none'. The same rule applies on the API-key side: 'publishable' 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.
Named key syntax
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..."
}
Target a specific key
// Only accept the "web" publishable key
withSupabase({ auth: 'publishable:web' }, handler)
// Only accept the "internal" secret key
withSupabase({ auth: 'secret:internal' }, handler)
Wildcard — accept any key in the set
// Accept any publishable key
withSupabase({ auth: 'publishable:*' }, handler)
// Accept any secret key
withSupabase({ auth: 'secret:*' }, handler)
Which key matched?
When using named keys, ctx.authMode 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.
Combining named keys with other modes
withSupabase({ auth: ['user', 'publishable:web'] }, async (_req, ctx) => {
// Accepts either a valid JWT or the "web" publishable key
return Response.json({ authMode: ctx.authMode })
})
How auth flows through the system
extractCredentials(request)readsAuthorization: Bearer <token>andapikeyfrom headers- Each mode in
authis tried in order against the extracted credentials - First match wins — returns an
AuthResultwithauthMode,token,userClaims,jwtClaims, andkeyName. A mode falls through to the next only when its credential is absent; a credential that is present but invalid terminates the chain withInvalidCredentialsError. - The auth result is used to create scoped clients (
supabasewith the user's token,supabaseAdminwith the secret key — constructed on its first property access) - Everything is bundled into a
SupabaseContextand passed to your handler