also looking at this
fix(fleet): carry the on-box database when a site moves
#178The hole
site:move moved the site's tree and left an on-box engine database behind.
- A SQLite database rides along already — it lives under
shared/, which is the whole point ofsharedPaths. - An external database (RDS, a managed host) needs nothing — the target reaches the same endpoint the source did.
- An on-box Postgres or MySQL database is neither. Its data lives in the engine's own directory, which belongs to the box rather than to the site.
So the move passed every check it had and was still wrong. The app started on the target, answered its health gate on loopback, took the DNS cutover — and served production an empty database, with the real rows still sitting on a box the operator was about to delete. Nothing in the plan could have caught it, because every signal it looks at was green.
The fix
The database is carried explicitly, as three resumable steps folded into the existing plan:
pause-workers → database-dump → snapshot → transfer → database-transfer
→ database-restore → restore → health → gateway → dns → drain-sourceThe dump is taken while background work is stopped, so it is consistent for the same reason the tree snapshot is. It is loaded before the app starts on the target, so the app's first request finds its data.
Every part of it is a script something else already uses — buildBackupScript for the dump, buildDatabaseSetupScript for the role and schema, buildBackupRestoreScript for the load — so a moved database is built exactly like a provisioned one rather than by a second mechanism that drifts.
Two preconditions, not steps
Both are answers only the operator can give, so they refuse to build a plan rather than appearing as a step with a flag on it:
- A project with an on-box database and no way to carry it refuses to move at all. Moving half an application is worse than not moving it.
- A target that already holds a database of that name with tables in it refuses too. Loading a dump over live data is the one genuinely destructive thing this operation could do, and a
--confirmflag is the wrong shape for "you are about to overwrite someone else's database" — rename or drop it first, or point the project at a different name.
The dump is re-taken on a resume, for the same reason the tree snapshot is: one from an earlier attempt predates whatever the source has committed since, and shipping stale rows is worse than dumping twice. The transfer still resumes normally.
Verification
bun test — 4124 pass, 0 fail. Typecheck and lint clean.
7 new cases: the refusal with no carrier, the refusal when the target holds data, step ordering (dump after the pause, restore before the app starts), end-to-end movement, re-dumping on resume while the transfer skips, no database steps at all for an external database, and the source left serving when the restore fails.
Standing caveat from #176, now more pointed: this has been exercised at the unit level with injected effects, not against two live boxes. The remote scripts and the scp hops still deserve a rehearsal on throwaway servers before the first real production move.
4 changed files on the files tab, with 0 review threads.