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 --help
voltro 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_DIALECTSTOREapp.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 subdirectory

The descriptor half imports only @voltro/protocol (or @voltro/workflow/define)

  • effect — browser-safe by construction; the server graph lives in the paired .server file. Fill in the TODOs, then voltro dev discovers 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.json

It 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 applying

rbac/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 check itself, 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).