Back to Supabase

New API Keys and Asymmetric Authentication

apps/docs/content/guides/self-hosting/self-hosted-auth-keys.mdx

1.26.0414.2 KB
Original Source

You can configure self-hosted Supabase to use the new API keys alongside the legacy API keys (ANON_KEY and SERVICE_ROLE_KEY HS256-signed JWTs).

Before you begin

  • Complete the Docker setup guide, including running generate-keys.sh so that JWT_SECRET, ANON_KEY, and SERVICE_ROLE_KEY are set in your .env file.
  • Ensure openssl and node version 16 or newer are available on the machine where you will generate new keys.
  • If you are upgrading an existing self-hosted Supabase environment, make sure to check the changelog 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.

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

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

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

Then restart all services:

sh
docker compose down && docker compose up -d

New API keys format

The new API keys use the same format 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:

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:

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:

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)TypeDescription
JWT_SECRETSymmetric secretExisting: Shared secret for signing and verifying HS256 JWTs. Used by multiple services.
ANON_KEYHS256 JWTExisting: Legacy client-side API key. Embedded JWT with role: "anon".
SERVICE_ROLE_KEYHS256 JWTExisting: Legacy server-side API key. Embedded JWT with role: "service_role".
SUPABASE_PUBLISHABLE_KEYOpaqueNew: Short random key with checksum. Replaces ANON_KEY for client-side use.
SUPABASE_SECRET_KEYOpaqueNew: Short random key with checksum. Replaces SERVICE_ROLE_KEY for server-side use.
JWT_KEYSJSON arrayNew: JSON array of signing JWKs containing the new asymmetric key pair and the legacy symmetric key. Used by Auth to sign tokens.
JWT_JWKSJWKS (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:

  • All new variables are optional. If left with empty values, the API gateway (Kong) and all services behave exactly as before.
  • Kong 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.
  • Services fall back gracefully. PostgREST uses ${JWT_JWKS:-${JWT_SECRET}} - if JWT_JWKS is empty, it uses JWT_SECRET directly.
  • 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
docker compose down && docker compose up -d
<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

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.

API gateway routing

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

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

RouteServiceAPI key requiredHeader substitution
/auth/v1/*AuthYesAuthorization
/rest/v1/*PostgRESTYesAuthorization
/graphql/v1PostgRESTYesAuthorization
/realtime/v1/api/*Realtime (REST)YesAuthorization
/realtime/v1/*Realtime (WebSocket)Yesx-api-key
/storage/v1/*StorageNoAuthorization
/functions/v1/*Edge FunctionsNo-

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

On GitHub: