Drift detection

How the framework detects schema drift (live DB ≠ last applied fingerprint), what causes drift, and how to reconcile it — with the planner's introspection, or via corrective plan, or by accepting + re-baselining.

Drift = the live database's schema doesn't match the fingerprint of the last applied voltro db apply plan. It's a passive detection — the framework only knows about drift after introspecting + comparing fingerprints. The detection itself is cheap (one COUNT + one fingerprint compare per check); the reconciliation path depends on cause.

How drift gets detected

Three trigger paths:

  1. Manualvoltro db drift runs the check on demand, exits 4 on drift
  2. On boot (dev) — every voltro dev boot runs the planner, which detects drift implicitly (the plan will be non-empty)
  3. Periodic (cloud) — the cloud dashboard polls each app's /_voltro/inspect/migrations endpoint; drift state is in the response

All three paths produce the same DriftSnapshot shape:

{
  isDrifted: boolean,
  liveFingerprint: string,             // current introspected user-schema fingerprint
  lastAppliedFingerprint?: string,     // newest _voltro_migration_plans row's fp
  lastAppliedAt?: string,              // when it was applied
  lastAppliedId?: string,              // plan id
}

isDrifted: falseliveFingerprint === lastAppliedFingerprint.

User-schema only: the fingerprint excludes _voltro_* framework tables. Bookkeeping tables grow rows + change row counts as the app runs; including them in the fingerprint would make every boot look drifted.

Common causes

Manual DDL

Someone ran ALTER TABLE ... or CREATE INDEX ... via psql / DataGrip / Adminer instead of the framework. The live DB has changes the planner's history doesn't reflect.

$ voltro db drift
db drift: live schema DIVERGED from last applied state
  last applied: 8f507ba1e1aadad5  at 2026-06-15 14:32:00  (plan_mig_5k78)
  live now:     a8f2c9d10b3f4e62

Probable causes:
  - manual DDL ran out-of-band (psql session, another tool)
  - someone applied a plan but the row never made it into _voltro_migration_plans

To reconcile, run `voltro db plan` to see what your code expects vs the live DB.

Out-of-band auto-applier

Multiple tools applying to the same DB (the framework + a separate Flyway / Liquibase process / hand-written deploy script). The other tool's changes don't go through _voltro_migration_plans.

Truncated history table

_voltro_migration_plans was truncated, restored from a backup, or the DB was restored to a point-in-time before the latest applies. The live schema is post-apply but the table doesn't know it.

Replica fingerprinted instead of primary

The drift detector ran against a read-replica that's lagging. Wait for the replica to catch up + re-check. (The framework's drift detector targets primary by default; this only bites when the user explicitly points the check at a replica URL.)

Reconciliation paths

Path 1 — adopt the live state by declaring it in TS

When the live DB IS what you want (the manual DDL is correct, only bypassing the planner was sloppy), bring the declared schema up to the live shape: edit the *.entity.ts files so they describe exactly what the live DB now has. The next voltro db plan then diffs empty, and a voltro db apply records a fresh _voltro_migration_plans row at the new fingerprint — re-baselining history without any DDL.

voltro db plan    # confirm the diff is now empty (declared == live)
voltro db apply --note 'accepting manual DDL from 2026-06-15 — see ticket #789'

voltro db apply with an empty plan writes no DDL; it just locks in the current fingerprint with your note. The history shows it:

plan_mig_5k79  fp=a8f2c9d1  env=dev  src=auto-diff  0 op(s)  12ms  ...  by=alice
  [note: accepting manual DDL from 2026-06-15 — see ticket #789]

There is no metadata-only --reconcile flag — re-baselining always goes through the declare-then-apply loop, so the TS schema stays the single source of truth.

Path 2 — corrective plan against drift

When the live DB has accumulated cruft + the declared schema is what you want:

voltro db plan         # see the diff between code + live
voltro db apply        # execute the diff, removing the drift

The plan diff will show the corrective ops:

schema diff: 2 operations, 0 blocked

  ✗ DROP INDEX manual_idx_we_forgot_to_remove  # safe (no FK depends on it)
  ⊕ ALTER TABLE users ADD COLUMN missing_field text  # backfill: sql`'default'`

  fingerprint: a8f2c9d10b3f4e62 → 8f507ba1e1aadad5

Apply lands the corrections + the new fingerprint matches the declared schema.

Path 3 — declared schema needs updates

The live DB has a column the declared schema doesn't reference, and you WANT to keep that column in the schema. Update the schema TS file to add it:

// users.entity.ts
export const users = table('users', {
  id: id(),
  email: text(),
  extra_field: text().nullable(),   // add to declared
})

Now the live shape matches the declared shape after the next plan (which will be empty). The drift "fixed itself" through code changes.

Drift on prod

Production drift is the most important to catch quickly because it suggests an unauthorised change to the production DB. The cloud dashboard's drift detector runs every 5 minutes against each customer's prod app + surfaces the divergence as soon as it appears.

Surfaces:

  • Dashboard banner on the affected app's Migrations tab
  • Audit log row tagged drift.detected

Out-of-band channels (Slack, email, PagerDuty) are intentionally NOT in the framework. Subscribe to the audit log via your existing observability stack — every drift event is a row your SIEM / monitoring already consumes, and your team's incident process kicks in from there.

The org's incident response process kicks in from there. Common immediate actions:

  1. Check the audit log for any non-CI DB access
  2. Run voltro db drift against a snapshot to confirm the divergence (the check is a structural fingerprint compare — it tells you THAT the schema diverged, not which rows changed)
  3. Decide: corrective plan or re-baseline?

What the dashboard shows

The Migrations tab's drift banner renders when isDrifted: true:

⚠ Schema drift detected
  last applied: 8f507ba1e1aadad5  at 2026-06-15 14:32:00
  live now:     a8f2c9d10b3f4e62

  → Run `voltro db plan` to see what your code expects vs the live DB.

Click → expands to a comparison view showing the introspected live shape + the declared shape, highlighting the divergent tables. (Cloud dashboard only; local devtools shows just the banner without the comparison view.)

What about replicas?

Each replica has its own catch-up state. The framework's drift detector compares against PRIMARY by default; replicas catch up via the normal replication stream + reach the same fingerprint within their lag window.

If you specifically want to monitor replica drift (rare; mostly relevant during major maintenance windows), the cloud dashboard's Settings page allows enabling "Replica drift monitoring" which polls each replica URL separately + alerts on lag > N minutes.

Drift after rollback

voltro db rollback itself records a new row in _voltro_migration_plans, so the fingerprint of that row matches the post-rollback state. No drift gets reported as a side-effect of rollback.

If something else changed the live DB between the original apply + the rollback, that drift was already present + the rollback doesn't surface it differently. Run voltro db drift after rollback to confirm reconciliation if you're suspicious.

Detecting drift is the easy part

The hardest part of drift response is figuring out what changed + who did it. The framework can tell you that the fingerprints differ + show the structural diff. It can't tell you who ran the DDL or why. Pair the framework's drift detector with:

  • Database audit logs (Postgres pgaudit, MySQL audit plugin, MSSQL Audit, SQLite no-op)
  • Network access logs (who reached the DB during the drift window)
  • Application logs filtered by trace id (if the drift happened during a request, trace shows the caller)
  • The team's normal incident response (Slack channel for accidental changes, post-mortem cadence)

TL;DR

Detect:    voltro db drift
Fix code:  voltro db apply   (apply corrective plan from current diff)
Adopt DB:  edit the *.entity.ts to match live, then voltro db apply (empty plan re-baselines)
Backup:    if data was lost, restore from your DB backup system — the framework can't help