Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,16 @@ await fetch(refreshEndpoint, {

## Auth Modes

| Mode | Credential | Use case |
| ------------------ | --------------------- | --------------------------------------------------- |
| `"user"` (default) | Valid JWT | Authenticated user endpoints |
| `"publishable"` | Valid publishable key | Client-facing, key-validated endpoints |
| `"secret"` | Valid secret key | Server-to-server, internal calls |
| `"none"` | None | Open endpoints, wrappers that handle their own auth |
| Mode | Credential | Use case |
| ------------------ | ------------------------------- | --------------------------------------------------- |
| `"user"` (default) | Valid JWT | Authenticated user endpoints |
| `"publishable"` | Valid `default` publishable key | Client-facing, key-validated endpoints |
| `"secret"` | Valid `default` secret key | Server-to-server, internal calls |
| `"none"` | None | Open endpoints, wrappers that handle their own auth |

Array syntax (`auth: ["user", "secret"]`) accepts multiple auth methods — first match wins. An absent credential falls through to the next mode; a present-but-invalid JWT rejects the request (no silent downgrade). See [`docs/auth-modes.md`](docs/auth-modes.md).
Array syntax (`auth: ["user", "secret"]`) accepts multiple auth methods — first match wins. An absent credential falls through to the next mode; a present-but-invalid JWT rejects the request (no silent downgrade).

Named key validation: `auth: "publishable:web_app"` or `auth: "secret:automations"` validates against a specific named key in `SUPABASE_PUBLISHABLE_KEYS` or `SUPABASE_SECRET_KEYS`.
Named key validation: `auth: "publishable:web_app"` or `auth: "secret:automations"` validates against a specific named key in `SUPABASE_PUBLISHABLE_KEYS` or `SUPABASE_SECRET_KEYS`. Bare `auth: "secret"` (or `"publishable"`) matches only the `default` key; use the wildcard `auth: "secret:*"` to accept any key in the set. See [`docs/auth-modes.md`](docs/auth-modes.md).

> **Supabase Edge Functions:** By default, the platform requires a valid JWT on every request. If your function uses `auth: 'publishable'`, `auth: 'secret'`, or `auth: 'none'`, disable the platform-level JWT check in `supabase/config.toml`:
>
Expand Down
2 changes: 1 addition & 1 deletion docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ type AuthMode = 'none' | 'publishable' | 'secret' | 'user'
type AuthModeWithKey = AuthMode | `publishable:${string}` | `secret:${string}`
```

Extended auth mode with named key support. Examples: `'publishable:web'`, `'secret:*'`, `'secret:internal'`.
Extended auth mode with named key support. Examples: `'publishable:web'`, `'secret:*'`, `'secret:internal'`. The bare form (`'publishable'` / `'secret'`) matches only the `default` key; `:*` accepts any key in the set.

### Allow / AllowWithKey (deprecated aliases)

Expand Down
18 changes: 11 additions & 7 deletions docs/auth-modes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ Every request is validated against one or more auth modes before your handler ru

> **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 publishable key in `apikey` header | Client-facing, key-validated endpoints |
| `'secret'` | Valid secret key in `apikey` header | Server-to-server, internal calls |
| `'none'` | None | Open endpoints, custom auth wrappers |
| 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](#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`:
Expand Down Expand Up @@ -79,7 +81,7 @@ The caller must send:
apikey: sb_publishable_abc123...
```

By default, `publishable` mode validates against the `"default"` key in `SUPABASE_PUBLISHABLE_KEYS`. Use named key syntax to target a specific key (see below).
By default, `publishable` mode validates against the `"default"` key in `SUPABASE_PUBLISHABLE_KEYS`. Use named key syntax to target a specific key or `publishable:*` to accept any key (see below).

## Secret mode

Expand All @@ -103,6 +105,8 @@ The caller must send:
apikey: sb_secret_xyz789...
```

By default, `secret` mode validates against the `"default"` key in `SUPABASE_SECRET_KEYS`. Use named key syntax to target a specific key or `secret:*` to accept any key (see below).

## None mode

No credentials required. Every request is accepted.
Expand Down
4 changes: 4 additions & 0 deletions docs/core-primitives.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ The client is configured with:
- The user's JWT as the `Authorization: Bearer` header (if token is provided)
- Server-safe auth settings: `persistSession: false`, `autoRefreshToken: false`, `detectSessionInUrl: false`

**Which key is used.** With `auth.keyName` set, that named key from `SUPABASE_PUBLISHABLE_KEYS` is used (and it throws if the key doesn't exist). With `keyName` omitted, the `default` key is used, falling back to the first key in the set when no `default` exists. Note this differs from the `'publishable'` auth mode, which matches the `default` key only and never falls back.

This function throws `EnvError` if `SUPABASE_URL` or the required publishable key is missing. Wrap in try/catch when using directly.

## createAdminClient
Expand All @@ -170,6 +172,8 @@ const supabaseAdminInternal = createAdminClient({
})
```

**Which key is used.** With `auth.keyName` set, that named key from `SUPABASE_SECRET_KEYS` is used (and it throws if the key doesn't exist). With `keyName` omitted, the `default` key is used, falling back to the first key in the set when no `default` exists. Note this differs from the `'secret'` auth mode, which matches the `default` key only and never falls back.

Same server-safe settings as `createContextClient`. Throws `EnvError` if the secret key is missing.

## Full example: custom multi-route handler
Expand Down
23 changes: 14 additions & 9 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,29 +21,34 @@ See `src/core/utils/timing-safe-equal.ts` for the implementation.

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 |
| 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` | `default` publishable key (timing-safe) | A known client app | Row-Level Security | Full access |
| `secret` | `default` secret key (timing-safe) | A trusted server/service | Full access | Full access |
| `none` | Nothing — all requests are accepted | Unknown | Row-Level Security | Full access |

For `publishable` and `secret`, the bare mode matches only the `default` key; use `publishable:<name>` / `secret:<name>` for a specific key or `publishable:*` / `secret:*` to accept any key in the set.

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.
- **`publishable` and `secret` modes** compare the `apikey` header against known keys. The comparison is timing-safe. Bare `auth: 'publishable'` / `auth: 'secret'` match only the `default` key; use named keys (`auth: 'secret:automations'`) to accept only that specific key — this follows the principle of least privilege — or the wildcard (`auth: 'secret:*'`) to accept any key in the set.
- **`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:
Bare `auth: 'secret'` matches only the `default` key. You can restrict an endpoint to a specific named key, or accept any key in the set with the wildcard:

```ts
// Accepts any secret key
// Matches only the "default" secret key
withSupabase({ auth: 'secret' }, handler)

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

// Accepts any secret key in the set
withSupabase({ auth: 'secret:*' }, 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.
Expand Down
2 changes: 1 addition & 1 deletion skills/supabase-server/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ await fetch('https://<project>.supabase.co/functions/v1/my-function', {
})
```

Use `auth: 'secret'` to accept any secret key, or `auth: 'secret:name'` to require a specific named key.
Bare `auth: 'secret'` matches only the `default` key. Use `auth: 'secret:name'` to require a specific named key, or `auth: 'secret:*'` to accept any secret key in the set.

## When to use `auth: 'none'`

Expand Down
15 changes: 15 additions & 0 deletions src/core/create-admin-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,29 @@ import { resolveEnv } from './resolve-env.js'
* Uses a secret key for authentication, giving full access to all data.
* Stateless — one client per request.
*
* ## Which key is used
*
* With `auth.keyName` set, that named key from `SUPABASE_SECRET_KEYS` is used —
* and it throws if the key doesn't exist. With `keyName` omitted, the `default`
* key is used, falling back to the first key in the set when no `default` exists.
*
* Note this differs from the `"secret"` auth mode, which matches the `default`
* key only and never falls back — see {@link index.AuthModeWithKey}.
*
* @throws {@link index.EnvError} If `SUPABASE_URL` is missing or the specified secret key is not found.
*
* @example Basic usage
* ```ts
* // Uses the `default` secret key (or the first key if no `default` exists)
* const supabaseAdmin = createAdminClient()
* const { data } = await supabaseAdmin.from('audit_log').insert({ action: 'user_login' })
* ```
*
* @example Specific named key
* ```ts
* const supabaseAdmin = createAdminClient({ auth: { keyName: 'internal' } })
* ```
*
* @category Primitives
*/
export function createAdminClient<Database = unknown>(
Expand Down
10 changes: 10 additions & 0 deletions src/core/create-context-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ import { resolveEnv } from './resolve-env.js'
* Configured with a publishable key and (optionally) the caller's JWT,
* so Row-Level Security policies apply. Stateless — one client per request.
*
* ## Which key is used
*
* With `auth.keyName` set, that named key from `SUPABASE_PUBLISHABLE_KEYS` is
* used — and it throws if the key doesn't exist. With `keyName` omitted, the
* `default` key is used, falling back to the first key in the set when no
* `default` exists.
*
* Note this differs from the `"publishable"` auth mode, which matches the
* `default` key only and never falls back — see {@link index.AuthModeWithKey}.
*
* @throws {@link index.EnvError} If `SUPABASE_URL` is missing or the specified publishable key is not found.
*
* @example With verified auth
Expand Down
7 changes: 5 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@
* | Mode | Credential | Use case |
* |------|-----------|----------|
* | `"user"` | Valid JWT | Authenticated user endpoints |
* | `"publishable"` | Publishable key | Client-facing, key-validated endpoints |
* | `"secret"` | Secret key | Server-to-server, internal calls |
* | `"publishable"` | `default` publishable key | Client-facing, key-validated endpoints |
* | `"secret"` | `default` secret key | Server-to-server, internal calls |
* | `"none"` | None | Open endpoints |
*
* Bare `"publishable"` / `"secret"` match only the `default` key; use
* `"secret:<name>"` for a specific key or `"secret:*"` to accept any key.
*
* Array syntax tries modes in order — first match wins:
* ```ts
* withSupabase({ auth: ['user', 'secret'] }, handler)
Expand Down
11 changes: 8 additions & 3 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@ export type { JSONWebKeySet }
* Authentication mode that determines what credentials a request must provide.
*
* - `"none"` — No credentials required. Every request is accepted.
* - `"publishable"` — Requires a valid publishable key in the `apikey` header.
* - `"secret"` — Requires a valid secret key in the `apikey` header (timing-safe comparison).
* - `"publishable"` — Requires a valid publishable key in the `apikey` header. Matches only the `default` key.
* - `"secret"` — Requires a valid secret key in the `apikey` header (timing-safe comparison). Matches only the `default` key.
* - `"user"` — Requires a valid JWT in the `Authorization: Bearer <token>` header.
*
* Bare `"publishable"` / `"secret"` resolve the `default` key from
* `SUPABASE_PUBLISHABLE_KEYS` / `SUPABASE_SECRET_KEYS`. To target another key or
* accept any key, see {@link AuthModeWithKey}.
*
* @example Single mode
* ```ts
* // Single mode
Expand Down Expand Up @@ -42,7 +46,8 @@ export type Allow = AuthMode
*
* Use the colon syntax (`"publishable:web_app"`) to require a specific named key
* from the `SUPABASE_PUBLISHABLE_KEYS` or `SUPABASE_SECRET_KEYS` JSON object.
* Use `"publishable:*"` or `"secret:*"` to accept any key in the set.
* Use `"publishable:*"` or `"secret:*"` to accept any key in the set. The bare
* form without a colon (`"publishable"` / `"secret"`) matches only the `default` key.
*
* @example Named key
* ```ts
Expand Down
Loading