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
| Key | Scope | Bypasses RLS? | Belongs in |
|---|---|---|---|
anon | anon (unauthenticated) | No | client code (browsers, mobile, public CLIs) |
service_role | service_role (admin) | Yes | server-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
# 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-roleIn 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
| Caller | Use |
|---|---|
| Browser / mobile app | anon key + (after login) the user’s JWT |
| Trusted backend service / cron worker | service_role key |
| Edge function | the 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:
# Issues fresh anon + service_role keys; OLD KEYS STOP WORKING IMMEDIATELYanvilbase secrets rotate api-keys --project <id>What it does:
- Generates fresh
anon+service_rolekeys. - Marks all existing key rows inactive.
- 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.
JWT secret rotation (related but separate)
# Generates a new project JWT secret; ALL existing user JWTs become invalidanvilbase 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”
- Rotate immediately (
anvilbase secrets rotate api-keys). Old keys die within seconds. - Audit who revealed it: filter the audit log for
secret.reveal.service_role. - Review database access: inspect
pg_stat_activityfor unfamiliar sessions; consider enablingpg_audit. - Scan your code paths for anywhere the key could have been logged (crash reporters, Sentry tags, config dumps).
- 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.