Database & migrations
Chorus uses an append-only, checksummed migration ledger. The numbered files in
src/db/schema/ are migrations: each exact file is committed once, recorded, and skipped on
later starts. Chorus currently supports SurrealDB 3.2.4 with surrealdb JavaScript SDK
2.0.4; Compose and CI pin those versions rather than floating on a major tag.
Startup sequence
Section titled “Startup sequence”After connecting, Chorus performs these steps before bootstrap, workers, or the HTTP listener:
- Load every
.surqlfile whose name matchesNNN-lowercase-name.surql. - Require unique versions contiguous from
000, and hash the exact UTF-8 bytes with SHA-256. - Idempotently bootstrap
schema_migrationplus the singletonschema_migration_state:singleton(ledger format 2), transactionally persisting whether the pre-bootstrap database was an emptyfresh_installor a populatedlegacy_adoption. - Require the database ledger to be an exact prefix of the binary catalog.
- Before the first pending migration in
066–069, require the epoch-specific quiescence acknowledgement when the initial ledger belongs to an existing schema below069. - Before pending migration
075on an existing database, require its separate one-start quiescence acknowledgement. - Apply each pending whole file in its own transaction.
One migration transaction executes the file, compare-and-sets the singleton version, and creates its ledger row containing filename, checksum, app schema version, build commit, and timestamp. All three effects commit together or roll back together. Whole-file execution also means semicolons in comments or string literals are no longer split by application code.
Competing startup replicas write the same singleton and deterministic migration record. A verified SurrealDB write conflict retries the complete transaction with bounded full jitter; after one replica wins, the other observes the matching ledger row and skips it.
Fail-closed checks
Section titled “Fail-closed checks”Startup aborts before serving when any of these is true:
- a filename is malformed, duplicated, or leaves a version gap;
- an applied filename or checksum differs from the binary;
- the database ledger is not an exact prefix, is internally inconsistent, or is ahead;
- the ledger format is unsupported;
- migration SQL, schema coercion, permissions, or an explicit
THROWfails; - a domain/state conflict is not a verified retryable database write conflict.
Never edit or rename an applied file. A comment change changes its checksum.
Required quiescence for migrations 066–069
Section titled “Required quiescence for migrations 066–069”Migrations 066, 067, 068, and 069 install database-enforced graph, memory-admission,
workstream-link admission, and approval-action integrity protocols. An old Chorus process does not
write the new protocol fields, so allowing it to remain connected during this transition can create
rows that the new binary must reject. The startup guard therefore fails closed whenever migrations
066–069 are pending,
except for a runner whose initial ledger snapshot is both
initialization_origin=fresh_install and current_version=-1. A populated pre-ledger database is
durably classified as legacy_adoption, even though its first ledger version is also -1, and
requires the acknowledgement before any numbered migration runs. A database originally installed
fresh but later upgrading from 065, 066, 067, or 068 requires it as well.
Concurrent runners that both snapshot a completely unapplied fresh ledger may race safely. A
lagging runner that first observes partial progress fails closed at the unsafe boundary and can be
restarted after the winning runner records 069. Once 069 is recorded, restarts no longer need
the acknowledgement.
The acknowledgement is an exact, non-secret operator assertion—not a password and not a mechanism that disconnects writers. Use this sequence:
-
Back up SurrealDB and record the current migration-ledger version.
-
Stop every old Chorus database writer: API replicas, embedded or standalone workers, dispatchers with direct storage access, cron/maintenance processes, and one-off scripts. Verify that no old Chorus process retains a SurrealDB connection. Keep SurrealDB itself running.
-
Set the following value on exactly the new replica that will perform the upgrade:
CHORUS_MIGRATION_069_QUIESCENCE_ACK=I_HAVE_STOPPED_ALL_OLD_CHORUS_DB_WRITERS_FOR_MIGRATIONS_066_TO_069Any other non-empty value, including surrounding whitespace or a shortened confirmation, aborts configuration. A missing value aborts before the first unsafe schema transaction.
-
Start one new Chorus replica. Wait for the schema log to report
Ledger current=69(or newer) and for health checks to succeed before starting any additional new replicas. Review the migration-069 preflight counts first: fingerprint-valid terminal history may remain, but executable legacy gates without both a committed execution lease and a covering token TTL are snapshotted with their dependents inapproval_lifecycle_quarantine. The migration never synthesizes those values; create a fresh gate after upgrade when the action is still needed. -
Remove
CHORUS_MIGRATION_069_QUIESCENCE_ACKfrom the environment and restart/scale the new replica set normally. Do not leave the assertion permanently configured or reuse it as a general migration bypass.
For Compose, put the exact value temporarily in the deployment .env, start only the chorus
service at one replica after the old writers are down, confirm the ledger line, then delete the
value from .env before scaling out. If startup aborts while the ledger remains below 069, keep
old writers stopped, correct the problem, and retry the same bounded sequence. If a migration has
committed, prefer forward repair; do not restart an old writer against the advanced schema.
Required quiescence for migration 075
Section titled “Required quiescence for migration 075”Migration 075 changes Inbox receipts and the durable stream contract. Old Chorus writers must
not write the affected tables while it commits. Back up SurrealDB, stop every old API, worker,
dispatcher, maintenance job, and one-off process with a DB connection, then set this exact value on
one new Chorus replica:
CHORUS_MIGRATION_075_QUIESCENCE_ACK=I_HAVE_STOPPED_ALL_OLD_CHORUS_DB_WRITERS_FOR_MIGRATION_075Start only that replica and wait for Ledger current=75 plus a healthy readiness check. Then remove
the variable and restart the new replica before scaling or reconnecting clients. Chorus rejects the
acknowledgement when 075 is not pending, so it cannot be left in Compose as a standing bypass. If
startup fails before 075 commits, keep old writers stopped, repair the cause, and retry the same
bounded step.
First ledger adoption
Section titled “First ledger adoption”On an existing pre-ledger database, ledger bootstrap atomically records
initialization_origin=legacy_adoption. Because the current catalog includes the unsafe
066–069 transition, the quiescence acknowledgement is required before the runner applies even
000; a missing acknowledgement leaves the numbered ledger empty. With the guard satisfied, the
binary applies and records the complete contiguous catalog in order. Migrations 000–040 are
the one-time adoption baseline. Back up first and treat that start as a migration window. In
particular:
024performs its legacy memory-type normalization scans one final time;025clears wrong-dimension embeddings and rebuilds the 1024-dimensional HNSW index one final time;- subsequent starts execute neither file.
The legacy DML returns NONE, so those scans do not materialize every changed record into the
startup process. Re-populate cleared embeddings with
bun run reembed -- --namespace <namespace> when required.
Fresh install, configured bootstrap, and upgrade
Section titled “Fresh install, configured bootstrap, and upgrade”| Path | Behavior |
|---|---|
| Fresh | Apply and record the full catalog, then zero-config creates the local admin only when no configured bootstrap exists and storage is empty. |
| Configured bootstrap | Migrations commit first. A missing, invalid, or unsuccessful CHORUS_BOOTSTRAP then aborts startup before listening. |
| Restart | Validate every recorded checksum and skip the exact applied prefix. |
| Upgrade | Back up, deploy code with only new highest-numbered files, and let startup commit pending migrations before accepting traffic. |
For the executable Compose backup, restore, destructive confirmation, and disposable rollback drill, see Backup, restore & rollback.
Migration 041-enrollment.surql backfills and revokes every unredeemed pre-v2 invite, then
enforces one org, one central membership, and the durable configured-bootstrap singleton. Legacy
codes are intentionally not grandfathered; create a new invite after upgrading.
Migration 042: strict SIWE and credential inventory
Section titled “Migration 042: strict SIWE and credential inventory”Before applying 042-siwe-challenge.surql, startup emits a read-only inventory such as:
[schema] Preflight 042-siwe-challenge.surql: {"affected_api_keys":2,"quarantined_api_keys":1,"revoked_api_keys":1,"uppercase_wallet_rows":1,"wallet_casefold_collisions":0}affected_api_keysis every legacy key missing explicitkindorissued_viametadata.quarantined_api_keysandrevoked_api_keysare the same fail-closed set: active expiring credentials with no trustworthy issuance provenance.uppercase_wallet_rowsinventories identity, auth-request, and API-key wallet values that will be normalized to lowercase.wallet_casefold_collisionsmust be zero.- If two identity rows collapse to the same lowercase wallet, startup aborts with the canonical wallet and conflicting identity IDs before mutation. Resolve ownership explicitly and retry; the migration never chooses an identity on the operator’s behalf.
- Permanent legacy keys remain active but are labeled
kind=permanent, issued_via=legacy_migration. - Unsafe expiring keys are revoked with a timestamp/reason and labeled
kind=session, issued_via=legacy_revoked_by_siwe_042.
The migration then normalizes wallet-bearing rows, enforces lowercase identity/auth-request/key
fields plus the unique identity-wallet index, creates the purpose-bound challenge table, deletes
every legacy free-standing nonce, and removes the nonce table. Invite challenges persist only a domain-separated digest,
SHA-256("chorus-invite-v2\\0" + code); raw codes are neither stored in the challenge nor echoed
in errors.
Operational sequence:
- Back up SurrealDB and capture the preflight line before the migration window. Stop if
wallet_casefold_collisionsis nonzero and resolve every listed identity pair first. - Notify owners that listed sessions will stop working immediately after 042 commits.
- Reissue wallet sessions through
POST /auth/challenge(purpose=login) followed by the exact signed message atPOST /auth/siwe-login. Reissue non-wallet automation credentials through an authenticated admin key create/rotation workflow; do not un-revoke quarantined hashes. - Confirm new keys have explicit
kindandissued_via, then verify the old key fails over both HTTP and stateless MCP.
If 042 has not committed, fix configuration/code and restart. If it has committed, a code rollback does not and must not reactivate revoked credentials or resurrect the nonce table. Prefer forward repair and reissue. The only full data rollback is restoring the pre-042 backup, which also rolls back every later write; use it only during a coordinated outage.
Migration 043: generation-bound leases
Section titled “Migration 043: generation-bound leases”043-generation-leases.surql expands signal, task, run-attempt, and dispatcher rows with an
immutable lease generation plus acquisition, heartbeat, and expiry timestamps. It also creates
the durable lease_rollout_state:singleton operator record and append-only quarantine records.
The migration deliberately does not invent generations or rewrite active legacy leases.
New acquisitions negotiate Chorus-Lease-Version: 1; an unnegotiated HTTP client receives 428
before storage is called. Every holder mutation must present the exact owner and generation minted
for that acquisition. Reacquiring as the same identity produces a different generation, so a
delayed heartbeat, ack, cancel, release, terminate, or dispatcher shutdown from the old process
cannot affect replacement work. Expiry decisions use database time, and reapers compare a bounded
snapshot of all lease fields before mutating.
Use this resumable rollout after 043 is applied:
# 1. Back up the database; stop/upgrade every old binary and verify it is gone.chorus admin leases inventorychorus admin leases quiesce
# 2. Wait until the recorded wait_until. The protocol minimum is 14,400,000 ms (4 h).# 3. Drain bounded pages until remaining is exactly zero; repeating is safe.chorus admin leases drain --limit 100chorus admin leases status
# 4. Verify, then enforce. Enforce performs a fresh legacy inventory.chorus admin leases verifychorus admin leases enforceAcquisition is disabled from quiesced through verified and resumes only after enforced.
drain cannot start before the durable wait_until, and a caller cannot configure a wait below
the protocol maximum of four hours. Legacy active rows are terminalized into quarantine; they are
never requeued and never assigned a guessed generation. If verify or enforce finds a legacy row,
drain again or find the old process that recreated it. Each step advances a durable revision, so an
operator can inspect status and resume after a crash without restarting the sequence.
Migration 046: federation v1 quarantine and v2 authority
Section titled “Migration 046: federation v1 quarantine and v2 authority”Before applying 046-federation-v2.surql, startup inventories peer rows carrying legacy raw
secrets, hashes, or scopes:
[schema] Preflight 046-federation-v2.surql: {"legacy_peers":1,"plaintext_secret_peers":1,"hashed_secret_peers":1,"scoped_peers":1}The migration deliberately does not convert those rows into trusted v2 peers. In one transaction
it copies only the old hash/scopes/expiry into metadata.federation_v1_quarantine, erases the raw
secret and every v1 authority field, creates encrypted-key/trust/request/receipt tables, and adds
dedicated signal provenance. If any DDL or DML fails, both quarantine and erasure roll back.
After commit, v1 calls cannot authenticate and code rollback must not be used to revive them. Create reciprocal v2 trust explicitly: one instance mints generation 1, the other installs it over an authenticated operator channel, and each stores only AES-GCM ciphertext. See Federation v2 for establishment and rotation. If immediate rollback is unavoidable, restore the coordinated pre-046 backup; that also discards all later writes.
Migration 047: authoritative task graph
Section titled “Migration 047: authoritative task graph”Before applying 047-authoritative-task-graph.surql, startup inventories legacy blocked_by
and blocks projections:
[schema] Preflight 047-authoritative-task-graph.surql: {"legacy_tasks":6,"candidate_dependencies":5,"valid_dependencies":2,"unsafe_dependencies":3,"missing_task_dependencies":1,"cyclic_task_dependencies":2,"graph_limit_exceeded_entries":0,"dependency_limit_exceeded_entries":0,"dependent_limit_exceeded_entries":0,"ambiguous_projection_entries":1}Safe dependencies become durable task_dependency relations, up to 100 per dependent task.
Missing, cyclic, invalid, cross-ring, duplicate, over-limit, and ambiguous reverse-projection entries go to
task_dependency_quarantine; bounded traversal exhaustion uses graph_limit_exceeded, distinct
from a proven cyclic_task_dependency. The migration then rebuilds every task projection from the
valid relations and records graph revision 1. DDL, quarantine, backfill, and projection rebuild
share one transaction. If any step fails, the ledger remains at 046 and legacy arrays remain unchanged.
Capture the preflight metrics and a database backup before rollout. Investigate every quarantine row after commit rather than manually restoring array fields: those arrays are read-only generated projections under the new runtime.
Migrations 049, 051, and 054: federated receipts and admission fencing
Section titled “Migrations 049, 051, and 054: federated receipts and admission fencing”049-federation-lease-receipts.surql adds the exact opaque lease generation to persisted request
and receipt rows. A federated peer must replay that signed generation on holder operations; it is
never reconstructed from identity or timestamps.
051-federation-claim-admission-fence.surql expands request outcomes with provisional, makes the
receipt optional until reconciliation, records finalization time, and creates the always-present
lease_acquisition_fence:singleton used by the first crash-safe admission protocol.
054-striped-lease-acquisition-fence.surql expands that serialization point into 128 fixed
stripes. A successful native or federated acquisition deterministically mutates one stripe in the
same transaction as its resource CAS; losing CAS attempts do not touch a stripe. Every rollout
phase transition mutates all stripes plus the legacy singleton atomically. Quiescence therefore
conflicts with an acquisition admitted against an older phase—even before an inventory row
exists—without forcing unrelated acquisitions through one global write hotspot.
A provisional row is deliberately not a successful replay result. After restart, the exact retry reconciles the persisted peer/signal/generation and produces one signed final receipt. If rollout quiescence or lease expiry won, reconciliation releases only the provisional generation and stores a rejection. Do not delete provisional rows manually: they are durable recovery state, and an operator should investigate any that remain old enough to exceed the configured claim TTL.
Migration 060: idempotent federation delivery
Section titled “Migration 060: idempotent federation delivery”060-federation-idempotent-delivery.surql gives each delivered intent a content and intent digest,
keeps exact- and idempotent-replay counters separate, and permits multiple proof-specific receipts
for one stable request intent. A caller-selected idempotency key is scoped to the authenticated peer
and principal: the same key plus the same canonical intent can safely recover after an ambiguous
transport failure, while a changed intent is rejected. Each fresh valid signed proof still receives
its own proof-bound receipt. Pre-060 rows have no intent digest and therefore remain exact-replay-only.
Migration 061: object-storage reconciliation
Section titled “Migration 061: object-storage reconciliation”061-object-storage-reconciliation.surql adds durable pre-write guards and generation-fenced
operation leases for uploads and deletes. An object write is prepared in SurrealDB before provider
I/O; metadata settlement and retry state survive process failure. Artifact deletion and tiered
memory-body deletion retain explicit pending markers until physical absence is confirmed. Memory
body keys include an immutable revision so a later content revision never aliases an earlier body.
Provider inventory is observational and fail-closed. Keys are classified as referenced, guarded, or quarantined across repeated scans; an unknown or legacy key is never deleted merely because no current database reference was found. Do not manually remove active guards or quarantined keys. Investigate the operation row and provider inventory, repair ownership/reference state, and let the reconciler settle it with the recorded lease generation.
Quiesce and drain every old writer before rolling out code that uses 061. Every provider mutation
must create its guard before provider I/O; mixed old/new writers weaken the evidence available to
reconciliation. Upload grace is five minutes, and a guarded PUT is abort-bounded to four minutes so
it remains at least 30 seconds inside that grace period. Delete grace is 30 seconds, reconciliation
dead-letters after eight failed attempts, and custom storage providers must honor AbortSignal.
The reconciler deletes only operation-owned unreferenced keys, verifies absence before terminal
settlement, and does not treat committed/completed history as an active guard. Quota authority,
protocol-version fencing, and durable inventory checkpoints are introduced by migration 063.
Migration 062: durable provider work
Section titled “Migration 062: durable provider work”062-durable-provider-work.surql moves embedding and LLM-extraction intent, attempt count, retry
time, lease ownership, and terminal state into SurrealDB. Memory creation and content revision write
the relevant provider intents atomically with the memory mutation. Workers claim one bounded item
with a fresh immutable generation; stale replicas cannot settle a replacement lease. Content drift
supersedes obsolete work, malformed provider payloads dead-letter, and retryable transport failures
use bounded backoff. LLM-derived memories and relations use deterministic IDs, so replacement
workers can repeat settlement without creating duplicates.
The admin health snapshot exposes fixed-cardinality counts for both work kinds. Treat
dead_letter > 0, expired leases, or a sustained pending + retry_wait increase as operational
signals. Terminal rows are recovery/audit evidence; do not delete them as a way to retry work.
Migration 063: object-storage quota authority and protocol fence
Section titled “Migration 063: object-storage quota authority and protocol fence”Migration 063-object-storage-quota.surql moves quota and unresolved-operation admission from
process-local reconstruction to SurrealDB-owned global and namespace authority rows. Each
protocol-v1 upload reserves bytes and unresolved capacity before provider I/O; settlement consumes
the exact reservation atomically with metadata. Pre-063 active operations become immutable
protocol-v0 dead-letter tombstones with accounting retained.
A database-clock, generation-fenced inventory lease persists each prefix cursor and commits page observations with its checkpoint in one transaction. Another replica can resume an incomplete scan without treating it as absence evidence. Inventory is observational: unknown objects are quarantined, never deleted merely because the current database has no reference.
An ambiguous upload tombstone remains charged even when a cleanup attempt observes provider absence, because a timed-out PUT can arrive late. A referenced delete dead letter instead recovers through an exact type-aware successor guard: metadata and unresolved ownership transfer atomically, provider deletion remains abort-bounded, and quota is released only after verified absence and generation-fenced settlement. Repeating that recovery is idempotent. Never delete guards, reservations, unresolved rows, inventory checkpoints, or tombstones manually.
After 063 commits, rolling back code alone is unsafe. The required, no-default
upload_protocol_version fence makes 061/062 writers fail before starting new provider I/O,
and protocol-v0 rows are immutable. Prefer forward repair. A full rollback requires a coordinated
restore of the pre-063 database backup and matching object-store state, discarding every later
write.
Migration 064: durable memory administration jobs
Section titled “Migration 064: durable memory administration jobs”Migration 064-durable-memory-admin-jobs.surql adds bounded durable import/export jobs, immutable
input/output chunks, 256 import lock stripes, generation-fenced leases and checkpoints, exact output
line/byte metadata, and database-owned capacity counters. It also upgrades any active legacy
ID-only re-embed scan to a fresh compound (created_at,id) database-clock snapshot.
Snapshot membership is fixed at admission; memory bodies and relations are read when a job processes each member, so an export is not a historical byte-for-byte snapshot. Job ownership, attempts, paging metadata, and terminal results survive restart. Use the admin job surfaces to resume or inspect work; do not edit chunks, checkpoints, counters, or lease fields directly.
Migration 065: durable delete-recovery links
Section titled “Migration 065: durable delete-recovery links”Migration 065-object-storage-delete-recovery.surql records each immutable delete tombstone’s
successor, root source, and eventual terminal operation. Resolution propagates through the durable
link chain before older source rows are eligible for bounded retention, preserving recovery proof
without keeping every ancestor operation forever.
Migration 068: bounded workstream links
Section titled “Migration 068: bounded workstream links”Migration 068-workstream-link-capacity.surql adds one internal serialization authority per
workstream and requires admission protocol v1 on every new link. Existing links are marked as
legacy v0 rows and remain deletable. New statements issued by an older client after commit cannot
create links, but a transaction already in flight can retain its pre-migration schema view. That
is why the rollout procedure requires every old writer to be stopped before migration rather than
relying on the field/event fence alone.
The first v1 link mutation lazily reconciles the indexed durable link count into the authority. At or above capacity, new links fail closed. Unlinking repairs the count transactionally and releases capacity one row at a time. Context reads independently fail on the 513th descriptor rather than truncate, protecting reads if legacy/manual writes left an over-capacity corpus.
Adding a migration
Section titled “Adding a migration”- Add the next highest contiguous migration number; never reuse or skip a number.
- Do not modify a migration that any environment may have applied.
- Put related DDL and DML in the same file when they must commit atomically.
- Prefer expand → backfill → contract across releases for destructive shape changes.
- Bump
CHORUS_SCHEMA_VERSION; CI’s schema-version guard enforces this. - Run unit catalog tests and required SurrealDB 3.2 integration tests. InMemory ledger tests prove orchestration only, not SCHEMAFULL enforcement or database contention.
Rollback
Section titled “Rollback”Migrations are forward-only. Chorus never deletes ledger rows and does not run automatic down migrations.
- A code rollback is safe only while the applied changes are backward-compatible.
- Destructive changes require a staged expand/backfill/contract rollout.
- If an incompatible migration committed, restore the pre-migration database backup.
The integration gate proves restart skip, failed DDL/DML rollback, correction of an unapplied file, two-runner contention, checksum drift rejection, and one-time application of the full catalog against a real pinned SurrealDB process.