@supabase/server - v1.7.0
    Preparing search index...

    Auth Modes

    Every request is validated against one or more auth modes before your handler runs. The auth config determines which modes are accepted.

    allow is deprecated. The auth option replaces the legacy allow option. allow still works (with a one-time console.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 on AuthResult and SupabaseContext was also renamed from authType to authMode so it matches the AuthMode type. The old names no longer work — update the option values you pass in and any runtime checks on ctx.authType (now ctx.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 the default key in SUPABASE_PUBLISHABLE_KEYS / SUPABASE_SECRET_KEYS. Use secret:<name> for a specific key or secret:* 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 in supabase/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({ 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.

    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 publishable matches only the default key. auth: 'publishable' validates only against the key named default in SUPABASE_PUBLISHABLE_KEYS. It does not fall back to your named keys, so if every key in the set is named, bare publishable never matches. Use publishable:<name> to target a named key, or publishable:* to accept any key in the set — see Named key syntax.

    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 secret matches only the default key. auth: 'secret' validates only against the key named default in SUPABASE_SECRET_KEYS. It does not fall back to your named keys, so if every key in the set is named, bare secret never matches. Use secret:<name> to target a named key, or secret:* to accept any key in the set — see Named key syntax.

    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.

    Accept multiple auth methods. Modes are tried in order — the first match wins. The array is for lists only: a single mode is written unwrapped (auth: 'user'), though auth: ['user'] means the same thing.

    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.

    Where 'none' can go. 'none' matches every request, so it only earns its place as the last entry of a list — auth: ['user', 'none'] is "a user if one is signed in, anonymous otherwise." Anything after it is unreachable, and a list of just ['none'] says nothing that a bare auth: 'none' doesn't. Both are type errors:

    withSupabase({ auth: ['user', 'none'] }, handler) // ✅ optional user
    withSupabase({ auth: 'none' }, handler) // ✅ open endpoint
    withSupabase({ auth: ['none'] }, handler) // ❌ use the bare 'none'
    withSupabase({ auth: ['none', 'user'] }, handler) // ❌ 'user' is unreachable

    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 InvalidJwtError (INVALID_JWT) — it will not silently fall through to 'publishable', 'secret', or 'none'. This prevents a bad token from being downgraded to a less-privileged auth mode. API keys behave differently: 'publishable' and 'secret' fall through when no apikey header is sent and also when the key matches none of the mode's configured keys, because a key held under another name may still match a later mode. When no mode matches, the final error reports the mismatch.

    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({ auth: 'publishable:web' }, handler)

    // Only accept the "internal" secret key
    withSupabase({ auth: 'secret:internal' }, handler)
    // Accept any publishable key
    withSupabase({ auth: 'publishable:*' }, handler)

    // Accept any secret key
    withSupabase({ auth: 'secret:*' }, handler)

    The Supabase Vercel integration sets SUPABASE_SECRET_KEY in Vercel to the most recently created secret key of the project. That value changes whenever a secret key is added or rotated, so it does not stay under any one name. Bare secret rejects it as soon as a key newer than default exists. For a function called from Vercel-hosted code, either accept any secret key or give Vercel a key of its own:

    // Accept whichever secret key the integration synced
    withSupabase({ auth: 'secret:*' }, handler)

    // Or create a secret key named "vercel", set it in Vercel yourself as a
    // variable the integration does not manage, and accept only that key
    withSupabase({ auth: 'secret:vercel' }, handler)

    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.

    withSupabase({ auth: ['user', 'publishable:web'] }, async (_req, ctx) => {
    // Accepts either a valid JWT or the "web" publishable key
    return Response.json({ authMode: ctx.authMode })
    })

    This library only understands the new-format API keys (sb_publishable_* / sb_secret_*) and JWT signing keys (JWKS-based verification). Old-format anon / service_role JWT-style API keys and legacy HS256-signed user JWTs are rejected, not silently downgraded.

    • On the API-key side, classifyApiKey recognizes old-format keys (eyJ...) only to produce a clearer INVALID_API_KEY error — it never accepts them.
    • On the JWT side, 'user' mode requires a kid in the token header; a legacy HS256 token signed with the project's shared JWT secret has no kid and is rejected before any JWKS lookup.

    To use this library, migrate your project to the new API key format and to JWT signing keys — see the API Keys and JWT Signing Keys guides.

    1. extractCredentials(request) reads Authorization: Bearer <token> and apikey from headers
    2. Each mode in auth is tried in order against the extracted credentials
    3. First match wins — returns an AuthResult with authMode, token, userClaims, jwtClaims, and keyName. A JWT that is present but fails verification terminates the chain with InvalidJwtError (INVALID_JWT). An apikey that matches none of a mode's keys falls through to the next mode, as does any absent credential.
    4. The auth result is used to create scoped clients (supabase with the user's token, supabaseAdmin with the secret key — constructed on its first property access)
    5. Everything is bundled into a SupabaseContext and passed to your handler