Migrations and recovery
How CapsuleDB migration history changes over time, what each startup state means, and how to recover from drift, failed preparation or removal.
CapsuleDB treats migration history as an append-only contract shared by the author and host. The runtime ledger records the capsule ID, migration ID, name, checksum, dialect, and application time. The metadata row records the complete manifest fingerprint and provider.
Checksums cover one dialect
A migration’s checksum covers only the body the host actually applied. A PostgreSQL host verifies the PostgreSQL body; adding a SQLite body for a new engine, or fixing SQLite SQL, leaves that checksum untouched. This is what makes it safe for a library to grow engine support after it has shipped:
| Change | PostgreSQL host | SQLite host |
|---|---|---|
Add a sqlite body |
Ready | Pending |
Edit the postgres body |
Drift | Ready |
Edit the sqlite body |
Ready | Drift |
| Add the next contiguous migration | Pending | Pending |
MigrationChecksumDrift and LedgerConflict name the dialect whose body
diverged, so an operator does not have to guess which engine is affected.
Upgrading a CapsuleDB 0.1 ledger
Manifest v1 hashed every dialect body of a migration together, which is the behavior per-dialect checksums replace, and a v2 runtime cannot reproduce a v1 checksum. Re-keying such a row therefore trusts its logical identity — capsule, migration ID, and name — rather than its content, which is a weaker guarantee than every other row gets. It is an operator decision, not a silent one:
Registry.layer({ provider: Pg.profile, capsules, allowLegacyLedgerUpgrade: true });
Until you set it, a v1 row reports Drift and preparation fails with
LegacyLedgerUpgradeUnauthorized, leaving the original checksum in place.
Confirm the applied history is the one your code still describes, then opt in
once: the first preparation adds the dialect column, rewrites each pre-existing
row to the checksum of the body this host applies, and logs what it rewrote. A
row that already has a dialect is never touched again, and a row whose name no
longer matches still fails closed.
The upgrade is safe to interrupt. On a provider with transactions the rewrite
rides in the same transaction as the rest of that preparation. On D1, which has
none, it runs after every other write, so a failure leaves the original rows
intact. Either way a row that has not been re-keyed yet is never counted as
ready: status reports Drift and assert mode refuses to boot until one more
preparation finishes the job.
Normal change flow
- Add the next contiguous migration ID in the capsule module.
- Keep the name lowercase and stable; classify the risk as
additiveordestructive. - Declare the change once with
Migration.createTable,addColumn, orcreateIndex, or drop toMigration.sqlfor engine-specific statements; useMigration.effectonly for transactional providers that support it. - Check the manifest and, for D1, regenerate and check optional artifacts.
- Deploy the host and build
Registry.layerbefore exposing capsule services, or prepare during the deploy (thecapsuledb/alchemyresource orRegistry.prepare({ provider, manifest })) and boot withmode: "assert".
bun run capsuledb -- manifest check \
--module ./src/capsules.ts \
--export capsule \
--manifest ./capsuledb.manifest.json \
--json
bun run capsuledb -- d1 check \
--module ./src/capsules.ts \
--export capsule \
--artifact ./d1-artifacts \
--json
Changing an applied migration’s ID, name, or rendered statement sequence changes its checksum and fails closed. Create a new migration instead of editing history. A gap or reordered history is rejected before provider state is touched.
Startup states
| State or failure | Meaning | Host action |
|---|---|---|
Pending |
No complete active history is recorded yet. | Run prepare in the startup phase. |
Ready |
Fingerprint, provider, and every active ledger row agree. | Expose the service layer. |
Drift / NotReady |
Metadata or ledger does not match the current registry. | Keep the host unavailable; inspect the history and rerun prepare. |
DestructiveMigrationUnauthorized |
A pending destructive migration was found without explicit authorization. | Obtain deployment approval, then pass allowDestructive: true. |
DatabaseAhead |
The active database has a migration absent from registered code. | Restore the exact code or investigate before changing the registry. |
LedgerConflict / checksum drift |
Applied name or body differs from the registered history. | Do not edit the applied entry; restore matching statements or plan a new migration. |
PartialMigration |
Ledger and readiness metadata describe an incomplete state. | Keep services hidden and repair from a backup/provider-specific runbook. |
ProviderMismatch |
Persisted metadata was produced for another provider profile. | Use the original provider or perform an explicit, reviewed migration. |
D1-specific recovery
D1 preparation validates every pending claim-first batch before applying the first one. Each migration then runs as one atomic batch, with no interactive transaction or savepoint fallback. If a batch fails, the claim and its static SQL must roll back together; retry only after the provider error is understood. Dynamic Effect bodies and over-limit batches are definition failures, not a signal to split work into multiple untracked batches.
Optional SQL artifacts can be deleted and regenerated from the same module and
manifest. d1 check is the guard against stale, edited, missing, reordered, or
unsupported files. Artifact regeneration never substitutes for runtime
preparation or ledger verification.
Removal and recovery of a capsule
An empty or reduced explicit registry does not drop the removed capsule’s tables or ledger rows. This preserves package-owned data. Re-registering the same capsule ID with its unchanged history allows the runtime to verify and reuse the existing rows. Treat a changed ID as a new namespace and do not use it to rename a capsule in place.
Backups and authorization
CapsuleDB does not provide backup, rollback, DDL diffing, or destructive-change approval. The host owns those operational controls. Take the provider’s normal backup before an approved destructive migration, run preparation in a controlled startup/deployment phase, and retain the readiness receipt and manifest fingerprint with the deployment record.