Skip to content

API Keys & Scopes

Every project is provisioned with two data-plane API keys. This page explains what they can do, where each belongs, and the rotation/leak playbook.

The two keys

KeyScopeBypasses RLS?Belongs in
anonanon (unauthenticated)Noclient code (browsers, mobile, public CLIs)
service_roleservice_role (admin)Yesserver-side code only

A logged-in end user presents a JWT instead, carrying the authenticated scope. The split is identical to Supabase’s, so @supabase/supabase-js (URL + anon key) works drop-in.

Key format: anvilbase_<scope>_<slug>_<random>. Only the first 8 chars (the prefix) are stored for lookup; the prefix is safe to log.

The anon key

Designed to ship inside client bundles. By itself it can only read rows your RLS policies permit — and a fresh project has RLS on every table with no policies, so an anon key can’t do anything until you write a policy. Treat the anon key as public. Rotating it is routine hygiene (~90 days), not an incident.

The service_role key

Bypasses RLS — equivalent to a Postgres superuser connection. Treat it exactly like a database password:

  • Never commit it.
  • Never put it in a browser bundle, mobile app, or user-facing edge worker.
  • Rotate immediately if it leaks.

Every reveal of the service_role key (console/CLI) is recorded in the audit log as secret.reveal.service_role.

Reading keys

Terminal window
# anon plaintext + service_role masked (safe to screen-share)
anvilbase projects keys <project_id>
# reveal service_role plaintext (audit-logged)
anvilbase projects keys <project_id> --show-service-role

In the console: project → API tab. The anon key is always visible; the service_role key is masked until you click Reveal (which audit-logs the access). The tab also shows key history (rotations append; deactivated rows remain visible for audit).

Which credential for which job

CallerUse
Browser / mobile appanon key + (after login) the user’s JWT
Trusted backend service / cron workerservice_role key
Edge functionthe injected service_role key (server-side)
Operator / console / CLI (management)admin token or PAT — not a data-plane key

See Concepts → The two API planes.

Rotation

Rotation is a dedicated endpoint, wrapped by the CLI:

Terminal window
# Issues fresh anon + service_role keys; OLD KEYS STOP WORKING IMMEDIATELY
anvilbase secrets rotate api-keys --project <id>

What it does:

  1. Generates fresh anon + service_role keys.
  2. Marks all existing key rows inactive.
  3. Updates the project’s stored keys.

No grace window. Unlike some systems, API-key rotation invalidates the old keys at once. Coordinate with every consumer before rotating production keys.

Expiry

API keys carry an optional expires_at. A key is accepted only while it is active and unexpired — the auth layer enforces expires_at IS NULL OR expires_at > now() on every request. A key with no expires_at never expires; once a key’s expires_at is in the past it stops authenticating (401), independently of rotation. Set an expiry on short-lived or scoped keys so a stray copy can’t be used forever.

Terminal window
# Generates a new project JWT secret; ALL existing user JWTs become invalid
anvilbase secrets rotate jwt --project <id>

Rotate the JWT secret if tokens may be compromised — every signed-in user must re-authenticate. See Auth → Rotating the JWT secret.

Incident response — “service_role leaked”

  1. Rotate immediately (anvilbase secrets rotate api-keys). Old keys die within seconds.
  2. Audit who revealed it: filter the audit log for secret.reveal.service_role.
  3. Review database access: inspect pg_stat_activity for unfamiliar sessions; consider enabling pg_audit.
  4. Scan your code paths for anywhere the key could have been logged (crash reporters, Sentry tags, config dumps).
  5. If JWTs may also be affected, rotate the JWT secret too.

Why keys aren’t hashed-only

The key-lookup table stores a hash + prefix, but a recoverable copy is also kept on the project row so operators can recover a lost key without rotating (the most common support need). That copy — along with the project’s jwt_secret — is encrypted at rest with AES-256-GCM (enc:v1:, keyed on CONTROL_PLANE_SECRET), so a raw read of the platform DB no longer yields usable service_role keys or JWT secrets; the control plane decrypts them in memory when it loads a project. Even so, the load-bearing control remains protecting the platform database (host-level encryption at rest, restricted network access, and guarding CONTROL_PLANE_SECRET) — see Encryption and the Production Checklist. That DB is the crown jewel either way.

FAQ

Multiple anon keys per project? Not today — rotation replaces rather than adds.

Scope a key to specific tables? Not at the key layer — use RLS policies and roles. The anon/service_role split is deliberately coarse.

Next: Multi-Tenancy & Isolation.