Files
supabase__server/docs/security.md
T
Tomás Pozo 18818e0b5d Pre-v1 API cleanup: rename allow/authType/claims, narrow authKeyName, refresh adapter docs (#48)
* feat: rename `allow` config option to `auth`

Aligns the SDK with Supabase CLI terminology — `auth: 'user'` reads
more naturally than `allow: 'user'`. The legacy `allow` key still
works (with a one-time `console.warn` per process) and will be
removed in a future major release; when both `auth` and `allow` are
provided, `auth` wins. Also exports new `AuthMode` / `AuthModeWithKey`
types alongside deprecated `Allow` / `AllowWithKey` aliases.

* feat!: rename auth mode values `'always'` → `'none'` and `'public'` → `'publishable'`

Aligns auth-mode values with Supabase CLI terminology. `'none'` reads more
directly than `'always'` for "no authentication required", and
`'publishable'` matches the `SUPABASE_PUBLISHABLE_KEY(S)` env var names.
`'secret'` and `'user'` are unchanged.

BREAKING CHANGE: the `'always'` and `'public'` mode values no longer work.
Replace `auth: 'always'` with `auth: 'none'`, `auth: 'public'` with
`auth: 'publishable'`, and `auth: 'public:<name>'` with
`auth: 'publishable:<name>'`. Runtime checks like
`ctx.authType === 'public'` must be updated to
`ctx.authType === 'publishable'`.

* feat!: rename `authType` field to `authMode` on `AuthResult` and `SupabaseContext`

Lines the field name up with its type — `authMode: AuthMode`. Reads more
naturally for both humans and AI agents working with the API.

BREAKING CHANGE: the `authType` field was renamed to `authMode` on
`AuthResult` (returned by `verifyAuth` / `verifyCredentials`) and on
`SupabaseContext` (passed to handlers). Find-and-replace
`ctx.authType` → `ctx.authMode` and `auth.authType` → `auth.authMode`
across your codebase.

* feat!: rename `claims` field to `jwtClaims` on `AuthResult` and `SupabaseContext`

Pairs naturally with `userClaims` and makes the snake_case JWT payload
distinct from the normalized identity view at a glance.

* refactor!: narrow `SupabaseContext.authKeyName` to `string | undefined`

The field used to be `string | null | undefined` (optional + explicitly
nullable), forcing consumers to handle two absence values. Collapse to a
single representation by dropping `null`: the property is simply omitted
for `'user'` and `'none'` modes, which don't match a named key.

`AuthResult.keyName` keeps its `string | null` shape — it's the
low-level type where the field is always present and `null` actively
signals "no named key for this mode."

* docs: add publishable-key example to README quick start

The auth-modes table documented the publishable mode but the quick start
only showed user, none, secret, dual, and server-to-server examples,
leaving readers without a concrete shape for publishable. Slot it
between the "no auth" and "secret" examples so the progression reads
no key → publishable (anon, key-gated) → secret (admin, key-gated).

The example clarifies the resulting client behavior — `supabase` is
anonymous, RLS still applies, and the publishable key is a client gate
rather than a user identity — which is the most common point of
confusion vs. `auth: 'secret'`.

* docs: update skill description

* docs: update ssr references accross docs and skill

* docs(adapters): add ecosystem index + community contribution guide

Adds src/adapters/README.md (index, maintenance model, contribution
checklist) and docs/adapters/h3.md. Moves docs/hono-adapter.md into
docs/adapters/. Slims the top-level README Framework Adapters section
to a canonical adapter table + brief examples. Updates CONTRIBUTING.md,
docs/getting-started.md, and the supabase-server skill to reference the
new paths.

* docs: sweep adapter and SSR docs to use renamed API

The cherry-picked docs commits were authored before the API renames in
this branch, so the new content arrived using `allow:`, `'always'`,
`'public'`, `claims`, `authType`, and `AllowWithKey`. Update the
newly-arrived files in line with the renamed API:

- docs/adapters/h3.md — `allow:` → `auth:`, `claims, authType` →
  `jwtClaims, authMode` throughout
- docs/ssr-frameworks.md — composed Next.js adapter example now uses
  `auth:` / `AuthModeWithKey` / `jwtClaims` / `authMode`
- src/adapters/README.md — adapter-test checklist mentions the four
  current modes (`'user'`, `'publishable'`, `'secret'`, `'none'`)
- CONTRIBUTING.md — same wording fix in the adapter-PR section
- skills/supabase-server/SKILL.md — top-level skill description points
  at `auth:` and the new mode values; legacy patterns folded into the
  existing migration trigger
- src/adapters/hono/middleware.ts — inline comment example uses `auth:`
  in both halves rather than mixing legacy and current option names
- README.md — collapsed Hono and H3 quick-start snippets use `auth:`

Migration prose (`README.md` callout, `docs/auth-modes.md` callout,
`docs/api-reference.md` deprecated-aliases section, `SKILL.md`
migration callouts) intentionally still references the old names; they
document the migration itself.

* docs: reframe Beta disclaimer for v1 launch + extract MIGRATION.md

The Beta callout ("APIs and documentation may change") directly
contradicts the SemVer commitment that v1 makes. For launch material
that pins to v1, the contradiction undermines the stability message
the version number is meant to carry.

Replace it with a v1.0 callout that leads with stability under SemVer
and follows with honest "active development continues" framing — new
adapters and ergonomic improvements in minor releases, breaking
changes only ever in a major bump.

Move the v0 → v1 rename map out of the README and into a dedicated
MIGRATION.md. The README quick start was buried under 20+ lines of
migration tables that only matter to upgraders, not first-time
readers — exactly the wrong tradeoff at launch. New short callout
points upgraders at MIGRATION.md.

SKILL.md gets the same Beta → v1.0 swap. The agent-operational
migration rules (lines 12-14: "always emit `auth:` in new code", "the
new mode values are `'none'` / `'publishable'`") are kept inline —
they're rules the agent applies every time it writes code, not
user-facing migration steps, so they don't belong in MIGRATION.md.

* docs: reframe v1.0 callout as "Public Beta" to match Supabase house style

The previous "Stable under SemVer; active development continues" framing
mixed two distinct axes — code stability (SemVer) and product
lifecycle stage (Public Beta / GA) — into the SemVer line. Several
Supabase docs run those independently: a release can be v1+ in SemVer
terms and still labeled Public Beta in lifecycle terms.

Lead with both signals in the headline: "v1.0 — Public Beta." Keep the
SemVer commitment ("breaking changes only ship as a major bump") so
launch copy can pin to v1, and pair it with the Public Beta lifecycle
stage so readers know the product line is still early. Same swap in
the SKILL.md mirror.
2026-05-06 12:16:14 +03:00

5.4 KiB

Security

This document explains the security decisions behind @supabase/server. It's informational — you don't need to read this to use the package, but it helps if you want to understand why things work the way they do.

Timing-safe credential comparison

API keys are compared using constant-time comparison to prevent timing attacks.

A naive string comparison (===) short-circuits on the first mismatched character. An attacker can measure response times to guess the key one character at a time. With enough requests, this leaks the full key.

The package uses a double-HMAC technique: both strings are HMAC'd with a random ephemeral key, then the resulting digests are compared byte-by-byte with a constant-time XOR loop. This ensures that comparison time is independent of where (or whether) the strings differ.

This applies to:

  • Publishable key verification (auth: 'publishable') — compares the apikey header against stored publishable keys
  • Secret key verification (auth: 'secret') — compares the apikey header against stored secret keys

See src/core/utils/timing-safe-equal.ts for the implementation.

Auth mode security model

Each auth mode provides a different level of trust:

Mode What it verifies Who the caller is supabase client supabaseAdmin client
user JWT signature against JWKS An authenticated user Row-Level Security Full access
publishable Publishable API key (timing-safe) A known client app Row-Level Security Full access
secret Secret API key (timing-safe) A trusted server/service Full access Full access
none Nothing — all requests are accepted Unknown Row-Level Security Full access

Key implications:

  • user mode verifies the JWT using a local JWKS (JSON Web Key Set). The token must contain a sub claim. Verification uses the jose library's jwtVerify with a local key set — no network calls to an auth server.
  • publishable and secret modes compare the apikey header against known keys. The comparison is timing-safe. If you use named keys (auth: 'secret:automations'), only that specific key is accepted — this follows the principle of least privilege.
  • none mode performs zero authentication. The handler runs for every request. The supabaseAdmin client is still available, so a compromised none endpoint with write operations is a security risk. Only use it for truly public endpoints or when you implement your own auth (e.g., webhook signature verification).

Named key isolation

Instead of accepting any valid API key, you can restrict an endpoint to a specific named key:

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

// Only accepts the "automations" secret key
withSupabase({ auth: 'secret:automations' }, handler)

This limits the blast radius if a key is compromised. An attacker with the web publishable key cannot access an endpoint that requires secret:automations. Named keys also make it easier to rotate or revoke access for a specific consumer without affecting others.

JWT verification

JWT verification in user mode works as follows:

  1. The Authorization: Bearer <token> header is extracted from the request
  2. The token is verified against the JWKS from the SUPABASE_JWKS environment variable
  3. Verification uses jose's jwtVerify with a local key set — there are no network calls to a JWKS endpoint
  4. The token must contain a sub (subject) claim to be considered valid
  5. On success, the decoded claims are available as ctx.userClaims and ctx.jwtClaims

If JWKS is not configured (SUPABASE_JWKS is missing or malformed), user mode is unavailable and will always reject requests.

No silent downgrade. When user is combined with other modes (e.g. auth: ['user', 'publishable']), a JWT that is present but fails verification rejects the request with InvalidCredentialsError — it does not fall through to the next mode. This prevents a bad token paired with a valid apikey (or with 'none') from being silently downgraded to a less-privileged auth mode. Requests that simply omit the Authorization header still fall through as expected.

CORS handling

withSupabase handles CORS automatically:

  • Preflight requests (OPTIONS) return 204 with CORS headers and skip the handler entirely — no auth check runs
  • All other requests get CORS headers appended to the response
  • Error responses (auth failures) also include CORS headers, so the browser can read the error

CORS defaults come from @supabase/supabase-js/cors. You can pass custom headers or disable CORS entirely with cors: false.

The Hono adapter does not handle CORS — use Hono's built-in cors middleware instead.

Credential extraction

Credentials are extracted from two standard headers:

  • Authorization: Bearer <token> → used by user mode
  • apikey: <value> → used by publishable and secret modes

Extraction is a separate step from verification (extractCredentials vs verifyCredentials). This separation means you can inspect raw credentials in custom flows without triggering validation.