# New API Keys and Asymmetric Authentication

Configure new API keys and ES256 asymmetric authentication for self-hosted Supabase.

You can configure self-hosted Supabase to use the [new 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

- Ensure OpenSSL and Node.js 16+ are available on the machine where you will generate new keys
- Complete the [Docker setup guide](/docs/guides/self-hosting/docker), including running `generate-keys.sh` so that `JWT_SECRET`, `ANON_KEY`, and `SERVICE_ROLE_KEY` are set in your `.env` file
- If you are upgrading an existing self-hosted Supabase environment, make sure to check the [changelog](https://github.com/supabase/supabase/blob/master/docker/CHANGELOG.md) and add/update 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/kong-entrypoint.sh`
- `volumes/api/kong.yml`

## 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`. Without `--update-env`, the script prints the values and prompts you interactively.

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.

After updating `.env`, enable new authentication by uncommenting these lines in `docker-compose.yml`:

```yaml name=docker-compose.yml
auth:
  environment:
    # JSON array of signing JWKs (EC private + legacy symmetric)
    GOTRUE_JWT_KEYS: ${JWT_KEYS:-[]}

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":[]}}
```

PostgREST does not need uncommenting - it already uses `PGRST_JWT_SECRET: ${JWT_JWKS:-${JWT_SECRET}}` which automatically picks up `JWT_JWKS` when set.

Podman does not support nested variable interpolation (`${A:-${B}}`). If you are using Podman, replace each nested expression with the required variable directly - see the inline comments in `docker-compose.yml` for the exact substitutions.

Restart all services:

```sh
docker compose down && docker compose up -d
```

### New API keys format

The new API keys use the [same format](/docs/guides/getting-started/api-keys) as 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 publishable key:

```sh
curl http://<your-domain>/rest/v1/ \
-H "apikey: your-supabase-publishable-key"
```

You should receive a valid response from PostgREST. Then verify that the legacy key still works:

```sh
curl http://<your-domain>/rest/v1/ \
-H "apikey: your-anon-key"
```

Both should work and return the same result.

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, and Storage to verify tokens.         |

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

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.

## 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
docker compose down && docker compose up -d
```

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.

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

Regenerating asymmetric keys invalidates all ES256 user sessions. Plan a maintenance window if your users have active sessions.

## How it works

Below are a few notes on the details of the new authentication architecture.

### What client SDK sends

Every request via `supabase-js` includes two headers:

- `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** accept requests without an API key. These services handle their own authentication.

### Kong API gateway routing

Kong is configured with two consumers that each accept both the legacy and new API keys:

```yaml name=volumes/api/kong.yml
consumers:
  - username: anon
    keyauth_credentials:
      - key: $SUPABASE_ANON_KEY # legacy HS256 JWT (ANON_KEY)
      - key: $SUPABASE_PUBLISHABLE_KEY # new opaque key (omitted when not configured)
  - username: service_role
    keyauth_credentials:
      - key: $SUPABASE_SERVICE_KEY # legacy HS256 JWT (SERVICE_ROLE_KEY)
      - key: $SUPABASE_SECRET_KEY # new opaque key (omitted when not configured)
```

When new API keys have not been added yet, the `kong-entrypoint.sh` script removes the empty credential entries before Kong loads the config.

To assist with the authorization flows a specialized configuration in `kong.yml` substitutes internal, gateway-level-only pre-signed JWTs for `sb_publishable` and `sb_secret` API keys. These pre-signed JWTs are also auto-configured in `.env` but **should not** be used in any application code.

| Route                | Service              | API key required | Header substitution |
| -------------------- | -------------------- | ---------------- | ------------------- |
| `/auth/v1/*`         | Auth                 | Yes              | `Authorization`     |
| `/rest/v1/*`         | PostgREST            | Yes              | `Authorization`     |
| `/graphql/v1`        | PostgREST            | Yes              | `Authorization`     |
| `/realtime/v1/api/*` | Realtime (REST)      | Yes              | `Authorization`     |
| `/realtime/v1/*`     | Realtime (WebSocket) | Yes              | `x-api-key`         |
| `/storage/v1/*`      | Storage              | No               | `Authorization`     |
| `/functions/v1/*`    | Edge Functions       | No               | -                   |

### Request flows

The API gateway (Kong) configuration has the logic to decide what `Authorization` header the upstream service, such as Auth, receives. The logic handles two cases: requests that only carry an API key (no user session), and requests that carry a user session JWT.

#### Unauthenticated requests (API key only, no user session JWT)

When the client sends only an `apikey` header with the API key (no `Authorization` header), or also the API key duplicated in `Authorization` by `supabase-js`:

1. The client sends `apikey: sb_publishable_xxx` (or legacy `apikey: eyJ...`).
2. The API gateway checks the key and identifies the consumer (`anon` or `service_role`).
3. The API gateway inspects the `Authorization` header. Since it is either absent or starts with `Bearer sb_` (an opaque key, not a session JWT), the plugin replaces it:
- **The new `sb_` key:** `Authorization` header is set to the internal pre-signed ES256 JWT that corresponds to the role.
- **The Legacy JWT key:** `Authorization` header is set to the legacy HS256 JWT (the `apikey` value is copied as-is).
4. The upstream service receives a valid JWT in `Authorization` and verifies it using `JWT_JWKS` (or `JWT_SECRET`).

#### Authenticated requests (user session JWT)

When the client has previously signed in through Auth and has a valid user session JWT token:

1. The client sends `Authorization: Bearer eyJ...` (a JWT session token from Auth) alongside `apikey: sb_publishable_xxx` (or legacy `apikey: eyJ...`).
2. The API gateway checks the API key and identifies the consumer.
3. The API gateway inspects the `Authorization` header. Since it exists and does **not** start with `Bearer sb_` (it's a real JWT, not an `sb_` API key), the plugin **passes it through unchanged**. This works the same way regardless of whether the `apikey` is a new `sb_` key or a legacy JWT - the gateway only looks at the `Authorization` header to decide whether a user session is present.
4. The upstream service verifies the session JWT. If Auth signed it with ES256 (when `JWT_KEYS` is configured), verification uses the EC public key. If Auth signed it with HS256 (legacy), verification uses the symmetric key. Both keys are available in `JWT_JWKS`.

The `request-transformer` expression in `kong.yml` implements this as a single Lua conditional:

```lua
-- Pseudocode for the Authorization header logic:
if authorization exists AND does not start with "Bearer sb_" then
  -- User session JWT: pass through unchanged
  keep authorization
elseif apikey matches secret key then
  -- Replace with pre-signed service_role ES256 JWT
  set authorization = "Bearer <service_role ES256 JWT>"
elseif apikey matches publishable key then
  -- Replace with pre-signed anon ES256 JWT
  set authorization = "Bearer <anon ES256 JWT>"
else
  -- Legacy JWT key: copy apikey as authorization
  set authorization = apikey
end
```

## Additional resources

- [Understanding 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)