Devtools UI
Walkthrough of the local devtools dashboard's Migrations tab — drift banner, pending plan card, and history timeline. What's clickable, what's not, and when the Apply button appears.
The local devtools dashboard (http://localhost:5179) ships a per-app Migrations tab that mounts the shared MigrationsPage component from @voltro/devtools-ui. The cloud dashboard mounts the same component over different transport — see Cloud UI.
To reach it: voltro dev boot launches the dashboard automatically, pick an app from the sidebar, click the Migrations tab between Database and Data.
Layout
┌──────────────────────────────────────────────────────────────┐
│ Migrations │
│ App: myApi · http://localhost:4000 │
├──────────────────────────────────────────────────────────────┤
│ ⚠ Schema drift detected │ ← drift banner
│ last applied: 8f507ba1e1aadad5 at 2026-06-15 14:32:00 │ only when drifted
│ live now: d32149b280101693 │
│ → Run `voltro db plan` to see what your code expects. │
├──────────────────────────────────────────────────────────────┤
│ Pending plan — 3 ops │ ← pending card
│ ✓ ALTER TABLE users ADD COLUMN bio text │ classifications +
│ ⊕ ALTER TABLE users ADD COLUMN email text │ fix hints
│ # NOT NULL, declared backfill: sql`'unknown-' || id` │
│ ⊕ UPDATE users SET email = ... │
│ │
│ from 8f507ba1e1aadad5 → to a8f2c9d10b3f4e62 │
├──────────────────────────────────────────────────────────────┤
│ Applied history — up to 20 plans │ ← history timeline
│ plan_mig_5k78 fp=d3214928 env=dev src=auto-diff ... │
│ plan_mig_5k77 fp=b414a413 env=dev src=auto-diff ... │
│ plan_mig_5k76 fp=bd27c13e env=dev src=auto-diff ... │
│ ... │
└──────────────────────────────────────────────────────────────┘Auto-refreshes every 10 seconds. The transport is HTTP — the page fetches GET <app-url>/_voltro/inspect/migrations directly + renders the JSON.
Drift banner
Renders at the top in rose when drift.isDrifted: true. Surfaces:
- last applied fingerprint + the time it was applied
- live fingerprint computed from the current introspection
- A pointer at the CLI command to investigate
The banner is informational — it doesn't block anything. The Apply button below it (when present) still works; the plan diff will include reconciling changes to bring live back to declared.
Common drift causes:
- A
psql(or equivalent) session ran DDL outside the planner - A team member applied a plan but the row didn't make it into
_voltro_migration_plans(rare; would mean the applier crashed after DDL but before writing the row) - The
_voltro_migration_planstable itself got truncated / restored from a backup
For non-trivial drift, run voltro db drift in the shell — same diagnosis, with a copy-pasteable fix path.
Pending plan card
Renders the MigrationPlan from the planner.
- One line per
PlannedOperationwith a color-coded badge for itsOperationClass:safe/needs-default→ emeraldneeds-backfill/needs-rename-annotation→ amberlossy→ roseonline-required→ cyanmulti-step→ fuchsia
- The op description follows the badge — e.g.
ALTER TABLE users ADD COLUMN email text NOT NULL - A dim-text reason follows:
# backfill declared (kind=sql) — applier will run 3-step add/update/set-not-null - For blocked ops, a red
! fix:hint underneath:! fix: declare email: text().backfill(sql...) ...
Below the op list, a chip row summarises counts per class. Below that, the from/to fingerprint short forms.
The Apply button
The shared MigrationsPage component renders an "Apply plan" button
only when BOTH the caller's canApplyMigration capability is set AND
the host wires an optional useApplyPlan hook (and the pending plan has
zero blocked ops). The local devtools is single-user, single-machine, so
it can wire that hook; the cloud dashboard never does (see
Cloud UI).
When the Apply button isn't wired, the page surfaces the next-best
thing: run voltro db apply from the project root + the page
auto-refreshes within 10 seconds to show the post-apply state. That CLI
path carries --note '...', runs with whatever credentials are in the
operator's shell, and exits with a status code CI/CD can act on — and
it's the only path that ever reaches production, since
voltro db apply refuses on NODE_ENV=production.
History timeline
Lists rows from _voltro_migration_plans newest-first, capped at 20.
Per-row:
- plan id —
plan_mig_5k78, the typeid from the row - fp — 16-char short fingerprint of the post-apply state
- env — dev / staging / prod (color-coded chip)
- src — auto-diff (planner) or file (file-based migration)
- op count — how many ops were in that plan
- duration — milliseconds the apply took
- applied at — ISO timestamp
- applied by — CLI user /
boot:dev/ service principal - notes — the freeform
--notestring if provided, dim italic
Click a row to expand → shows the operations JSON (the full PlannedOperation[] that ran). Useful for "why is users.legacy gone?" — find the plan that dropped it, see exactly what executed.
No per-row Rollback button
History rows are read-only — there is NO Rollback button on a plan row.
That matches the runtime: planner-applied plans have no auto-rollback
(see Rollback). The page DOES surface a separate
restore control for soft-dropped columns (when a plan was applied with
VOLTRO_SOFT_DROP=1, the sidecar columns get a per-plan "Restore"
button driving voltro db restore-snapshot). Reversing a file-based
migration's down body is a CLI-only action (voltro db rollback-file <id>).
Loading + error states
The page wraps the data fetch in a DataSource<MigrationsStatus>:
- Pending: shows a loading card while the first fetch is in flight
- Error: renders the error message in rose
- Data + error both undefined → page is in initial state, no flicker
A common error: the framework's inspect endpoint isn't reachable. Verify with curl -sS http://localhost:4000/_voltro/inspect/migrations — should return JSON. If not, check that the app is running + VOLTRO_INSPECT isn't off.
Multi-app
The dashboard's app picker shows every running voltro process (from ~/.voltro/runtime-registry.json). Each app gets its own Migrations tab; the data is per-app + the URL includes the app id.
If multiple apps target the same database, they're showing the same _voltro_migration_plans rows — applied plans are global. Drift detection runs against the live DB per-app inspect, so if one app is talking to a different db (env var override etc.) you might see drift on one but not the other.
Source code
The shared component lives in voltro/packages/devtools-ui/src/pages/MigrationsPage.tsx. The local devtools wiring is in voltro-devtools/apps/dashboard/src/pages/apps/[appId]/migrations.tsx. Both repos are open to extension.