Overview
The voltro CLI — every command, grouped by purpose, with the flags that actually matter.
The Voltro CLI is the single entry point for scaffolding, dev, build, and run. It also seeds the framework agent guide into every project — written under BOTH AGENTS.md and CLAUDE.md — so AI coding agents have your framework conventions on tap.
Command quick-reference
The dispatcher routes voltro <command> [args] to the matching subcommand and passes the rest through. voltro help is the authority — it prints the registry itself, so it cannot drift from what your installed CLI dispatches. The table below is every command that registry exposes, grouped by purpose:
| Group | Commands |
|---|---|
| Scaffolding | init (initialise the current directory as a workspace root — no apps), create-project, add-app, list-templates, new (scaffold one primitive — query / mutation / action / workflow / page) |
| Packages | package (create / publishable / private / status), create-package |
| Dev | dev, codegen, typecheck (tsc --noEmit with the app's own TypeScript), agents-md, env (check / sync / types / turbo), generate (AI app-builder), dashboard (serve the DevTools dashboard standalone; --port, VOLTRO_DASHBOARD_APPS) |
| Build & run | build, start, serve |
| Deploy | deploy (plan — auto-detect the target tier per app + function), serverless (list / dev / serve / build / deploy), static (hosts / deploy), dormancy (single-node scale-to-zero: fronts the app, stops it when idle, wakes on the next request; --idle-grace-ms / --tick-ms) |
| Database | migrate, db (plan / apply / plans / drift / squash / restore-snapshot / migrate / rollback / status / seed), evolve (schema-evolution copilot — propose a codemod + branch-verified backfill for a rename / retype / split / drop of an existing column or table) |
| Update | update (--to / --dry-run / --force / --exact) — bump every @voltro/*, install, run the codemods that adapt your source to the new version |
| Data transfer | data (export / import / unpack / inspect / backup / restore) — directory + single-file .vbundle bundles, streaming assets, masking, at-rest encryption |
| Ops / infra | cache (status / flush / invalidate), add (redis), baseline (list / status / set), schedule (run <name> — fire a registered schedule on demand; --process / --trigger manual|external / --url), schedule-manifest, storage (doctor / cors) |
| AI / data | embeddings backfill <table> --text <field> --vector <field> — (re)embed rows the vectorEmbedding() mixin missed (pre-existing rows / a model change); --dry-run to preview; eval — replay recorded agent runs against golden cases + judge, exit 1 on regression (a deploy gate; --json / --branch / --threshold) |
| Integrate | webhooks (consumer / events) — generate the ZERO-dependency Standard-Webhooks verification package your subscribers install, from your own declared events (--out / --name); list the events a subscriber can register for (--json) |
| Inspect & debug | inspect, logs, traces, workflows, cluster, check |
| Health & surface | doctor — serve preflight + the hand-roll detector (names the shipped primitive at the spot you're rebuilding it); capabilities (--json) — the export surface read from your installed @voltro/*, so it can be verified instead of recalled; info (--json) — CLI / node / package-manager / dialect + every installed @voltro/* version, flagging lockstep skew (exits 1 on skew) |
| Mobile | mobile (codegen / links) — the Expo app's build-time steps: the typed rpc client + deep-link table, and the apple-app-site-association / assetlinks.json a universal link needs. voltro codegen inside a mobile app runs the same generators. |
| Harness | test, e2e |
| Cloud | cloud (login / whoami / projects / env / import); login is a top-level alias of cloud login |
| Secrets | secret (generate [purpose] — the right var+format per secret; generate alone → a generic secret; list) |
| Meta | agents-md, telemetry (reports that Voltro collects none — no phone-home, nothing to opt out of), version, help |
Two commands are hidden from voltro help on purpose and are not in the
table: prune-runtime <deploy-dir> (trims a deployed web tree's node_modules
to the reachable runtime set — the standalone Dockerfiles run it for you) and
_apply-codemods (the re-exec target voltro update uses to run the target
version's codemods). Both are dispatchable; neither is something you invoke
directly.
Each command takes an optional path argument (the app directory) — defaults to . when run from inside an app. voltro init initialises the current directory as a workspace root — pnpm-workspace.yaml, a root package.json with dev/build/test/typecheck, a .gitignore and git init, idempotently and without scaffolding any apps (voltro create-project <name> does that, and bootstraps the same root when there isn't one). voltro secret generate [purpose] prints a cryptographically strong secret — with a purpose (data-transfer, session, bundle-key, field-encryption, storage, inspect) it emits the correct env var + length/format as a paste-ready NAME=value (voltro secret list shows them all); with no purpose, a generic base64url secret. voltro telemetry reports that Voltro collects none. voltro deploy shows the deploy paths — self-host via a baseline + CI, managed via the control-plane client voltro cloud (managed cloud deploy is coming soon — see Voltro Cloud), individual serverless functions (voltro serverless → self-hosted Node, or Cloudflare / Scaleway), or a static site (voltro static → Cloudflare Pages / S3 / Netlify).
Help + version
voltro help prints the command list; voltro version prints the CLI version. Both are also reachable as bare flags — voltro --help / -h and voltro --version / -v — handled by the dispatcher before it looks for a subcommand.
Per-command help is the useful one. voltro <command> --help (equivalently voltro help <command>) prints that command's usage line, its flags, the environment variables that change what it does, and worked examples:
voltro dev --helpvoltro dev — Start the local development server (auto-discovers routes / mutations).
Usage
voltro dev [app]
Examples
voltro dev
boot the app in the current directory
voltro dev apps/acme/api
boot a specific app
Environment
PORT override the port from app.config.ts
WATCH=0 run the server directly, with no file-watching supervisor
WATCH_POLL=1 poll instead of using native FS events (bind-mounts: k8s hostPath / Docker)
…--help never runs the command. A command with subcommands of its own (db, cloud, workflows, inspect, update, …) prints its own richer page instead.
Startup
The CLI loads a command's implementation only when you dispatch it, so voltro version, voltro info and voltro new do not pay for the dev server, the build toolchain or the runtime. The same applies to the voltro dev supervisor's respawned child on every save.
The CLI's first line is its own — no Node warnings ahead of it.
Common per-command flags
There is no universal global-flag layer; flags are per-command. The ones that recur:
| Flag | Where it applies |
|---|---|
--format pretty|json |
logs, traces, inspect, cluster, workflows — machine-readable output. |
--no-color |
logs, traces — strip ANSI codes when piping to a file. |
--process <name> |
logs, traces, inspect, cluster — narrow to one running process. |
--force |
agents-md — overwrite existing files. |
Commands like build / start / migrate / codegen parse no flags at all — only an optional path. Check each command's page for its real surface.
Common env vars
The framework reads more than a hundred distinct VOLTRO_* / DB_* / PG_*
variables across its packages, so this is not the full set and could not
usefully be — it is the ones you reach for. Each subsystem's page carries its own; the complete transport-security
and connection lists live in Production hardening
and Security.
| Var | Effect |
|---|---|
NODE_ENV |
production / development / test. Affects defaults across many commands. |
DB_DIALECT |
postgres (default) / mysql / mariadb / mssql / sqlite / turso / memory. Picks the SQL backend. |
STORE |
memory / postgres — alternate data-store selector (resolution order: DB_DIALECT → STORE → app.config.ts). |
WATCH |
0 / 1. Toggle filesystem watch in voltro dev. |
VOLTRO_DASHBOARD |
off to disable the auto-launched dashboard. |
VOLTRO_LOG_FORMAT |
pretty / json. Force the logger's output format. |
VOLTRO_LOG_LEVEL |
trace / debug / info / warn / error / fatal. |
VOLTRO_INSPECT |
off to disable the /_voltro/inspect/* HTTP endpoints. |
VOLTRO_INSPECT_TOKEN |
Bearer for the inspect surface. Fail-closed: unset → every request is 401. voltro dev mints one per project; voltro serve / voltro start mint nothing, so a public deploy is closed by default (set it explicitly to open the surface). |
VOLTRO_INSPECT_ALLOWED_HOSTS |
Extra Host names allowed to reach the dev inspect surface, past its DNS-rebinding guard (comma/space-separated). Loopback names + IP literals are always allowed; any other domain name is refused unless listed here — the api counterpart of vite's allowedHosts. |
DB_URL |
Database connection string (falls back to DB_PRIMARY_URL; or the discrete DB_* / PG_* fields). |
DB_ACQUIRE_TIMEOUT_MS |
How long a request may wait for a free pooled connection before failing (default 10000; 0 restores the driver's own unbounded wait). Read on every command and every dialect that can bound it. DB_ACQUIRE_QUEUE_LIMIT is its mysql/mariadb counterpart and is deliberately unset by default. |
VOLTRO_SESSION_SECRET |
Session-cookie signing secret (@voltro/plugin-auth). Rotate with zero downtime: move the old value to VOLTRO_SESSION_SECRET_PREVIOUS for one session lifetime — cookies signed with either secret keep verifying, and previous-key cookies are re-issued under the new one. |
VOLTRO_DATA_TRANSFER_SECRET |
Gates the prod data-transfer endpoints (POST /_voltro/admin/{export,import}); ≥16 chars or the routes don't mount. |
VOLTRO_BUNDLE_KEY |
Passphrase for .vbundle export encryption (voltro data export --encrypt). A DEDICATED key, not the transfer secret. |
VOLTRO_FIELD_ENCRYPTION_KEY |
Key for .encrypted() columns (governancePlugin({ fieldEncryption: true })) — a passphrase or a raw 64-hex AES-256 key. |
VOLTRO_STORAGE_SECRET |
Signs storage grant tokens (private files); falls back to the session secret if unset. |
AI_PROVIDER / AI_MODEL / AI_API_KEY |
Per-provider AI config. |
Transport security — the overrides on by default since 0.34.0
The api listener's cross-site check, proxy policy and security headers are on
by default and configured in app.config.ts's security block. Each has an
env override, resolved identically under dev and serve, with the explicit
config value always winning:
| Var | Effect |
|---|---|
VOLTRO_ORIGIN_GUARD |
off disables the cross-site origin check. Anything else leaves the default same-origin. |
VOLTRO_ALLOWED_ORIGINS |
Comma-separated origins allowed past that check — what a web app on a different origin than the api needs. |
VOLTRO_TRUSTED_PROXIES |
Which hops may set x-forwarded-for / x-forwarded-proto: a comma-separated CIDR/IP list, private, * (trust the leftmost token), or a hop count (2). Unset means the header is ignored entirely and the socket address wins — set it if you run behind an ingress AND rate-limit or audit per IP. |
VOLTRO_SECURITY_HEADERS |
off / default / strict — the whole header bundle's mode. |
VOLTRO_CSP |
Content-Security-Policy for non-HTML api responses. off drops just this one. |
VOLTRO_CSP_HTML |
CSP for HTML responses (under strict it defaults to the same policy as VOLTRO_CSP). off drops just this one. |
VOLTRO_HSTS |
Strict-Transport-Security value. off drops just this one. |
VOLTRO_MAX_RPC_BODY_BYTES |
Cap on the buffered POST /rpc JSON body (default 8 MiB) — an oversized body is refused 413 and never buffered past the cap. File uploads ride plugin routes with their own limits. |
VOLTRO_MAX_BODY_BYTES |
Cap on every OTHER body read — plugin HTTP routes, REST routes, incoming webhooks (default 8 MiB, matching the rpc cap). The config-file spelling is http.maxBodyBytes in app.config.ts; per-route overrides (defineRestRoute({ maxBodyBytes }), a webhook handler's maxBodyBytes) win over both. Oversize is 413 for Content-Length and chunked alike. |
VOLTRO_CRDT_COMPACT_MAX_BYTES |
Size above which a merged crdtText() / crdtDoc() blob is soft-compacted (default 512 KiB, 0 disables). The config-file spelling is crdt.compactMaxBytes in app.config.ts; this variable wins over it. See local-first. |
VOLTRO_GRPC_DRAIN_MS |
How long the gRPC surface lets open calls finish on shutdown before force-closing them (default 5000, 0 forces immediately). The config-file spelling is grpc.drainMs; this variable wins over it. Keep it below your orchestrator's termination grace. |
Response compression for the buffered non-rpc surfaces (and voltro start's
HTML) is configured in the same http: block — http.compression.{enabled,minBytes}
(default on, 1 KiB threshold; POST /rpc is never compressed). Details + the
BREACH reasoning: Security → compression.
Generate any of the secret vars above with voltro secret generate <purpose> (see secret) — it picks the right length and format. A lower environment's secrets must always differ from production's.
Workflow patterns
"I'm starting a new project"
mkdir acme && cd acme
pnpx voltro create-project acme --api api-backend --web frontend-landing
pnpm install
pnpm dev # pnpm -r --parallel dev — voltro dev in every app at once"I want to add a docs site to my existing project"
voltro add-app docs --template frontend-docs --to acme
pnpm install # picks up the new app's deps
pnpm dev # the new app joins the parallel boot automatically"I changed my schema and want to apply it"
voltro db plan # diff declared schema vs live, color-coded
# review the plan
voltro db apply # execute it (dev)
# `voltro migrate` is an alias of `db apply` — same differ, shorter name (CI / ops)"I want to encrypt a column that already has rows in it"
Turning on .encrypted() needs two things: the cipher, and a migration of the
rows that are already there. The second has a command — it is easy to miss,
because nothing about a schema change suggests a data pass is owed.
voltro db scan-credentials # find plaintext secrets, column by column
voltro db encrypt-column users.apiToken --dry-run
voltro db encrypt-column users.apiToken --yes--key-env names the key variable if it is not the default; the key must be the
SAME one the app runs with, or the rows come back undecryptable. voltro db
with no subcommand prints the full list.
"I want a clean rebuild"
voltro codegen # rewrite the generated rpc group + .framework/*
voltro build # vite build + SSG pre-render
voltro start # production server"Scaffold a new primitive the right way"
voltro new <kind> <name> writes the correct file convention(s) so you don't
learn the descriptor/executor split or the browser-safe boundary from a boot-time
error. It refuses to overwrite an existing file unless you pass --force.
voltro new query notes.list # notes.list.query.ts + notes.list.query.server.ts
voltro new mutation notes.create # descriptor + .server executor pair
voltro new action notes.touch # descriptor + .server executor pair
voltro new workflow orders.fulfill # .workflow.tsx descriptor + .workflow.server.tsx executor
voltro new page about # src-pages page.tsx under the name path
voltro new query billing.summary --dir queries # write into a subdirectoryThe descriptor half imports only @voltro/protocol (or @voltro/workflow/define)
effect— browser-safe by construction; the server graph lives in the paired.serverfile. Fill in theTODOs, thenvoltro devdiscovers it.
"Type-check before I commit"
voltro typecheck # tsc --noEmit against ./tsconfig.json
voltro typecheck apps/api # a specific app
voltro typecheck --project tsconfig.build.jsonIt runs the app's own TypeScript (a tsc --noEmit), so "green" means tests
AND types. Any flag it doesn't own passes straight through to tsc.
"What versions am I actually running?"
voltro info # CLI, node, package manager, dialect + @voltro/* versions
voltro info --json # machine-readable; exits 1 on version skew@voltro/* ship in lockstep, so a mismatch (e.g. @voltro/database a minor
behind @voltro/runtime) means an untested graph. voltro info flags it and
voltro update realigns everything.
"Something's wrong — inspect what's running"
voltro inspect rpc # discovered procedures + workflows
voltro inspect routes # web page tree (web apps)
voltro inspect metrics # request rates, p95 latencies
voltro logs --tail 100 # recent structured logs
voltro traces --errors # traces with an errored span
voltro workflows list # recent workflow runs"Did my edit break a binding?"
voltro check runs blast-radius checks over your app's typed graph — dangling
source / target tables, scopes no role grants, unguarded mutations, broken
route bindings, orphan tables — at edit time instead of at runtime. --json
emits LLM-shaped diagnostics so a coding agent can fix-and-repeat; --diff
previews the blast radius of a removal BEFORE you apply it.
It prefers a running api (its manifest is ground truth, including live table
introspection). With none reachable it assembles the same graph from source,
so it works as a pre-commit hook or a CI gate without a second terminal —
--offline forces that path.
voltro check # running api if there is one, else from source
voltro check --offline # never contact a server — the CI form
voltro check --url https://api.example.com # a DEPLOYED app
voltro check --json # { ok, diagnostics: [{ rule, node, breaks, fix }] }
voltro check --diff removeTable:todos # what a proposed removal would break, before applyingrbac/unknown-scope needs a declared scope vocabulary to compare against —
rbacPlugin({ roles }) publishes one automatically. It catches a guard
requiring a scope no role grants, which makes that procedure permanently and
silently uncallable. (With a custom resolvePermissions the vocabulary isn't
exhaustive, so the rule stays quiet rather than flagging correct code.)
rbac/unenforced-scope is the same registry read in the other direction: a
scope a role GRANTS that no handler ever guards on. That direction has no
artefact to inspect — you cannot grep for an authorization check that was never
written, which is exactly why it survives review. One app modelled
api-keys:write in its role catalogue, complete and reviewed, and no handler
checked it: any member could mint a shared credential, and nothing failed.
Tests pass when an authorization check is missing.
It is a warning, because three innocent explanations exist: a plugin route
enforces it internally (the graph cannot see inside a plugin), a REST route
carries its own guards, or it is a UI-affordance scope that useCan reads to
hide a button and no server check backs on purpose. All three are fine. Not
knowing which is not.
rbac/unguarded-mutation flags a mutation that declares neither a guard nor an
openAccess: reason. It skips internal: true procedures: those are in no
rpc group and on no route, so "any caller who can reach the rpc surface" names a
surface that does not exist — and neither remedy applies either, since a guard
would protect nothing and openAccess is refused outright on an internal
descriptor. This is the same isWireReachable predicate the boot access gate and
all three rpc-group assemblies use, so check and voltro doctor cannot answer
the question differently.
A manifest that does not carry the field at all — an api older than it — is read as reachable, not as internal. The rule stays loud rather than going quiet on the apps least able to notice.
Declared vs OBSERVED — reconciled against reality
A query's source and a mutation's targets are not documentation: the
framework routes optimistic patches and decides which subscriptions a write
invalidates from them. A wrong declaration is a live, user-visible bug that
nothing type-checks — the mutation succeeds, the write lands, and the wrong list
fails to update.
voltro dev records what each procedure ACTUALLY touched, into
app.graph.observed.generated.json (gitignored automatically). When that file is present,
check diffs it against the declarations:
observed: 12/34 procedures exercised (35%)
1 declared/observed mismatch among the 12 that ran
mutation(orders.place) (api/orders/place.mutation.ts)
writes 'inventory' (update) but declares no target for it
subscriptions on that table are not invalidated by this mutation
fix: add { table: 'inventory', op: 'update' } to this procedure's targets
20 never ran — no observation exists, so nothing is claimed about them
2 ran with no table access recorded — indistinguishable from touching nothing, so nothing is claimed about them either
Four things about that output are deliberate:
- Coverage comes first. Three findings at 8% coverage and three at 95% are different claims. Hiding the denominator is how a check starts overstating what it knows.
- Every result line carries the count it is a result about.
1 mismatch among the 12 that ran— never a bare verdict. A sentence that still reads as a conclusion once it is cut out of this block will eventually be cut out of it, and quoted as a clean bill of health for a surface nobody measured. - The counts partition the declared set — they add up to the total, always.
There are two ways to say nothing about a procedure, and both get a line:
it never ran, or it ran and no table access was recorded for it. A procedure
that appeared in no line at all would be indistinguishable from a defect in
checkitself, so the two kinds of blindness are named separately and never folded into the findings. - Observed diagnostics never fail the build. An observation is evidence
about the runs that happened, not a proof about the ones that didn't, and
check's exit code gates CI. They are always warnings.
A fifth thing is not visible in that output and matters more than any of the four: the recorder only knows what ran. A boot is not a run. An idle dev instance that started, served nothing and stopped produces
observed: 0/34 procedures exercised (0%)
nothing was compared — a declaration is only checked against a procedure that RAN
34 never ran — no observation exists, so nothing is claimed about them
Note what that is NOT: it is not "no mismatches found". At zero coverage there
is no result to report, so the section reports the absence of the comparison
instead. The honest answer is also a useless one — it says nothing about any of
the surface. If you want this as a CI gate, the recording pass has
to be a run that actually calls the procedures. voltro e2e is one: it spawns
voltro dev for the api, which turns recording on, and then drives the specs. A
harness of your own that boots the app in its own process needs
VOLTRO_OBSERVE_GRAPH=1 set before anything is imported. Either way, booting the
app and then running check measures nothing, and the report will not pretend
otherwise.
It is still worth wiring up at low coverage, because the findings are per
procedure and do not need company. One deployment had exactly one procedure
observed, and that one produced a real defect: a mutation upserting
push_subscriptions with no declared target while two queries read that table as
their source, so a registered device never appeared in any running subscription.
No file → the section is skipped silently. This is not derived by parsing your handlers: a static pass over code that reaches the store through shared helpers, behind conditionals, has a long tail of both false positives and false negatives — and a check that is sometimes wrong is one people stop reading.
Declared vs LIVE — against a running server
When check runs against a live api (a local voltro dev, or a deployed one
via --url), it also diffs the tags your source declares — plugin routes
included — against the server's actual rpc registry:
declared vs live: 2 of 214 source-declared tags are NOT registered on the server
⚠ presence.heartbeat — a generated client calls this and gets "Unknown request tag"
⚠ presence.list — a generated client calls this and gets "Unknown request tag"
fix: if the server runs older code, redeploy; if it is current, plugin route registration was dropped — check the boot line "plugin routes registered"
Only the source→live direction is a finding — a tag the server carries that your
source does not declare is normal runtime synthesis (agents, undo, approvals).
This exists because exactly that gap has shipped once: every
plugin-contributed procedure dead under voltro serve, with nothing anywhere
saying so — the only evidence was a Defect frame in the browser console of
whoever happened to look. (That registration defect is fixed; the diff is the
runtime backstop for the next one, e.g. a stale deploy.) The server now also
logs every Defect frame it sends (rpc defect sent to client), so an unknown
tag is an operator-visible event rather than a client-only one.
The HTTP surface is reachable directly too — e.g. curl -s localhost:4000/_voltro/inspect/rpc | jq (there is no /_voltro/inspect/queries endpoint; it's rpc for procedures, routes for the web page tree, subscriptions for active subscribers).
Where to read next
- Scaffolding — start + grow a project
- Dev — what happens during
voltro dev - Build & start — production paths
- Migrate — schema changes end-to-end
- Update — upgrade the framework + run codemods
- Inspect & test — debugging + harness