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.

The external app inspection is a snapshot. Use Refresh views after a pipeline change to fetch the current migration state. The separate Cloud approval queue is reactive because its rows live in the Cloud database; that does not make an external inspect request 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

The inspector shows one registered app URL at a time. Use Cross-environment diff to compare that app's declarations with another environment's SQL schema. Enter its URL and, when required, its inspect read token. No schema is applied; the resulting operations and blocked fixes appear immediately. The app retains up to 20 results in its serving process until restart.

Owners and admins can start a comparison when both inspect credentials are configured for the inspected app. Members can read retained results. A remote fetch failure is shown explicitly; it is never presented as a matching schema.

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
  • Combined cross-app migration plan — each comparison has one source app and one remote schema

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 app inspect credentials session, tenant membership and app inspect credentials
Multi-env per-app remote schema comparison per-app remote schema comparison
Apply CLI / operator pipeline CLI / operator pipeline
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)
External inspect updates serial polling, 4-second interval Refresh views; comparison results appear immediately

Source code

  • Component: voltro/packages/devtools-ui/src/pages/MigrationsPage.tsx — shared
  • Cloud wiring: voltro-cloud/apps/voltro-cloud/dashboard/src/pages/_/p/[orgId]/[projectSlug]/apps/[appSlug]/migrations/page.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)