The Management API (/api/v1/*) is the control surface: provisioning, configuration,
user/team administration, schema/DDL, secrets, backups, and platform stats. It is
not the data plane — application traffic lives under /v1/*
(Data-Plane API).
Base URL & auth
Every request (except POST /platform/login) needs Authorization: Bearer <token>. It fails closed — no valid credential ⇒ 401. Data-plane keys
(anon, service_role) are not valid here.
| Type | Format | Notes |
|---|
| Admin token | literal ANVILBASE_ADMIN_TOKEN | root, constant-time compared, always admin scope. anvilbase start generates one into .env for local stacks; a raw docker compose up deployment sets it (or leaves it unset to disable the bootstrap path and use PATs only) |
| PAT | anvilbase_pat_… | DB-backed; HMAC-verified via CONTROL_PLANE_SECRET; active + non-expired; carries its own scopes. Mint via POST /platform/tokens or anvilbase token mint |
| Session JWT | compact JWS | 12h, issued by POST /platform/login; role→scope mapped; user re-checked active per request |
Scopes are enforced per route: admin ⊇ deploy ⊇ read (unknown routes
require admin; the sensitive reads GET /projects/{id}/keys/service-role,
GET /admin/audit/export, and GET /admin/users require admin).
Under-scoped credentials get a 403 naming the required scope. Full model:
Management API → Scopes.
| Method | Path | Purpose |
|---|
| POST | /platform/login | email+password → 12h session JWT (unauthenticated; per-IP rate limited + 5-failure IP+email lockout) |
| POST | /platform/tokens | mint a PAT (`{name, scope |
| GET | /platform/tokens | list PAT metadata (never hashes/plaintext) — admin scope |
| DELETE | /platform/tokens/{id} | revoke — admin scope |
Conventions
- Bodies/responses are JSON unless noted. Errors use
{ "error": { "message": ..., "code": ... } } (Error Codes).
- Pagination:
limit (defaults vary; capped server-side — 100 for most lists,
200 for cache keys) + offset. Some lists wrap as { entries/users, total, limit, offset }.
- Destructive DDL requires a
?confirm=<name> echo matching the path segment.
- Mutations write a best-effort audit entry.
OpenAPI
Compile-time OpenAPI 3.x for the control plane (unauthenticated; partial coverage).
A per-project, schema-derived spec for your tables is at
GET /api/v1/projects/{id}/docs.
Health (unauthenticated)
| Method | Path | Notes |
|---|
| GET | /health | liveness (status, database); carries a top-level version string (the running control-plane CARGO_PKG_VERSION) |
| GET | /health/services | per-service fan-out (status, warnings, services); also carries a top-level version string |
Projects
| Method | Path | Purpose |
|---|
| POST | /projects | create (provisions DB + bucket + keys; returns jwt_secret + service_role_key once) |
| GET | /projects | list (paginated; ?status= filter) |
| GET | /projects/{id} | get one |
| PATCH | /projects/{id} | update name/description/settings/quotas |
| DELETE | /projects/{id} | soft-delete + deprovision (204) |
| POST | /projects/{id}/suspend | set status='suspended' — the data plane then rejects all traffic for the project (403) |
| POST | /projects/{id}/resume | set status='active' — restore data-plane access |
Suspend/resume are the only paths that set the suspended state; the data-plane
enforcement (in project-context middleware) already rejects any non-active
project.
Auth settings & OAuth providers
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/auth | get settings (secrets masked) |
| PATCH | /projects/{id}/auth | update email_enabled, magic_link_enabled, min_password_length, session_duration_seconds, refresh_token_duration_seconds |
| PUT | /projects/{id}/auth/providers | add/update a provider (github/google/apple/discord/microsoft) |
| DELETE | /projects/{id}/auth/providers/{provider} | remove a provider |
| GET | /auth/templates | provider templates + recommended scopes |
Project users
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/users | list (?search= email, paginated) |
| POST | /projects/{id}/users | create |
| GET / DELETE | /projects/{id}/users/{user_id} | get / delete |
| GET | /projects/{id}/users/{user_id}/sessions | list sessions |
| DELETE | /projects/{id}/users/{user_id}/sessions | revoke all sessions |
| DELETE | /projects/{id}/users/{user_id}/sessions/{token} | revoke one |
| POST | /projects/{id}/users/{user_id}/ban · /unban | ban / unban |
Team invites
| Method | Path | Purpose |
|---|
| POST | /projects/{id}/invites | create (returns invite_token once) |
| GET | /projects/{id}/invites | list pending |
| DELETE | /projects/{id}/invites/{invite_id} | revoke |
| POST | /invites/accept | accept ({token}) — token-authorized; binds to the session user (any authenticated platform user; user_id optional, must match the session user) |
Roles: owner, admin, editor (default), read_only.
Team members
Post-acceptance member administration (admin scope). A last-owner guard,
enforced atomically in the model, refuses to demote or remove the final owner
(409).
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/members | list members |
| PATCH | /projects/{id}/members/{user_id} | change a member’s role ({role}) — 404 if not a member, 409 last-owner |
| DELETE | /projects/{id}/members/{user_id} | remove a member — 404 if not a member, 409 last-owner |
Schema / DDL
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/schema/tables | list tables (columns/PK/FK/indexes/row estimate) |
| POST | /projects/{id}/schema/tables | create table (RLS on by default) |
| DELETE | /projects/{id}/schema/tables/{table}?confirm={table} | drop table |
| POST | /projects/{id}/schema/tables/{table}/columns | add column |
| DELETE | /projects/{id}/schema/tables/{table}/columns/{col}?confirm={col} | drop column |
| GET | /projects/{id}/schema/extensions | list extensions |
| GET | /projects/{id}/docs | per-project REST OpenAPI |
Database (migrations & reset)
| Method | Path | Purpose |
|---|
| POST | /projects/{id}/db/push | apply ordered migrations (idempotent) |
| GET | /projects/{id}/db/migrations | list applied |
| POST | /projects/{id}/db/reset | drop public tables + optionally re-apply |
Raw SQL is a data-plane RPC (rpc/exec_sql, service_role) — see
Data-Plane API.
Database branches
Neon-style branching (admin scope). A branch is a fully isolated clone
of the parent’s database with its own keys, JWT secret, DB, and bucket — a
distinct tenant. The parent is never touched.
| Method | Path | Purpose |
|---|
| POST | /projects/{id}/branches | create + clone + re-key a branch ({name, from?, schema_only?}; returns the branch’s own anon_key, service_role_key, jwt_secret once) |
| GET | /projects/{id}/branches | list branches (secret-free) |
| DELETE | /projects/{id}/branches/{slug} | drop the branch tenant (DB + bucket); the parent is never affected |
name becomes a slug (3–50 chars, lowercase alphanumeric + hyphens). from
defaults to "now" (clone the parent’s current state); PITR-sourced branches are
deferred. schema_only clones DDL with zero rows.
RLS
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/rls/status | per-table enabled/forced flags |
| POST | /projects/{id}/rls/tables/{table}/enable · /disable | toggle RLS |
| GET | /projects/{id}/rls/tables/{table}/policies | list policies |
| POST | /projects/{id}/rls/tables/{table}/policies | create policy |
| DELETE | /projects/{id}/rls/tables/{table}/policies/{name} | drop policy |
| POST | /projects/{id}/rls/test | simulate (always rolls back) |
Secrets vault
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/secrets | list (metadata only) |
| POST | /projects/{id}/secrets | create/update (upsert by name) — deploy scope |
| PUT | /projects/{id}/secrets/{name} | update an existing secret’s value (404 if absent) — admin scope |
| POST | /projects/{id}/secrets/{name}/rotate | rotate to a fresh random value (returned once) — admin scope |
| DELETE | /projects/{id}/secrets/{identifier} | delete by UUID or name — admin scope |
Values are never returned on list/get/update; rotate returns the new value
once.
Secret rotation
| Method | Path | Purpose |
|---|
| POST | /projects/{id}/rotate/jwt | new JWT secret (invalidates all JWTs) |
| POST | /projects/{id}/rotate/api-keys | new anon + service_role keys |
Connection pooler
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/pooler | reveal the project’s pooled + direct connection strings |
Returns { project_id, enabled, mode, pooled, direct, read_only? }. The pooled
string (transaction-mode Supavisor) embeds the per-project derived pooler
password, so the call is an audit-logged reveal (not part of the
secret-redacted project response). When the pooler is unconfigured
(SUPAVISOR_PUBLIC_HOST unset), enabled=false and pooled is null; direct
(session mode) is always present. When an opt-in read replica
is configured (ANVILBASE_READ_REPLICA_HOST set), the response also carries a
read_only connection string pointed at the replica for read-heavy queries
(writes must still use direct/pooled); the field is omitted entirely when no
replica is configured.
Edge functions
| Method | Path | Purpose |
|---|
| GET / POST | /projects/{id}/functions | list / create+deploy |
| GET / PUT / DELETE | /projects/{id}/functions/{name} | get / update / delete |
verify_jwt defaults to true; code ≤ 10 MB; quota breach ⇒ 429. Invocation is
data-plane.
Webhooks
| Method | Path | Purpose |
|---|
| POST / GET | /projects/{id}/webhooks | create (returns secret once) / list |
| PATCH / DELETE | /projects/{id}/webhooks/{wid} | update / delete |
| GET | /projects/{id}/webhooks/{wid}/deliveries | delivery attempts |
| POST | /projects/{id}/webhooks/{wid}/test | synchronous test (SSRF-guarded) |
| POST | /projects/{id}/webhooks/{wid}/attach | attach the row-change trigger to a table ({table, events, schema?}) — deploy scope |
| DELETE | /projects/{id}/webhooks/{wid}/attach/{table} | detach the trigger (?schema=) — deploy scope |
| GET | /projects/{id}/webhooks/{wid}/attachments | list attached tables + events |
Attach/detach wire the Supabase-style database (row-change) webhook trigger onto
a user table; see Guides → Webhooks
for the payload shape and security model.
Email (templates & SMTP)
| Method | Path | Purpose |
|---|
| GET / PUT / DELETE | /projects/{id}/email/templates[/{type}] | manage templates |
| GET | /projects/email/templates/defaults | built-in defaults |
| GET / PUT | /projects/{id}/email/smtp | SMTP config (password write-only) |
| POST | /projects/{id}/email/test | send a test email |
Types: magic_link, verify_email, reset_password, invite, welcome.
Cache (Valkey)
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/cache/keys | list (?pattern=, ?limit= ≤ 200) |
| GET / DELETE | /projects/{id}/cache/keys/{key} | get / delete |
| POST | /projects/{id}/cache/keys | set string (optional ttl) |
| POST | /projects/{id}/cache/flush | delete all project keys |
| GET | /projects/{id}/cache/stats | key count |
These are the admin/console cache reads (session-gated). The full server-side
KV surface (set/incr/scan/…) is a service_role data-plane API — see
Data-Plane API → Cache.
Cron (inspector)
Admin/console read-only mirror of the pg_cron data plane — the console holds
a session, not a service_role key, so it inspects via these endpoints. Scheduling
and unscheduling stay in the service_role data-plane API.
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/cron/jobs | list this project’s scheduled jobs (un-namespaced names) |
| GET | /projects/{id}/cron/jobs/{name}/runs | recent run history, newest-first (?limit= ≤ 100, default 20) |
Logs
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/logs | unified per-project log read (admin/console) |
Merges edge_function_logs (function invocations) and proxy_access_logs
(data-plane access logs) newest-first, tenant-scoped. Query: ?source=function|access|all
(default all), ?limit= (1–500, default 50), ?before= (opaque keyset cursor
echoed back as next_before). Entries are telemetry-shape only — method, status,
duration, route/function name — never bodies, headers, or keys.
Usage
| Method | Path | Purpose |
|---|
| GET | /projects/{id}/usage | per-project hourly usage time-series (admin/console) |
Returns { usage: [...] } newest-first — each bucket has bucket_start,
requests, bytes_in, bytes_out, function_invocations, storage_bytes.
Query: ?from= / ?to= (RFC3339 bounds on bucket_start), ?limit= (1–5000,
default 168 ≈ 7 days of hourly buckets).
Backups & PITR
| Method | Path | Purpose |
|---|
| GET / POST | /projects/{id}/backups | list / trigger |
| GET | /projects/{id}/backups/settings | read the scheduled-backup config |
| PUT | /projects/{id}/backups/settings | upsert schedule + retention + verify config ({enabled, interval_minutes, retain_days?, retain_count?, verify_enabled, verify_interval_minutes}) — admin scope |
| POST | /projects/{id}/backups/{backup_id}/restore | restore |
| GET | /projects/{id}/pitr/restore-points | list restore points |
| GET | /projects/{id}/pitr/restore/preview | read-only dry-run for a target (?target_time=<RFC3339>): selected basebackup, WAL window, data-loss cliff, warnings — no writes, safe to repeat |
| POST | /projects/{id}/pitr/restore | request restore ({target_time}; 202). PITR replays shared WAL, so the eventual restore is instance-wide and the destructive cutover is operator-gated |
Setting a backup schedule provisions nothing external — the control-plane backup
scheduler reads the config each tick (no pg_cron).
Saved queries & history
| Method | Path | Purpose |
|---|
| GET / POST | /projects/{id}/queries/saved | list / upsert |
| DELETE | /projects/{id}/queries/saved/{query_id} | delete |
| GET / POST | /projects/{id}/queries/history | list / record |
eIDAS config
| Method | Path | Purpose |
|---|
| GET / PATCH | /projects/{id}/eidas | get / replace config |
eIDAS wallet flows are data-plane (/v1/eidas/{id}/*).
| Method | Path | Purpose |
|---|
| GET | /admin/stats | platform-wide counts |
| GET | /admin/audit | recent entries (?action=, paginated) |
| GET | /admin/audit/export | bulk export (?format=json|csv, filters) |
| POST | /admin/audit/prune | prune the audit log + auth-event archive on demand (?days=; defaults to ANVILBASE_AUDIT_RETENTION_DAYS) |
| POST | /admin/webhooks/dlq/purge | purge DLQ deliveries on demand (?days=; defaults to ANVILBASE_DLQ_RETENTION_DAYS) |
| POST | /admin/encrypt-settings-secrets | one-shot sweep migrating legacy plaintext settings-secrets and the per-project projects credential columns (jwt_secret/previous_jwt_secret/anon_key/service_role_key) to enc:v1: at rest (idempotent; also runs as a non-fatal startup sweep) |
| POST | /admin/platform/upgrade | data-safe upgrade runner. ?target= (omit → running version, version-agnostic). Defaults to a side-effect-free DRY-RUN that returns the UpgradePlan (target version, images to pull, pending platform + per-project migrations, per-project snapshot plan). With ?execute=true it snapshots every project FIRST, aborts if any snapshot fails, applies migrations idempotently, and returns the rollback manifest (prior version/images + per-project snapshot ids). See Upgrades. |
| GET / POST | /admin/users | list / create platform users |
| PATCH / DELETE | /admin/users/{id} | update / delete |
Platform roles: super_admin, project_admin, developer, viewer (can’t delete
the last active super_admin).
For request/response bodies and worked curl examples per endpoint, this catalog
pairs with the in-repo docs/api/management.md long-form reference and the live
GET /openapi.json. See also Data-Plane API and
Error Codes.