Cloud UI
Cloud dashboard's Migrations tab — what it shows, the submit→review→approve workflow with HMAC-signed review URLs, why it never applies (and never will), how it differs from the local devtools UI, and the multi-tenant boundary.
The cloud dashboard's per-app Migrations tab mounts the same MigrationsPage component the local devtools uses — the page itself is portable. The difference is transport + capabilities.
Transport
Local devtools fetches /_voltro/inspect/migrations directly over HTTP. The cloud dashboard can't — it doesn't have direct network access to the customer's app, and the inspect endpoint isn't internet-exposed in a sane deploy.
Instead, the cloud dashboard subscribes via cloud-RPC:
useAppInspectMigrationsStatus(appId)
// → calls the cloud-api's `apps.inspectMigrationsStatus({appId})` query
// → cloud-api looks up the app row (tenant-scoped to the caller)
// → cloud-api calls `<app.url>/_voltro/inspect/migrations` with the app's stored inspectToken
// → cloud-api returns the response to the dashboardThree indirection levels: dashboard → cloud-api → customer-app → DB. Each hop is auth-scoped — the dashboard only sees apps the calling user has access to; the cloud-api enforces caller-owns-tenant on every request; the customer-app verifies the inspectToken.
Live data: the subscription stays open; deltas push when the underlying _voltro_migration_plans row set changes (the customer-app's reactive engine emits a change event on insert, which propagates through the cloud-api's RPC proxy back to the dashboard subscription). Same UX as the local devtools' 10-second polling, but push-driven.
The Apply button is gone — permanently
Apply locally with voltro db apply (the dev CLI). The cloud dashboard never applies — by design; it shows the plan, the approval workflow gates a human-triggered apply, and the apply itself runs in your own pipeline.
Reasoning:
- Cloud-applied plans would need to hold migration-grade credentials in the cloud-api. Today the inspectToken authorises READ-only access to inspect endpoints; an apply endpoint would require an entirely separate trust boundary.
- Cloud-applied plans would mean the cloud platform owns the operational responsibility for the customer's schema. We don't want to.
- The plan model is designed for human review BEFORE apply. A button click that says "apply now" without forcing the operator through
voltro db planfirst → review → deploy isn't the safety story the docs promise. - For prod specifically: the rule is
voltro db applyruns as an explicit deploy step (and refuses onNODE_ENV=production). No browser button bypasses that.
What the cloud dashboard DOES instead: surfaces the pending plan (ops + classifications + fingerprints) read-only, and points the operator at the CLI command to run in their own pipeline:
Pending plan — 3 ops · 0 blocked
Apply via your deploy pipeline:
voltro db apply # re-diffs live + applies; run as a deploy stepThe apply re-diffs the live DB against the deployed code, so the
dashboard doesn't hand out a serialised plan to feed back in — the
plan it shows is the PREVIEW, and voltro db apply recomputes the same
diff at deploy time. This keeps the dashboard the source of truth for
what's pending, while never executing.
Multi-environment view
A per-app cloud dashboard has multiple environments. The Migrations tab surfaces all of them with sub-tabs:
┌────────────────────────────────────────────────────────────┐
│ Migrations │
│ App: myApi · project: acme · org: acme-inc │
├────────────────────────────────────────────────────────────┤
│ [ dev ] [ staging ] [ prod ] │ ← env tabs
├────────────────────────────────────────────────────────────┤
│ <selected env's MigrationsPage> │
└────────────────────────────────────────────────────────────┘Each tab shows its own MigrationsStatus. Switching tabs is instant + the data is per-env subscription.
Common workflow:
- Reviewer opens the PR's preview env (
dev) tab → confirms the plan ran cleanly there - Reviewer switches to
staging→ sees the SAME fingerprint applied → fingerprint chain looks healthy - Reviewer switches to
prod→ fingerprint is still old → operator hasn't run apply yet, that's expected, OK to merge
When prod drifts (the production fingerprint is older than staging), the prod tab shows the drift banner. Out-of-band alerting (Slack ping, email, PagerDuty) is intentionally NOT the framework's job — your deploy pipeline already has hooks for it (GitHub/GitLab Actions, k8s controllers, ArgoCD, etc.). The dashboard is the source of truth; whatever notification system your team already runs subscribes to the relevant pipeline events.
Pending-plans queue + approval workflow
The cloud-api ships a human-gated approval flow on top of the read-only
view above: an operator's deploy pipeline submits a plan, reviewers
approve (or reject) it, and only then does a human run
voltro db apply from the pipeline. The cloud still NEVER applies the
schema itself — approval gates the human apply, it does not perform it.
Submit a plan
The pipeline computes the plan with voltro db plan --json and posts it
to the migrations.submitPlan mutation. The plan lands as pending in
the migration_plans table (tenant-scoped) and the mutation returns the
plan id plus an HMAC-signed shareable review URL:
const { id, status, reviewUrl } = await submitPlan.mutate({
orgId, // = the active tenant id
appId, // which deployed app the plan targets
env: 'staging', // free-form env tag the pipeline submits
fingerprint: livePlan.toFingerprint,
operations: livePlan.operations, // [{ kind, table, classification, … }]
summary: livePlan.summary, // per-classification counts
})
// status === 'pending'
// reviewUrl === 'https://dashboard…/migrations/review?t=<signed-token>'The review URL carries a token whose body is { planId, exp },
base64url-encoded, with an HMAC-SHA256 signature appended. The token is
signed with VOLTRO_CLOUD_SECRET (falling back to
VOLTRO_SESSION_SECRET), so it can't be forged for a plan the reviewer
was never sent, and it expires after 7 days. A reviewer opening the link
hands the token to migrations.reviewByToken, which verifies the
signature + expiry server-side (constant-time compare) before returning
the plan — no interactive session required.
Review + decide
The dashboard's per-app Migrations tab renders a Pending Approvals
section above the read-only history. It subscribes to
migrations.pendingApprovals (a reactive, tenant-scoped query over
migration_plans) — each plan expands to its operation list +
per-classification summary badges, with Sign / Approve and
Reject buttons plus an optional note. Signing a decision calls the
migrations.approve mutation, which — in one transaction — appends an
immutable row to migration_approvals and flips the plan's status to
approved / rejected. The subscription drops the plan from the queue
on the status flip.
Approval is role-gated: only org owner / admin can sign a
decision (the same role guard the member-invite flow uses). A reviewer
without the role sees a typed MigrationApprovalForbidden rejection.
Still no apply from the browser
Approval is distinct from apply, by design. An approved plan tells the
operator "a reviewer signed off"; the operator still runs
voltro db apply as an explicit deploy step (which re-diffs live and
refuses on NODE_ENV=production without a pre-reviewed plan). When the
apply lands, the pipeline reports back and the plan's status moves to
applied. No browser button ever executes DDL.
Audit log
The cloud-api's audit log records the events it observes — who read the
migrations tab, who submitted / approved / rejected each plan (the
migration_approvals table is the immutable decision trail, stamped
with signedBy + decidedAt), and (via the customer app's own history)
which plans were applied and when.
What's NOT in the cloud UI
- Apply button — see above
- Rollback button — there is no per-plan rollback anywhere (the runtime has no auto-rollback for planner plans — see Rollback), so the dashboard surfaces none
- Inline plan editing — plans are immutable; correct via a new PR
- Cross-app comparison — each app's tab is independent
Permissions
Reading the Migrations tab requires the canViewMigrationDetails
capability + caller-owns-tenant on the app. The framework's Subject
model + per-org role mapping carries this; the dashboard hides the tab
from users who lack the cap. Signing an approval decision is gated
further: only org owner / admin can call migrations.approve — the
server enforces this regardless of which buttons the UI renders.
Comparison with local devtools
| Local devtools | Cloud dashboard | |
|---|---|---|
| Transport | HTTP fetch (direct) | Cloud-RPC subscribe (proxied) |
| Auth | none (local-only) | session + tenant scope + per-app cap |
| Multi-env | no (per app at a time) | yes (tabs per env) |
| Apply | cap-gated (single-user) | NEVER |
| Approval workflow | no | submit → review → approve (owner/admin) |
| Audit log | local log buffer | persisted in cloud-api |
| Drift alerts | banner only | banner only (in-app) |
| Live updates | 10s polling | push via subscription |
Source code
- Component:
voltro/packages/devtools-ui/src/pages/MigrationsPage.tsx— shared - Cloud wiring:
voltro-cloud/apps/voltro-cloud/dashboard/src/pages/_/p/[orgSlug]/[projectSlug]/apps/[appSlug]/migrations.tsx - Cloud-api proxy:
voltro-cloud/apps/voltro-cloud/api/queries/apps.inspectMigrationsStatus.query.ts+.query.server.ts - Approval schema:
voltro-cloud/apps/voltro-cloud/api/database/migrationPlans.entity.ts+migrationApprovals.entity.ts - Approval rpc:
migrations.submitPlan+migrations.approve(mutations),migrations.pendingApprovals(query),migrations.reviewByToken(action) - Review-URL signing:
voltro-cloud/apps/voltro-cloud/api/lib/migrationReviewUrl.ts(HMAC-SHA256 over{planId, exp},VOLTRO_CLOUD_SECRET/VOLTRO_SESSION_SECRET)