CLI Reference
The AnvilBase CLI (anvilbase) is a single Rust binary that drives the control
plane’s HTTP API. Most commands resolve a control-plane URL, attach your token, call
an endpoint, and print formatted output.
Install
cargo install --path cli # from a checkout# or build explicitlycargo build -p anvilbase-cli --release./target/release/anvilbase --helpThe crate is anvilbase-cli; the binary is anvilbase. Standard clap helpers
apply: anvilbase --help, anvilbase <group> --help, anvilbase --version.
Configuration & auth
Control-plane URL resolution (first match wins)
| Order | Source |
|---|---|
| 1 | --url <URL> flag or ANVILBASE_URL env var |
| 2 | --env <name> → that environment’s url in anvilbase.toml |
| 3 | .anvilbase/config.json (url field) in the current directory |
| 4 | http://localhost:39001 (default) |
A .env in the current directory is auto-loaded, so ANVILBASE_URL /
ANVILBASE_TOKEN can live there.
Token
Set ANVILBASE_TOKEN to a management credential (a PAT anvilbase_pat_…, or the
admin token). It’s attached as Authorization: Bearer <token> on every request.
export ANVILBASE_TOKEN="anvilbase_pat_xxxxxxxx"The management API is fail-closed, so any /api/v1 command needs a valid token;
health/status work without one. realtime subscribe is the exception — it
authenticates via a ?token= query param and takes a project credential from
ANVILBASE_REALTIME_TOKEN (or --token), not the management ANVILBASE_TOKEN
(see Realtime).
For a CLI-managed local stack, anvilbase start generates ANVILBASE_ADMIN_TOKEN
into .env and anvilbase status --local prints an export ANVILBASE_TOKEN=<…>
tip for it. For raw docker compose up deployments you set it yourself.
Mint, audit, and revoke PATs (requires an admin credential):
anvilbase token mint --name ci-deploy --scope deploy --expires 90d # plaintext shown ONCEanvilbase token list # metadata onlyanvilbase token revoke <id>Scopes: admin (everything) ⊇ deploy (CI surface + reads) ⊇ read (GET-only,
minus sensitive reads: secret reveals, bulk audit export, platform-user listing).
See Security → RBAC.
Global flags
| Flag | Env | Description |
|---|---|---|
--url <URL> | ANVILBASE_URL | control-plane base URL |
--env <name> | — | target a named environment from anvilbase.toml |
--help / -h | — | help for the binary or any subcommand |
--version / -V | — | CLI version |
Platform & health
anvilbase health # GET /health/services — overall + per-service OK/FAILanvilbase status # health + /api/v1/admin/stats (project counts)anvilbase status --local # local stack URLs + secrets from .env (no network)/health and /health/services report the running control-plane version.
health and status compare it against the CLI’s own version and print a
one-line warning: CLI … != server … to stderr on a mismatch — a stale binary
otherwise reads as “missing features.” Matching versions (or an older server
that doesn’t report version) print nothing.
Local stack
Thin wrappers over docker compose (compose file auto-discovered; COMPOSE_FILE
honored). On first start, a .env with fresh secrets is generated if missing.
anvilbase start # docker compose up -d --waitanvilbase start --build # rebuild images firstanvilbase start --no-wait # don't block on healthchecksanvilbase ps # container statusanvilbase restartanvilbase stop # stop, keep containers + dataanvilbase down # remove containers/networks (keeps volumes)anvilbase down --volumes # also remove volumes — DESTROYS local dataProjects
anvilbase projects list # GET /api/v1/projectsanvilbase projects create "My App" # POST /api/v1/projectsanvilbase projects create "My App" --slug my-appanvilbase projects info <id> # GET /api/v1/projects/{id}anvilbase projects suspend <id> # POST /api/v1/projects/{id}/suspend (blocks data plane)anvilbase projects resume <id> # POST /api/v1/projects/{id}/resumeanvilbase projects delete <id> # DELETE /api/v1/projects/{id}anvilbase projects keys <id> # anon plaintext + service_role maskedanvilbase projects keys <id> --show-service-role # reveal service_role (audit-logged)anvilbase projects pooler <id> # GET /api/v1/projects/{id}/pooler — connection strings
# SMTP — per-project email deliveryanvilbase projects smtp show <id> # GET /api/v1/projects/{id}/email/smtpanvilbase projects smtp set <id> # local dev Mailpit defaults (host=mailpit port=1025 encryption=none)anvilbase projects smtp set <id> \ --host smtp.sendgrid.net --port 587 \ --from noreply@myapp.com --from-name "My App" \ --user apikey --encryption starttls # production provider (password prompted)create prints the new id, anon key, service role key, and JWT secret — the
service role key and JWT secret are shown once.
pooler prints the project’s pooled (Supavisor, transaction mode) and
direct (session) Postgres connection strings. The pooled string embeds the
per-project pooler password, so the reveal is audit-logged on the server. The
pooled endpoint is profile-gated; if you ran a plain docker compose up, start
Supavisor with docker compose --profile pooler up -d (or use anvilbase start,
which enables it automatically). anvilbase projects pooler surfaces the
server’s pooler_reachable signal and warns when the pooler isn’t reachable. See
Connection Pooling.
After creating a project, run projects smtp set <id> to configure email delivery.
Mailpit defaults work for local dev out of the box (no flags needed).
Branch (database branching)
Isolated clones of a project’s database — Neon-style branching. A branch gets its own database, API keys, and JWT secret, so it’s a full, isolated tenant; deleting a branch never touches the parent.
anvilbase branch create --project <id> dev # POST /api/v1/projects/{id}/branches — clone schema + dataanvilbase branch create --project <id> dev --schema-only # DDL only, ZERO rows (fast)anvilbase branch list --project <id> # GET /api/v1/projects/{id}/branchesanvilbase branch delete --project <id> <slug> # DELETE /api/v1/projects/{id}/branches/{slug}branch create takes the branch name as a positional arg (it becomes the slug)
and prints the new branch’s id and keys. A data branch clones in ~30-60s
(dump-based fallback) or near-instantly when the in-engine TEMPLATE path
applies; --schema-only is always fast.
Database
anvilbase db push --project <id> # dry-run: list pending migrationsanvilbase db push --project <id> --apply # apply ordered ./migrations/*.sqlanvilbase db push --project <id> --path ./db/migrations --applyanvilbase db pull --project <id> # dump schema → schema-<id>.sqlanvilbase db pull --project <id> --data --yes # RLS-scoped seed sample → seed-<id>.sqlanvilbase db reset --project <id> # drop all tables + re-apply (prompts)anvilbase db reset --project <id> --yes # skip prompt (CI)
anvilbase migrations list --project <id> # applied vs local (drift detection)anvilbase migrations apply --project <id> # alias for db push --applySchema
anvilbase schema tables --project <id>anvilbase schema extensions --project <id>anvilbase schema table create --project <id> --name todos --from ./todos.jsonanvilbase schema table drop --project <id> --name todos --confirm todosanvilbase schema column add --project <id> --table todos --name notes --type text --nullable trueanvilbase schema column drop --project <id> --table todos --name notes --confirm notesDrops require --confirm to match the name exactly (checked before any HTTP call).
See Tables & Schema.
RLS
anvilbase rls status --project <id>anvilbase rls enable --project <id> --table todosanvilbase rls disable --project <id> --table todosanvilbase rls policies --project <id> --table todosanvilbase rls policy create --project <id> --table todos \ --name owner_select --command SELECT --using "user_id = current_user_id()"anvilbase rls policy drop --project <id> --table todos --name owner_selectanvilbase rls test --project <id> --table todos --role authenticated --user <user_id>See Row Level Security.
Auth (users, sessions, invites)
anvilbase auth users list --project <id>anvilbase auth users create alice@example.com --project <id> --name "Alice"anvilbase auth users ban <user_id> --project <id> --reason spam --expires-in 3600anvilbase auth users unban <user_id> --project <id>anvilbase auth users delete <user_id> --project <id>anvilbase auth sessions revoke --project <id> --user <user_id> # all sessionsanvilbase auth invites create --project <id> --email new@example.com --role adminanvilbase auth invites list --project <id>Secrets
anvilbase secrets list --project <id>anvilbase secrets set STRIPE_KEY "sk_live_abc==" --project <id> # name & value separateanvilbase secrets delete STRIPE_KEY --project <id>anvilbase secrets rotate jwt --project <id> # invalidate all JWTsanvilbase secrets rotate api-keys --project <id> # new anon + service_roleFunctions
anvilbase functions deploy hello --project <id> # ./functions/hello.ts OR ./functions/hello/index.tsanvilbase functions deploy webhook --project <id> --path ./src/edgeanvilbase functions list --project <id>functions deploy accepts both ./functions/<name>.ts and
./functions/<name>/index.ts (the Supabase layout) — whichever exists. It
persists through the management API (MinIO + the edge_functions store of
record), so deploys are durable — they survive restarts and appear in
functions list immediately (the listing reads the durable management store, no
invoke required). The function goes live on its first invocation via lazy
rehydration. See
Writing & Deploying Functions.
Webhooks
anvilbase webhooks list --project <id>anvilbase webhooks create --project <id> --url https://example.com/hook \ --events "insert,update,delete" --description "audit hook" # prints signing secret onceanvilbase webhooks update --project <id> --id <wid> --is-active falseanvilbase webhooks test --project <id> --id <wid>anvilbase webhooks deliveries --project <id> --id <wid> # recent deliveriesanvilbase webhooks deliveries --project <id> --id <wid> --status dead_letter # the DLQanvilbase webhooks delete --project <id> --id <wid>Database webhooks (row-change triggers)
Attach a webhook to a user table so it fires on INSERT/UPDATE/DELETE
(Supabase-style database webhooks). Both attach and detach are idempotent.
anvilbase webhooks attach --project <id> --webhook <wid> --table todos \ --events "insert,update,delete" # omit --events to fire on all ops; --schema defaults to publicanvilbase webhooks detach --project <id> --webhook <wid> --table todosanvilbase webhooks attachments --project <id> --webhook <wid> # tables this webhook is attached toRetries & dead-letter queue
# Re-enqueue an exhausted (dead-letter) delivery — resets it to pending and# re-notifies the webhooks service (increments manual_retry_count). Find the# delivery id via `webhooks deliveries --status dead_letter`.anvilbase webhooks retry --project <id> --webhook <wid> --delivery <delivery_id>
# Purge DLQ deliveries older than --days (admin). Omit --days to use the server# default (ANVILBASE_DLQ_RETENTION_DAYS, 30). A nightly pg_cron job handles the# steady-state case automatically.anvilbase webhooks dlq purge # server default windowanvilbase webhooks dlq purge --days 7 # tighter window for this runBackups
anvilbase backup create --project <id>anvilbase backup list --project <id>anvilbase backup restore <backup_id> --project <id>PITR (point-in-time recovery)
List restore points and dry-run / execute operator-driven restores to any moment covered by the basebackup + WAL archive.
anvilbase pitr restore-points --project <id> # GET /api/v1/projects/{id}/pitr/restore-pointsanvilbase pitr restore --project <id> --target-time 2026-04-17T15:04:05Z --dry-run # read-only previewanvilbase pitr restore --project <id> --target-time 2026-04-17T15:04:05Z # enqueue the restore job--target-time <RFC3339>— the moment to restore to (e.g.2026-04-17T15:04:05Z).--dry-run— preview only: prints which basebackup would be selected, the WAL replay window, and how much write activity would be rewound. Hits…/pitr/restore/preview; enqueues nothing.- Without
--dry-run,restorePOSTs to…/pitr/restoreand enqueues a pending restore job.
Upgrade (data-safe platform upgrade)
anvilbase upgrade drives the data-safe upgrade runner. It defaults to a
DRY-RUN — it prints exactly what would happen (target version, images to pull,
the platform + per-project migrations that would apply, and the per-project
snapshot plan) and changes nothing. You must pass --yes to actually
execute, which makes an accidental destructive upgrade impossible.
# DRY-RUN (default): print the plan, change nothing.anvilbase upgrade # target = the running version (no-op preview)anvilbase upgrade --target 1.1.0 # preview the plan to upgrade to 1.1.0anvilbase upgrade --target 1.1.0 --dry-run # explicit dry-run (same as default)
# EXECUTE: snapshot every project FIRST, abort if any snapshot fails, then# apply migrations idempotently. Prints the rollback manifest (per-project# pre-upgrade snapshot ids) at the end.anvilbase upgrade --target 1.1.0 --yes--target <version>— the version to upgrade to. Omit to use the running control plane’s own version (version-agnostic; a no-op until a newer release is deployed). A downgrade (target < current) is rejected.--dry-run— explicit preview (this is the default; mutually exclusive with--yes).--yes/-y— execute the upgrade. Requires an admin credential.
Rollback: restore the per-project pre-upgrade snapshots and pin the prior images. Full procedure in Upgrades.
Logs (audit)
anvilbase logs --limit 50anvilbase logs --project-id <id> --limit 100anvilbase logs --follow --project-id <id> # polls every 2s
# Retention (admin): prune the audit log + auth-event archive on demand.# Omit --days to use the server default (ANVILBASE_AUDIT_RETENTION_DAYS, 90).# A nightly pg_cron job handles the steady-state case automatically.anvilbase audit prune # server default windowanvilbase audit prune --days 30 # tighter window for this runRealtime (debug)
realtime subscribe needs a project credential — a project JWT, service_role
key, or anon key — supplied via ANVILBASE_REALTIME_TOKEN or --token. It is
deliberately a different env var from the management ANVILBASE_TOKEN: the token
is placed in the WebSocket ?token= query string, which leaks into access logs, so
the CLI refuses to send a management PAT (anvilbase_pat_…) or the ambient
ANVILBASE_TOKEN there.
export ANVILBASE_REALTIME_TOKEN="<project-jwt-or-service-role-or-anon-key>"anvilbase realtime subscribe --project <id> --channel "realtime:public:todos"# connect directly to the realtime container in local dev:REALTIME_URL=ws://localhost:4000 anvilbase realtime subscribe --project <id> --channel "realtime:public:todos"Code generation
anvilbase gen types typescript --project <id> > database.types.ts # Supabase-compatibleanvilbase gen types drizzle --project <id> > src/db/schema.ts # drizzle-ormMCP setup (AI tools)
anvilbase mcp setup # interactive: pick tools, project or globalanvilbase mcp status # what's configured + whether the token resolvesanvilbase mcp remove --agent cursorSee Tools → MCP.
Environments & deploy (multi-env)
Named environments live in anvilbase.toml (topology only — no secrets); local link
state lives in .anvilbase/config.json (0600).
anvilbase env add staging --url https://anvil.staging.example.com \ --project <ref> --token-source env:ANVILBASE_TOKEN_STAGINGanvilbase env listanvilbase env use staging # set the active default envanvilbase env diff staging prod # non-zero exit on divergence — a CI gate
anvilbase deploy staging # preflight → db push → secrets → functions → verifyanvilbase deploy prod --dry-run # preview, change nothinganvilbase deploy staging --only migrations,functionsanvilbase deploy prod --yes # required for require_approval envs (CI)Safety: CI owns promotion. Real writes to any non-
allow_local_writesenv are refused outside CI; promote by committing and letting the deploy workflow run. Reads (status,migrations list,env diff,db pull) are always allowed. Data only flows down (sampleddb pull --data); no command pushes local data up to a real environment.
config (local link file)
anvilbase config set --url https://anvil.example.comanvilbase config getanvilbase config show # active URL, its source, and resolution orderanvilbase link https://anvil.example.com # write url to .anvilbase/config.jsonanvilbase link staging # validate + activate a named envExit behavior
Commands exit non-zero on failure (connection errors, non-2xx, validation such as a
mismatched --confirm). config get is the exception: with no URL configured it
prints a notice to stderr but exits 0, so scripts can probe for a link file.
Next: Tools → MCP.