scribasedocs

Control API /v1

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 switch prints the URL and the anon or 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

JSON
{
  "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_key and secret_key are null for viewers.
  • api_url is null when the control plane has no public gateway URL configured.
  • ?reveal=jwt_secret adds the raw jwt_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.
Terminal
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 anon and service_role keys, 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.