Files
supabase/apps/docs/content/guides/self-hosting/self-hosted-auth-keys.mdx
T
Miranda Limonczenko 7ce4ee53ae chore(docs) Retire supa-mdx-lint (#50602)
Closes
[DOCS-1289](https://linear.app/supabase/issue/DOCS-1289/get-the-linter-to-fix-what-it-flags-or-retirereplace-the-linter)

Stacked on #50600, which points contributors at the authoring skills.
Merge that one first.

## Problem

Contributors experienced friction with the linter. They felt nickle and
dimed for tiny nits and felt detracted from the work itself. PRs would
become noisy with tiny one-word suggestions.

Additionally, our homegrown linter is not very intelligent, causing
frequent overrides.

## Solution

This removes the linter entirely in favor of directing contributors to
use SKILLS instead.

The removal entails...

- **CI.** Delete the three `docs_lint` workflows: the PR check, the
external-PR comment companion, and the nightly `--fix` bot. Drop the
stale `zizmor.yml` ignore entry for the deleted workflow.
- **Tooling.** Delete `supa-mdx-lint.config.toml` and the 14 rule files.
Drop the `lint:mdx` script and the `@supabase/supa-mdx-lint` dependency
from docs, learn, and ui-library, and regenerate the lockfile.
- **Content.** Remove the 181 directives. A separate commit carries
Prettier's reformatting of the tables and blank lines those comments had
suppressed, so the deletion commit stays readable. No prose changes.
- **Style guide.** The word list states each rule directly instead of
describing what the linter flagged. Every term survives, including the
phrase groups that mirrored `Rule004ExcludeWords`.
- **Skills.** `write-the-docs`, `edit-the-docs`, and `review-the-docs`
drop `pnpm lint:mdx` from their self-review commands and check the word
list directly. `ask-the-docs`'s CI reference drops both workflows.

## Manual testing

1. Run `git grep -i supa-mdx-lint -- . ':!pnpm-lock.yaml'`. No matches.
2. Run `pnpm install --frozen-lockfile --lockfile-only`. It passes, so
the lockfile matches the three trimmed manifests.
3. Run `git diff master...HEAD --name-only --diff-filter=ACMR | grep -E
'\.(md|mdx)$' | xargs npx prettier --config prettier.config.mjs
--check`. All changed markdown passes.
4. Open the [reformatted filter
table](https://docs-git-docs-retire-mdx-linter-supabase.vercel.app/docs/guides/observability/logs#filter-events)
on the preview and compare it with
[production](https://supabase.com/docs/guides/observability/logs#filter-events).
The table renders the same.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Documentation guidance now uses manual prose and terminology review
with the shared word list.
* Clarified storage configuration and common Realtime channel mistakes.
* Improved table formatting, text wrapping, and selected reference
links.
  * Updated documentation authoring and review guidance.

* **Chores**
* Retired automated MDX linting from workflows and local validation
commands.
* Removed lint-suppression markers throughout documentation without
changing instructions.
  * Added targeted documentation review guidance for pull requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-22 10:00:41 -07:00

216 lines
11 KiB
Plaintext

---
title: 'New API Keys and Asymmetric Authentication'
description: 'Configure new API keys and ES256 asymmetric authentication for self-hosted Supabase.'
subtitle: 'Configure new API keys and ES256 asymmetric authentication for self-hosted Supabase.'
---
You can configure self-hosted Supabase to use the [publishable and secret API keys](/docs/guides/getting-started/api-keys) alongside the legacy API keys (`ANON_KEY` and `SERVICE_ROLE_KEY` HS256-signed JWTs).
## Before you begin
- Complete the [Docker setup guide](/docs/guides/self-hosting/docker) so that `JWT_SECRET`, `ANON_KEY`, and `SERVICE_ROLE_KEY` are set in your `.env` file. [Quick start (Linux)](/docs/guides/self-hosting/docker#quick-start-linux) handles this automatically; the manual path runs [`generate-keys.sh`](/docs/guides/self-hosting/docker#generate-keys-and-secrets).
- If you are upgrading from a legacy self-hosted Supabase environment, make sure to check the [changelog](https://github.com/supabase/supabase/blob/master/docker/CHANGELOG.md#2026-03-16) and [add/update](/docs/guides/self-hosting/updating) the following files:
- `.env.example` (merge new sections into your `.env` file)
- `docker-compose.yml`
- `utils/add-new-auth-keys.sh`
- `utils/rotate-new-api-keys.sh`
- `volumes/api/envoy/*`
## Adding the new keys
From your project directory where you have `docker-compose.yml`:
```sh
sh utils/add-new-auth-keys.sh --update-env
```
This generates new configuration environment variables and writes them to `.env`. Omit `--update-env` to review and confirm the changes interactively.
<Admonition type="caution">
The script reads `JWT_SECRET` from `.env` and includes it as a symmetric key inside both `JWT_KEYS` and `JWT_JWKS`. If you later change `JWT_SECRET`, you must regenerate the JWKS as well.
</Admonition>
In addition, the following configuration is uncommented automatically by the script for the new authentication to work correctly:
```yaml name=docker-compose.yml
auth:
environment:
# JSON array of signing JWKs (EC private + legacy symmetric)
GOTRUE_JWT_KEYS: ${JWT_KEYS:-[]}
rest:
environment:
# PostgREST accepts a plain-text symmetric secret, a single JWK, or a JWKS.
PGRST_JWT_SECRET: ${JWT_JWKS:-${JWT_SECRET}}
realtime:
environment:
# JWKS for token verification (EC public + legacy symmetric)
API_JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
storage:
environment:
# JWKS for token verification (EC public + legacy symmetric)
JWT_JWKS: ${JWT_JWKS:-{"keys":[]}}
functions:
environment:
# JWKS for token verification (EC public + legacy symmetric).
SUPABASE_JWKS: ${JWT_JWKS:-{"keys":[]}}
```
<Admonition type="caution">
Nested variable interpolation (`${A:-${B}}`) requires `podman-compose >= 1.6.0`. Earlier versions (still shipped by some Linux distributions) do not support it - if you are on an older `podman-compose`, either upgrade or replace each nested expression with the required variable directly, see the inline comments in `docker-compose.yml` for the exact substitutions.
</Admonition>
Restart all services:
```sh
sh run.sh recreate
```
### New API keys format
These keys use the same format as [API keys](/docs/guides/getting-started/api-keys) on the Supabase platform:
```
sb_publishable_<22-char-random>_<8-char-checksum>
sb_secret_<22-char-random>_<8-char-checksum>
```
### Verifying the setup
Test with the new secret key:
```sh
curl http://<your-domain>/rest/v1/ \
-H "apikey: your-supabase-secret-key"
```
You should receive a valid response from PostgREST. Then verify that the legacy service role key still works:
```sh
curl http://<your-domain>/rest/v1/ \
-H "apikey: your-service-role-key"
```
Both should work and return the same result.
<Admonition type="note">
`/rest/v1/` is the Open API root and requires admin-level keys (`sb_secret_*` or legacy `SERVICE_ROLE_KEY`). Public keys (`sb_publishable_*` or legacy `ANON_KEY`) return `403` for this endpoint.
</Admonition>
You can also verify the public JWKS endpoint:
```sh
curl http://<your-domain>/auth/v1/.well-known/jwks.json
```
This should return the EC public key (the symmetric key is excluded). Third-party services can use this endpoint to obtain the public key and verify asymmetric user session JWTs without needing the private key.
### Environment variables configuration
New variables default to empty values in `.env.example`. When empty, the API gateway and all services operate in legacy-only mode: `sb_publishable` and `sb_secret` API keys are not configured.
| Environment variable (existing and new) | Type | Description |
| --------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `JWT_SECRET` | Symmetric secret | **Existing:** Shared secret for signing and verifying HS256 JWTs. Used by multiple services. |
| `ANON_KEY` | HS256 JWT | **Existing:** Legacy client-side API key. Embedded JWT with `role: "anon"`. |
| `SERVICE_ROLE_KEY` | HS256 JWT | **Existing:** Legacy server-side API key. Embedded JWT with `role: "service_role"`. |
| `SUPABASE_PUBLISHABLE_KEY` | Opaque | **New:** Short random key with checksum. Replaces `ANON_KEY` for **client-side** use. |
| `SUPABASE_SECRET_KEY` | Opaque | **New:** Short random key with checksum. Replaces `SERVICE_ROLE_KEY` for **server-side** use. |
| `JWT_KEYS` | JSON array | **New:** JSON array of signing JWKs containing the new asymmetric key pair and the legacy symmetric key. Used by Auth to sign tokens. |
| `JWT_JWKS` | JWKS (JSON) | **New:** Contains the new public key and the legacy symmetric key. Used by PostgREST, Realtime, Storage, and Functions. |
### Differences from the Supabase platform
- **One key per role.** Self-hosted Supabase supports a single `sb_publishable` and a single `sb_secret`. The platform allows creating multiple `sb_` keys per project.
- **No checksum validation.** The opaque keys use the same format as the platform (`sb_publishable_<random>_<checksum>`), but the API gateway does not validate the checksum. Keys are matched as opaque strings by the API gateway.
### Backward compatibility
The new authentication configuration is fully backward compatible:
- **The API Gateway accepts both key types simultaneously.** You can migrate clients incrementally - some using legacy API keys, others using the new ones.
- **JWKS includes the symmetric key.** `JWT_JWKS` contains both the EC public key (for verifying new ES256 tokens) and the legacy `JWT_SECRET` as a symmetric JWK (for verifying old HS256 tokens). Services that receive `JWT_JWKS` can verify both token types.
- **No database changes required.** The asymmetric key system operates entirely at the API gateway and service configuration layer.
<Admonition type="caution">
When `JWT_KEYS` is set, Auth will start signing new user session JWTs with the new asymmetric ES256 key pair. Make sure all services that verify tokens (PostgREST, Realtime, Storage) are configured with `JWT_JWKS` so they can verify both the new ES256 and legacy HS256 tokens.
</Admonition>
## Rotating the new API keys
If your new API keys are compromised or you want to rotate them periodically, you can regenerate `sb_publishable` and `sb_secret` without touching the asymmetric key pair:
```sh
sh utils/rotate-new-api-keys.sh --update-env
```
After rotating, restart services and update your client applications with the new keys:
```sh
sh run.sh recreate
```
<Admonition type="note">
Rotating new API keys does not invalidate existing user sessions. User session JWTs issued by Auth are unaffected because they are verified using the asymmetric key pair, which remains unchanged.
</Admonition>
## Regenerating asymmetric key pair
If the EC private key is compromised or you need to regenerate everything:
```sh
sh utils/add-new-auth-keys.sh --update-env
```
This generates a new EC P-256 key pair, new JWKS, new asymmetric JWTs, and new `sb_` API keys. After updating `.env` and restarting services:
- New user session tokens will be signed with the new EC key.
- Existing user session tokens signed with the old EC key will fail verification. Users will need to sign in again.
- Existing user session tokens signed with the legacy symmetric key (`JWT_SECRET`) will continue to work, since `JWT_SECRET` hasn't changed and is still included in the new JWKS.
<Admonition type="danger">
Regenerating asymmetric keys invalidates all ES256 user sessions. Plan a maintenance window if your users have active sessions.
</Admonition>
## How it works
Requests via `supabase-js` include two headers for every service except Edge Functions:
- `apikey` - the API key (`sb_` or legacy JWT)
- `Authorization` - when unauthenticated, the client SDK copies the API key here (`Bearer sb_publishable_xxx` or `Bearer eyJ...`). When authenticated, this contains the user session JWT minted by Auth.
For **Realtime WebSocket** connections, the API key is sent as a `?apikey=` query parameter in the upgrade URL instead of an `apikey` header.
**Storage** and **Edge Functions** also accept requests without an API key. These services handle their own authentication.
For details on how the API gateway routes and authenticates requests, see the [Envoy API Gateway guide](/docs/guides/self-hosting/self-hosted-envoy#authentication).
## Additional resources
- [API keys](/docs/guides/getting-started/api-keys) - How API keys work on the Supabase platform
- [Auth architecture](/docs/guides/auth/architecture) - How the Auth service handles authentication and token signing
- [JWT Signing Keys](/docs/guides/auth/signing-keys) - Best practices on managing keys used by Supabase Auth to create and verify JSON Web Tokens
- [JSON Web Token (JWT)](/docs/guides/auth/jwts) - How to best use JSON Web Tokens with Supabase
- [Self-hosting with Docker](/docs/guides/self-hosting/docker) - Initial setup guide, including legacy key generation
On GitHub:
- [Upcoming changes to Supabase API Keys (Discussion #29260)](https://github.com/orgs/supabase/discussions/29260)
- [Supabase Auth: Asymmetric Keys support (Discussion #29289)](https://github.com/orgs/supabase/discussions/29289)
- [Self-hosted Supabase: Envoy becomes the default API gateway (Discussion #48048)](https://github.com/orgs/supabase/discussions/48048)