Environment API keys
Each environment has its own keys. Apps pass them to supabase-js (or any
Supabase client) when they call the environment's /auth/v1, /rest/v1,
/storage/v1, /realtime/v1 and /functions/v1 endpoints. They are different
from the access tokens and organization API keys that call
the /v1 control API.
| Key | Format | Who may hold it |
|---|---|---|
anon |
HS256 JWT, role anon |
Anyone. Safe in an app bundle; row-level security decides what it can read |
publishable_key |
sb_publishable_... |
Same as anon. The new-style opaque form |
service_role |
HS256 JWT, role service_role |
Servers only. Bypasses row-level security |
secret_key |
sb_secret_... |
Servers only. The opaque form of service_role |
All four derive from one JWT secret per environment. The JWTs have a fixed
issue time and a ten-year expiry, so they stay the same until the secret is
rotated, like Supabase project keys. The gateway (or, on the single box, the
project router) swaps an opaque key for its JWT on the way in, so current
supabase-js versions work with either style. An opaque key the environment
did not issue gets 401.
Where to find them
- Console: the environment's API keys page.
- CLI:
scribase apps switchprints the URL and theanonor publishable key for an app. It never prints the server keys. - API: the route below.
GET .../environments/{env}/api-keys
GET /v1/organizations/{org}/projects/{project}/environments/{env}/api-keys
{
"organization_id": "acme",
"project_id": "my-app",
"environment_id": "production",
"api_url": "https://<label>.<domain>",
"anon_key": "eyJ...",
"jwt_secret_version": 1,
"jwt_secret_source": "control_plane",
"publishable_key": "sb_publishable_...",
"service_role_key": "eyJ...",
"secret_key": "sb_secret_..."
}service_role_keyandsecret_keyarenullfor viewers.api_urlisnullwhen the control plane has no public gateway URL configured.?reveal=jwt_secretadds the rawjwt_secret. It mints any token for the environment, so it needs the owner or admin role and, on an account with an authenticator app, a fresh MFA code.
curl "$SCRIBASE_API_URL/v1/organizations/acme/projects/my-app/environments/production/api-keys" \
-H "Authorization: Bearer $SCRIBASE_ACCESS_TOKEN"POST .../environments/{env}/api-keys/rotate
POST /v1/organizations/{org}/projects/{project}/environments/{env}/api-keys/rotate
Replaces the environment's JWT secret. This invalidates the old anon,
service_role, publishable and secret keys and every session token the
environment issued, so every client needs the new keys and every user signs in
again. The response has the same shape as GET, with the new keys and a higher
jwt_secret_version. The control plane then publishes the new secret to the
environment's services and rolls them out.
- Needs the owner or admin role and, on an account with an authenticator app, a fresh MFA code.
- A personal access token cannot answer an MFA challenge, so on such accounts
revealing and rotating are console-only. CI can still read the
anonandservice_rolekeys, which are never MFA-gated, or use a service-account machine token. 409 jwt_secret_operator_managed: the deployment signs every environment with one operator-managed secret (SCRIBASE_TENANT_JWT_SECRET). Rotate it there instead.
Keys after an import
scribase import supabase never copies the source project's keys or JWT
secret. The environment keeps its own keys, so the two projects never trust
each other's tokens. Users keep their ids and passwords and sign in once more.