Files
supabase__server/docs/auth-modes.md
T
Tomás Pozo 661329bb9e docs: add SDK documentation and SKILL.md (#20)
* docs: add initial documentation and skills.md

* docs: apply formatting

* docs: update SKILL.md to resolve docs from package location and ship docs with npm

SKILL.md now instructs agents to find documentation in the installed
@supabase/server package (node_modules or repo root) instead of using
relative paths. Added docs/ and SKILL.md to package.json files array
so they ship with npm installs.

* docs: add missing HTTPException import in error-handling example

* docs: fix strictNullChecks issues, duplicate variables, and missing context in examples

- Add non-null assertions (!) after error guards where TS can't narrow
  destructured result tuples
- Split duplicate variable declarations into separate code blocks
- Add missing imports and show where variables like `auth` come from
- Keep { data, error } destructuring pattern consistent with SDK convention

* docs: reframe as runtime-agnostic and add env auto-injection details

- getting-started: replace Edge Function framing with runtime-neutral
  language, explain module worker pattern works across Deno/Bun/Workers,
  add Runtimes section covering all supported environments
- webhooks: replace Deno.env with process.env for portable examples
- environment-variables: add "Auto-injected in" column distinguishing
  Platform vs Local CLI, reframe section headers
- auth-modes: clean up example key values
- core-primitives: clarify "Integration with frameworks" wording
- types: simplify TSDoc for publishable/secret key descriptions

* docs: add SSR frameworks guide and update references

Add docs/ssr-frameworks.md covering the pattern for using core
primitives in Next.js, SvelteKit, Nuxt, and Remix — cookie extraction,
env bridging, JWKS caching, and a complete Next.js adapter example.

Replace the basic SSR example in core-primitives.md with a pointer
to the new dedicated doc. Add SSR row to SKILL.md routing table.

* docs: add disclaimer of new package

* docs: extend explanation on keys env vars

* docs: add platform-specific quick starts to SKILL.md

Split the single generic example into per-platform sections
(Edge Functions, Cloudflare Workers, Hono, SSR Frameworks) so
AI agents pick the correct import specifier for each runtime.
Adds npm: prefix to all Deno examples and a Deno column to the
entry points table. Also adds createSupabaseContext examples.

* docs: add server-to-server quick starts and allow:always guardrails

Add secret key auth and webhook signature verification quick starts
to SKILL.md. Add explicit decision tree for allow:'always' so AI
agents confirm with the user before leaving endpoints unprotected.

* docs: add legacy keys warning, skills install, remove webhook docs

- Add legacy keys warning to SKILL.md (avoid anon/service_role keys)
- Add AI coding skills install section to README
- Add server-to-server quick start with caller code to README
- Add runtimes, documentation table, and named secret keys to README
- Remove verifyWebhookSignature references from all docs
- Delete docs/webhooks.md (code removal in separate PR)

* docs: add verify_jwt = false note for non-user auth modes

Edge Functions require verify_jwt = false in config.toml when
using allow: public, secret, or always — otherwise the platform
rejects requests before the handler runs.

* docs: add edge function recipes and refactor env vars doc

Add recipes for function-to-function calls, pg_net from database,
Stripe webhooks, and generic webhook signature verification.
Document the @supabase/server/wrappers entry point.
Refactor environment-variables.md into Supabase vs non-Supabase sections.

* docs: add security doc covering timing-safe comparison, auth model, CORS

* docs: link auth-modes timing-safe mentions to security.md

* docs: adding 'local cli' to secrets table

This envs will be injected from cli too

* docs: setting Deno as first installation choice

* docs: adding 'verify_jwt=false' disclaimer for non-user auth

* docs: split Deno/Supabase runtime section, merge Deno/Node/Bun

* docs(skills): adding legacy code migration example

* docs(skills): explaining why legacy code should be migrated

* docs: rewrite migration section, improve skill description triggers

---------

Co-authored-by: Kalleby Santos <kalleby_santos@hotmail.com>
2026-03-31 16:14:55 -05:00

6.7 KiB

Auth Modes

Overview

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 in supabase/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({ 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.

Public 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({ 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).

Secret mode

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...

Always mode

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.

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({ 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.

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

// Only accept the "internal" secret key
withSupabase({ allow: 'secret:internal' }, handler)

Wildcard — accept any key in the set

// Accept any publishable key
withSupabase({ allow: 'public:*' }, handler)

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

Which key matched?

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.

Combining named keys with other modes

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

How auth flows through the system

  1. extractCredentials(request) reads Authorization: Bearer <token> and apikey from headers
  2. Each mode in allow is tried in order against the extracted credentials
  3. First match wins — returns an AuthResult with authType, token, userClaims, claims, and keyName
  4. The auth result is used to create scoped clients (supabase with the user's token, supabaseAdmin with the secret key)
  5. Everything is bundled into a SupabaseContext and passed to your handler