Inspect & test
The HTTP inspect surface, the dashboard, voltro logs / voltro traces, voltro test, voltro e2e — the debugging + harness tools.
When something's wrong, these are the tools. Live inspection of a running app happens over an HTTP surface (and the dashboard that consumes it), not a dedicated CLI verb. The shell-facing debugging commands are voltro logs and voltro traces; the harness commands are voltro test and voltro e2e.
voltro inspect
voltro inspect <subcommand> is the ergonomic CLI wrapper over the HTTP surface below. It discovers every running api via ~/.voltro/runtime-registry.json, fans the matching GET/POST out to each, and renders the merged result — no curl + jq needed.
voltro inspect app # manifest meta (kind, name, store, …)
voltro inspect routes # web page tree (web apps only)
voltro inspect rpc # procedures + workflow descriptors (api only)
voltro inspect metrics # rolling rpc latency buckets
voltro inspect cache # web data-cache stats (web apps only)
voltro inspect schedules # cron registrations + coordination mode
voltro inspect schedules --failing # only broken crons — exits 1 if any (see below)
voltro inspect aggregates # materialised aggregate views
voltro inspect invoke --tag users.list --input '{}' # call a procedure over HTTPFlags on every subcommand: --process <name> narrows to one api; --format pretty|json (default pretty). invoke additionally takes --tag <procedureTag> and --input <json>. With no subcommand it prints the endpoint map + the live processes it can reach.
schedules --failing — is any cron actually broken?
A schedule fires unattended: there is no user watching it fail. The plain
schedules listing answers which crons exist and when they fire next — never
whether they work. --failing rolls each schedule's recent runs
(/_voltro/inspect/schedules/runs) into a verdict and prints only the broken
ones:
$ voltro inspect schedules --failing
# schedules @api
sprint.report 0 2 * * * FAILING x87
every recorded run failed (last 20)
Workflow "sprint.report" was started with an invalid payload. missing required field(s): teamIdIt exits 1 when anything is failing, so it works as a post-deploy gate and not only as something someone remembers to run:
voltro inspect schedules --failing || echo "broken cron — do not promote"A trailing success ends a streak (a recovered job is not reported), and
skipped / missed runs are ignored — those are coordination outcomes (another
pod took the tick, the process was down), not handler verdicts. A schedule that
has never run is not "failing".
Pair it with voltro logs --level error: a failing handler now logs at error
level, so the two surfaces agree.
Targeting a deployed app
Every command in this family resolves its target from the local runtime registry — the apps running on this machine. Pass --url to point one at a deployed app instead:
voltro inspect app --url https://api.example.com --token "$TOKEN"
voltro logs --tail 100 --url https://api.example.com
voltro traces --errors --url https://api.example.com
voltro check --url https://api.example.com--token (or VOLTRO_INSPECT_TOKEN) supplies the bearer; VOLTRO_INSPECT_URL sets a default target so you can drop the flag. Works for inspect, logs, traces, workflows, cluster and check.
Securing the local surface
voltro dev mints a per-project VOLTRO_INSPECT_TOKEN into .env.local, so the inspect surface is authenticated from the first boot — the dev server listens on every interface, and without a token anyone on the same network could read your rows, schema and logs. You don't have to wire it anywhere: the CLI picks the token up from the runtime registry (so the commands work from any directory), and the dashboard's server-side proxy supplies it for same-machine targets. Setting VOLTRO_INSPECT_TOKEN yourself always wins.
The inspect HTTP surface
Every voltro dev / voltro start instance exposes a read-only introspection surface under /_voltro/inspect/*. The Voltro Dashboard consumes it to render the route sitemap, RPC list, subscription panel, workflow runs, and metrics. You can also hit the endpoints directly with curl (the voltro inspect subcommands above are the thin wrapper over exactly these).
PORT=4000 # from the app's app.config.ts
curl -s localhost:$PORT/_voltro/inspect/routes | jq # web: page tree + render-mode flags
curl -s localhost:$PORT/_voltro/inspect/rpc | jq # api: every query / mutation / action / workflow
curl -s localhost:$PORT/_voltro/inspect/metrics | jq # rolling per-tag latency + invocation countThere is no /_voltro/inspect/queries endpoint. The registered GET surface is app, routes (web-only), cache (web-only), rpc (api-only), metrics, and subscriptions (api-only) — rpc is the procedure list, routes is the web page tree.
What the surface reads:
- The endpoints serve in-process state from the running instance — no separate daemon.
- The default target is
http://localhost:<port>based on the cwd'sapp.config.ts. - GET responses are JSON; pipe into
jq.
Routes & RPC
curl -s localhost:$PORT/_voltro/inspect/routes | jq # web: page tree + render-mode flags
curl -s localhost:$PORT/_voltro/inspect/rpc | jq # api: query / mutation / action / workflow listSubscriptions panel
curl -s localhost:$PORT/_voltro/inspect/subscriptions | jqShows every active subscriber with:
- Query name + input
- Subject (who's subscribed)
- Read set (which tables / rows are tracked)
- Frame buffer depth (backpressure indicator)
- Connection age
For "why isn't this updating?" — check the read set. If your mutation writes to a table not in the read set, the subscription doesn't invalidate.
Workflow runs — voltro workflows
voltro workflows is the primary surface for inspecting + operating runs:
voltro workflows list # recent runs
voltro workflows show <runId> # one run's steps + events
voltro workflows retry <runId>
voltro workflows cancel <runId>
voltro workflows suspend <runId>
voltro workflows resume <runId>
voltro workflows signal <runId> --name approval # inject a named signal
voltro workflows update <runId> --name …
voltro workflows children <parentExecutionId>Underneath, workflow state lives in the _voltro_workflow_runs + _voltro_workflow_run_steps tables and is surfaced live by the dashboard's Workflows panel. The same data is reachable over HTTP:
curl -s localhost:$PORT/_voltro/inspect/workflows/runs | jq # recent runs
curl -s "localhost:$PORT/_voltro/inspect/workflows/runs/<runId>/steps" | jq # step-by-step
curl -s "localhost:$PORT/_voltro/inspect/workflows/runs/<runId>/events" | jq # the run's event logEach run row carries ID, name, status (running / succeeded / failed / dead), step count, last completed step, and duration. The per-run action endpoint matches …/workflows/runs/<runId>/<action> for cancel / retry / suspend / resume / signal — the voltro workflows subcommands and the dashboard's run-detail buttons both POST to these:
curl -s -X POST localhost:$PORT/_voltro/inspect/workflows/runs/<runId>/retry
curl -s -X POST localhost:$PORT/_voltro/inspect/workflows/runs/<runId>/cancelFor dead-letter triage, filter runs by status, or open the dashboard's Workflows tab. See Workflows / Debugging.
voltro logs
The fastest way to see what a running instance is doing. Buffers the last 2000 server-side log records plus every browser console line the dev console bridge forwarded.
voltro logs # last 100 from every running process
voltro logs --tail 50 --level error # only errors, last 50
voltro logs --since 30s # last 30 seconds
voltro logs --filter 'notes.summarise' # message substring
voltro logs --trace <traceId> # the WHOLE causal chain for one request
voltro logs --format json | jq '.records' # machine-parseableRun this BEFORE grepping source — the buffer carries the real error, stack, and rpc tag. The full flag set (--scope, --source, --process, --no-color, …) is documented in Traces & logs from the shell — the canonical reference for both commands.
voltro traces
Mirror of voltro logs for the distributed-trace buffer.
voltro traces # 20 newest traces, pretty
voltro traces --errors # only traces containing an errored span
voltro traces --id <traceId> # one trace, span waterfall
voltro traces --errors --format json | jq '.traces[]'Workflow: voltro traces --errors --format json to find a failure, then voltro logs --trace <id> --format json for the full chain (frontend → api → api, in order). The full flag set (--min-duration, --status, --process, …) lives in Traces & logs from the shell.
voltro cluster
voltro cluster status gives a clustering snapshot of every running api — one row per instance with its replicaId, runner address, dialect, CDC flavour, coordination mode, and server_id, plus a flag for any SQL runner stuck on localhost (a common misconfiguration that silently breaks cross-instance work). Use it to confirm a multi-instance deployment actually formed a cluster rather than N isolated nodes.
voltro cluster status # pretty table across every running api
voltro cluster status --json # machine-readable (also --format json)
voltro cluster status --process api # narrow to one named processLike the rest of the inspect family it reads the live /_voltro/inspect/* surface, so an api has to be running.
voltro test
voltro test
voltro test path/to/file.test.ts
voltro test --filter notesRuns Vitest with the framework's preset:
- Auto-loaded global setup (test context, mock providers).
STORE=memoryby default for fast isolation.- Plays well with
@voltro/testinghelpers (mock stores).
Every vitest flag is forwarded
Anything the command does not interpret itself goes straight to vitest — parsed by vitest's own CLI parser, not a list this wrapper maintains:
voltro test --coverage
voltro test --reporter=junit --outputFile=reports/junit.xml
voltro test --coverage --reporter=junit --outputFile=reports/junit.xmlThat covers coverage numbers and a JUnit report for a merge-request widget, which is what most pipelines want beyond the exit code.
The framework keeps three decisions for itself and they win over a forwarded
flag: the root (a positional that is an existing directory, which vitest
would otherwise read as a filter), --watch, and passWithNoTests — an explicit
filter that matches no file is an error, which vitest cannot decide because it
does not know which positional was treated as a root.
An unrecognised flag is ignored rather than fatal, and a flag vitest cannot parse at all degrades to "run without the extra flags" with a warning instead of taking the run down.
The actual test runner is Vitest; this command is a thin wrapper that injects the framework's config. You can run vitest directly if you prefer:
pnpm vitestvoltro e2e
voltro e2e # current dir
voltro e2e apps/web # explicit web app directoryvoltro e2e takes only an optional path to the web app; it parses no other flags. Boots:
voltro devfor the api app.voltro devfor the web app.- Playwright runs
*.e2e.tsfiles. - Tear down: stops the boot processes.
The e2e files use Playwright's API directly:
// apps/web/tests/signup.e2e.ts
import { test, expect } from '@playwright/test'
test('signup creates a user', async ({ page }) => {
await page.goto('/signup')
await page.fill('[name=email]', 'a@b.com')
await page.fill('[name=password]', 'correct horse battery staple')
await page.click('button[type=submit]')
await expect(page).toHaveURL(/\/dashboard/)
})The framework's voltro e2e handles the lifecycle for you: it boots the api + web siblings, runs Playwright against them, and tears the processes down on exit. Per-test isolation, fixtures, reporters, and sharding are configured in your Playwright config — voltro e2e itself forwards no flags to Playwright.
Securing the inspect surface
The surface is fail-closed: with no VOLTRO_INSPECT_TOKEN configured, every endpoint answers 401. The absence of a secret is not consent — a check you were configured not to perform is a refusal, not a pass.
That is why voltro dev mints one for you (above) and why nothing mints outside dev: in production a missing secret must stay a boot-time decision rather than an invented value. So a production voltro start serves nothing here until you set the token yourself:
VOLTRO_INSPECT_TOKEN=$(openssl rand -hex 32)
voltro start apps/apiThen every request must carry the token:
curl -s -H "Authorization: Bearer $VOLTRO_INSPECT_TOKEN" \
localhost:4000/_voltro/inspect/rpc | jqOr disable the surface entirely: VOLTRO_INSPECT=off. The dashboard then can't introspect the production instance — that's intentional.
Anti-patterns
- Assuming the surface is open because you didn't configure it. It is the reverse: no token means
401, not "everyone". If a production dashboard suddenly stops introspecting, the missingVOLTRO_INSPECT_TOKENis the first thing to check — not a network problem. voltro testagainstSTORE=postgresby default. Slower + flaky (test isolation harder). Use postgres only for integration tests that NEED it.- Skipping
voltro e2ebecause "it's slow". It catches integration bugs that unit tests miss. Run it in CI on every PR; locally for changes that touch queries.
See also
- Workflows / Debugging — the dashboard's workflow panel
- Self-hosting — production observability setup