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 dashboard

Three 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 plan first → review → deploy isn't the safety story the docs promise.
  • For prod specifically: the rule is voltro db apply runs as an explicit deploy step (and refuses on NODE_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 step

The 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:

  1. Reviewer opens the PR's preview env (dev) tab → confirms the plan ran cleanly there
  2. Reviewer switches to staging → sees the SAME fingerprint applied → fingerprint chain looks healthy
  3. 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)