İçeriğe geç

ADR-012: Migration And Release Safety

Geliştirme · 0.0.0-dev

Yayın

Doküman
0.0.0-dev
Uygulama
0.0.0

Bu sayfa

Uygulama
0.0.0
  • Status: Accepted
  • Date: 2026-08-18
  • Decision owners: database, release, security and provenance maintainers
  • Scope: application migration authority, applied-state evidence, recovery and release authorization

The accepted legacy Supabase public snapshot has no application-owned migration ledger. Migration filenames and date prefixes inventory source material, but do not prove which files ran, in what order, against which database, with which bytes or with what result. Munderecat therefore cannot inherit legacy history or treat a provider-owned migration table as project authority.

The new system also has stronger permanence requirements than a conventional application database. Core releases, public identities, shared anchors and accepted curation history cannot be edited or recreated to make a down migration convenient. Derived data can be rebuilt and caches can be dropped, but those permissions do not extend to immutable or curated authority.

ADR-002 requires append-only correction and historical identity. ADR-003 requires trusted-registry authorization and atomic publication. ADR-006 separates validation evidence from publication authority. ADR-007 keeps accepted curation and compensation append-only. ADR-010 makes API activation a locked compare-and-activate operation. ADR-021 defines prod_migrator as a non-login, non-bypass capability and forbids retained broad membership. MAT-001 fixes owner, mutation, rebuild and recovery classes. This ADR defines how migrations and complete releases cross those boundaries.

A migration definition is one immutable, ordered repository artifact claim. It binds an opaque migration ID, exact file bytes and SHA-256, predecessor chain root, transaction mode, declared write scope, expected object/permission diff, validation profile and recovery method. A filename is a locator, not order or application proof.

A migration chain is one branch-free ordered sequence of accepted definitions. Its root commits to every preceding definition. Reusing an ID, changing one byte, reordering, inserting into the middle or replacing the predecessor creates a conflict; it cannot create an equivalent chain.

A migration attempt is one append-only execution fact for one exact migration, release authorization and target identity. Start, checkpoints, observed result, failure, uncertainty and later reconciliation are events; mutable status is a derived projection.

A database application receipt is the private in-database proof of accepted application state. It mirrors successful definitions and transactional results so schema state can be checked locally. It is not release authorization and cannot prove failed attempts that rolled back outside its transaction.

A migration authorization is an externally verified, single-use approval to apply one exact candidate chain transition to one exact target state. A release attestation is the externally signed statement that the resulting code, schema, data, policy and documentation tuple passed its declared gates and may be activated. The authorization precedes mutation; the final attestation follows observed results.

A target identity is a versioned, non-secret fingerprint of the logical environment, database instance/recovery epoch and platform facts available under its profile. A database name, URL or current schema hash alone is not identity.

Migration safety uses two ledgers with different authority:

  1. The external release ledger is immutable control evidence outside the target database. It owns accepted migration definitions, chain roots, authorizations, every attempt event, final attestations, restore events and release-channel order.
  2. The private database receipt ledger records the exact chain installed in that database and participates in atomic application where the transaction mode permits. It detects local drift and uncertain outcomes but never authorizes a migration or release by itself.

The external ledger is append-only and hash chained. Each event binds its schema and policy version, event ID, previous event root, event kind, exact payload hash and retained artifact references. Insertion order, wall-clock order and database sequence values are not chain authority.

All roots and signed payloads in this ADR use the Munderecat canonical envelope profile v1. A producer first validates the artifact-kind-specific closed schema, rejects unknown keys, normalizes every string to Unicode NFC and serializes the value with RFC 8785 JSON Canonicalization Scheme as UTF-8 without a byte-order mark. Hashes are 64-character lowercase hexadecimal SHA-256 values; IDs and paths are NFC strings; timestamps are UTC YYYY-MM-DDTHH:MM:SSZ; counts and positions are non-negative JSON integers within the interoperable safe-integer range; optional fields are present as null; and set-valued arrays are unique and sorted by their canonical UTF-8 bytes. Ordered arrays retain declared order. Floating-point numbers, implementation-native timestamps, omitted optional keys and pre-normalized aliases are forbidden.

The preimage for an artifact kind is the exact ASCII bytes munderecat:<artifact-kind>:v1, one 0x0A byte and then its canonical JSON bytes. Its root is lowercase hexadecimal SHA-256(preimage). The external event envelope is exactly {schema, policy_id, event_id, previous_event_root, event_kind, payload_sha256, artifact_refs} in that profile, where schema is munderecat.external-ledger-event.v1; artifact_refs is a sorted unique array of closed {kind, locator, sha256} objects. Genesis uses the same preimage rule with artifact kind external-ledger-genesis and exact envelope {ledger_id}. Its resulting root is the first event’s previous_event_root; ordinary events use artifact kind external-ledger-event.

There is one externally verifiable authoritative head for each ledger ID. Append is a serialized compare-and-swap that accepts an unused event ID only when previous_event_root equals that head and atomically advances the head to the new event root. Competing successors cannot both become authoritative; a rejected proposal may later be recorded as a new event against the then-current head, but cannot be spliced into history. A backend that cannot prove this non-equivocation property is not an accepted external ledger.

The database receipt ledger is also append-only after publication. An ordinary application, agent, curation executor, cache builder or database owner cannot insert, update, delete or truncate it. A current-state view may derive the installed head, but cannot replace historical receipts or external events.

The canonical definition inventory contains at least:

  • migration ID and zero-based position in one named chain;
  • exact repository path, byte count and lowercase SHA-256 of unmodified file bytes;
  • predecessor migration ID and predecessor chain root, or explicit genesis values;
  • transactional, non_transactional or backfill execution mode;
  • closed object/owner/permanence write-scope manifest and its root;
  • expected before and after schema, owner, grant, RLS, routine and extension diff roots;
  • required core, identity, curation and annotation invariant profile IDs;
  • compatibility-impact placeholder governed by F2-018;
  • validation/evidence profile and recovery method IDs; and
  • author packet, accepted review and source/provenance bindings.

The migration-definition envelope is exactly {schema, chain_id, migration_id, position, file_path, file_byte_count, file_sha256, predecessor_migration_id, predecessor_chain_root, transaction_mode, write_scope_root, expected_diff_root, invariant_profile_ids, compatibility_impact, validation_profile_id, evidence_profile_id, recovery_method_id, author_packet_id, accepted_review_root, provenance_ref_roots} under the canonical profile. schema is munderecat.migration-definition.v1; root fields use the profile’s SHA-256 encoding; transaction_mode is the closed mode enum; ID/root arrays are sorted and unique; and F2-018 supplies the closed compatibility enum. The migration-definition root uses artifact kind migration-definition.

For position zero, predecessor_migration_id is null and predecessor_chain_root is the root produced by artifact kind migration-chain-genesis and exact envelope {chain_id}. Every accepted position n derives its link from artifact kind migration-chain-link and exact envelope {chain_id, position, predecessor_chain_root, migration_definition_root}. The current migration-chain head is that last accepted link root, or the genesis root for an empty chain. One chain ID has at most one accepted definition at each position and one accepted successor per head.

The migration root therefore includes the exact file hash and all semantic fields above. A comment-only edit is still a different migration. Accepted or applied definitions are never amended, squashed, renamed in a way that changes their locator claim, or marked unapplied. A correction appends a new definition. A future squashed baseline is a new genesis chain with a new chain ID and an exact crosswalk to the historical chain; it does not erase the old ledger. F3 may publish closed JSON Schemas and storage columns for these envelopes but cannot change their field set, encoding or root derivation without a versioned successor profile.

Production migration has one active control-plane lane and one target-database lock. Both are required:

  1. A protected release workflow obtains a lease bound to candidate manifest hash, expected target identity/head and expiry.
  2. It verifies migration authorization outside the target database but does not yet treat it as consumed.
  3. It opens one short-lived release session and acquires the target’s singleton migration lock before reading authoritative preflight state.
  4. Under that lock it rechecks target identity, installed chain root, schema/ACL fingerprint, freeze epoch, pending attempt set and expected predecessor.
  5. While still holding the lock, it atomically appends an attempt_started event to the external ledger by compare-and-swap. That event binds an unused attempt ID, authorization root and single-use nonce, target identity/recovery epoch, candidate manifest, migration transition and both observed predecessor heads. Acceptance of this one event durably reserves the attempt and consumes the authorization.
  6. Only after receiving and re-verifying the accepted event root does it execute exactly the authorized bytes and publish database receipts according to the declared transaction mode.
  7. It performs postflight validation, closes/reconciles the external attempt and removes every transient credential or membership before releasing the lane.

Failure to append or confirm attempt_started is a pre-mutation failure: no SQL may run. Failure to append a result after SQL does not erase or reuse the consumed authorization. The durable started attempt remains unresolved, the migration lane is frozen and reconciliation compares the database receipt and full target fingerprint before appending exactly one terminal result. It never retries the SQL under the same or a replacement authorization merely because external result publication failed.

Neither layer substitutes for the other. A stale control-plane lease cannot wait and then apply against a new head; a database lock obtained without a valid external authorization cannot begin. Concurrent candidates against one predecessor produce at most one winner. The loser records a rejected/stale attempt without executing SQL.

prod_migrator remains NOLOGIN, NOINHERIT, non-admin and NOBYPASSRLS. It is the production execution capability, not an object owner or signing identity. The release broker may issue one short-lived, non-exportable session credential for the exact authorization. Any owner-scoped DDL mechanism must be closed over the manifest’s declared owner set and leave no usable login, membership, grant or default-privilege edge after postflight. F3 selects the PostgreSQL mechanism and must prove the ADR-021 steady-state role graph before activation; broad retained owner membership, superuser/service-role credentials and caller-supplied SQL are forbidden.

Release operators trigger the protected workflow but do not receive the credential. Agents, application roles and human accounts cannot connect as or delegate prod_migrator. The release operator cannot be the same release’s author and sole approver. F2-018 fixes the exact approval matrix.

Before SQL executes, the workflow must prove:

  • target identity and expected recovery epoch;
  • exact current external event root and database receipt chain root;
  • no unresolved or uncertain prior attempt;
  • exact schema object definitions, owners, grants, default privileges, RLS/policies, routines, extensions and required platform settings under a versioned fingerprint profile;
  • exact current code, API, core/data, annotation/curation and policy release bindings;
  • accepted migration authorization, signer policy and non-revoked authorization state; and
  • backup/checkpoint readiness required by the migration’s recovery class.

Any unexpected DDL, missing/extra receipt, edited migration byte, hash disagreement, wrong target, stale predecessor or unknown object blocks execution. The system does not insert synthetic historical receipts, mark unknown SQL as applied or accept the current catalog as a new baseline. Legitimate out-of-band emergency work is reconciled through incident evidence and a new reviewed migration/attestation chain.

transactional is the default. The exact migration statements, invariant checks and successful database receipt commit in one database transaction while the singleton lock is held. A statement, validation or commit failure leaves no authoritative schema change and no success receipt. The external ledger still retains the failed attempt.

If the client loses the commit result, outcome is uncertain, not failed. The workflow does not retry SQL blindly. A fresh read-only reconciliation compares target identity, receipt, chain root and complete postflight fingerprint, then appends a resolved-success or resolved-failure event. Any mixed or unexplained state freezes the lane.

non_transactional is an explicit exception for operations PostgreSQL cannot safely place in the required transaction. Its definition must include:

  • maintenance/read-write freeze and traffic behavior;
  • ordered idempotent checkpoints with exact expected state per checkpoint;
  • crash/fault injection evidence at every checkpoint boundary;
  • a deterministic resume, compensation or forward-fix path;
  • a final publication barrier that readers cannot cross early; and
  • stricter F2-018 review classification.

A failed non-transactional attempt may have changed the database. It is recorded as partial or uncertain, the target is quarantined and no release is activated until reconciliation plus an authorized recovery transition completes. Calling this state “rolled back” is forbidden.

backfill separates large data work from schema activation. It uses an immutable workset root, bounded idempotent chunks, exact checkpoint receipts and complete coverage/parity validation. Expand, backfill, verify and contract are separate migrations/releases when partial deployment or old-code compatibility matters. Backfill cannot update immutable core or curated historical rows in place; those domains require a successor release or accepted curation transaction.

One release candidate is not production-authorizable until retained evidence proves:

  1. Fresh empty install reaches the exact expected migration, schema, role and default privilege roots.
  2. Every supported predecessor upgrade reaches the same roots and expected data state.
  3. Replaying against an already installed exact chain is a no-op verification, while edited, missing, duplicate, reordered or unknown definitions fail.
  4. Actual object, owner, grant, RLS, routine, extension and exposure diffs equal the reviewed manifest; undeclared changes fail.
  5. Core reconstruction and immutable/curated authority roots are unchanged unless the candidate explicitly publishes their valid successor releases.
  6. Derived rebuilds reproduce parity roots and cache loss changes no authority.
  7. Transaction abort, process termination, timeout/uncertain commit, concurrent writer and stale predecessor fixtures follow the declared result paths.
  8. Every non-transactional checkpoint and backfill chunk passes fault, resume, completeness and no-early-publication tests.
  9. Backup restoration produces exact immutable/curated roots and a new declared recovery target identity.
  10. Postflight roles, memberships, owners, grants, default privileges, exposed schemas, RLS and routines match the closed security manifest with no transient credential.

Production authorization references exact evidence artifact hashes. A green CI label, mutable job URL, sampled row count or reviewer prose is not sufficient.

The release candidate manifest uses artifact kind release-candidate-manifest and the exact envelope {schema, release_id, channel, predecessor_attestation_root, source, build_artifacts, migration, target, components, contracts, validation, compatibility, signing}. schema is munderecat.release-candidate-manifest.v1. The closed members are:

  • source = {commit, tree} and build_artifacts is a sorted unique array of {kind, artifact_id, root};
  • migration = {definition_chain_root, prior_receipt_root, new_receipt_root};
  • target = {identity_profile_id, identity_root, recovery_epoch, expected_preflight_root, expected_postflight_root};
  • components is a sorted unique array of {kind, release_id, root} for core, identity, annotation, curation and other declared components;
  • contracts is a sorted unique array of {kind, contract_id, root} for API, OpenAPI, generated contracts, application and documentation artifacts;
  • validation = {profile_id, evidence_manifest_root, known_limit_codes};
  • compatibility = {impact_class, approval_policy_id} under F2-018; and
  • signing = {envelope_policy_id, key_registry_root}.

Before mutation, every approving signer signs the artifact kind migration-authorization-payload with exact envelope {schema, candidate_manifest_root, target_identity_root, recovery_epoch, expected_external_event_root, expected_database_receipt_root, valid_from, expires_at, single_use_nonce, approval_policy_id, approved_signer_ids}. schema is munderecat.migration-authorization-payload.v1, and each signer signs the same payload including the complete sorted approved-signer set. The authorization root uses artifact kind migration-authorization and exact envelope {schema, payload_sha256, signatures}, where schema is munderecat.migration-authorization.v1 and signatures is a sorted unique array of {signer_id, key_id, algorithm_id, signature_base64url} with unpadded base64url bytes.

After successful postflight, every final approver signs artifact kind release-attestation-payload and exact envelope {schema, candidate_manifest_root, predecessor_attestation_root, migration_authorization_root, attempt_started_event_root, attempt_result_event_root, target_identity_root, recovery_epoch, observed_receipt_ids, observed_receipt_root, observed_schema_root, observed_security_root, observed_component_roots, activation_eligibility, approval_policy_id, approved_signer_ids}. schema is munderecat.release-attestation-payload.v1; receipt IDs and signer IDs are sorted and unique; component roots are a sorted unique array of {kind, release_id, root}; and activation eligibility is a closed F2-018 value. The final attestation root uses artifact kind release-attestation and exact envelope {schema, payload_sha256, signatures} with schema munderecat.release-attestation.v1 and the same closed signature object shape.

Signatures therefore cover canonical profile bytes, and the retained root also binds the exact signature set. A displayed JSON document, database row or mutable tag cannot be substituted. An attestation missing any authorization, start, result, target or predecessor binding is invalid even when all observed roots happen to match.

Private signing keys remain outside Git, logs, artifacts and the target database. The target stores only non-secret receipt references and verified hashes. Verification uses a versioned algorithm/key policy and an immutable public-key registry snapshot. Key rotation appends a new key/policy state. Revocation records reason and effective scope; it never edits historical bytes. A compromised historical authorization causes incident review, channel freeze and successor/withdrawal evidence according to policy, not silent signature reinterpretation.

Exact threshold counts and separation-of-duty combinations belong to F2-018. This ADR requires at minimum that policy identity and satisfied signer set are bound, the release operator is not the sole author/approver, and no signer can derive approval from target-database state alone.

The final release attestation chain is single-parent per release channel. Branching candidates may exist, but activation accepts only the attestation whose predecessor is the current channel head. Replay against another environment, recovery epoch, channel, predecessor or manifest is rejected. Public release metadata exposes safe hashes and verification status; private target, actor and incident details remain in restricted audit evidence under ADR-006.

Successful migration is necessary but not sufficient for reader activation. The system stages code/schema/data/docs, obtains the final release attestation and then uses ADR-010’s singleton lane plus expected-current compare-and-activate operation. Traffic changes only after the activation receipt binds the exact attestation and deployed tuple. Schema-cache refresh is part of that receipt.

If migration succeeds but postflight, signing or activation fails, the database stays at the new installed chain while the previous compatible deployment remains active or the channel is frozen. The workflow never falsifies the receipt head or reverses schema implicitly. F2-018 decides which predecessor application versions may remain active against the new schema.

Recovery operations are closed and distinct:

  • Forward fix appends a new migration and release authorization. It is the normal correction for an applied defect.
  • Application rollback reactivates a prior application/API deployment under ADR-010 without changing migration history. It is allowed only when F2-018 declares that tuple compatible and fresh policy/security checks pass.
  • Compensating migration is a new reviewed migration that semantically counteracts an earlier effect while preserving both definitions and attempts. It is not an edit or reusable generic down script.
  • Quarantine/freeze blocks activation and mutation while an uncertain, partial, drifted or compromised state is investigated.
  • Disaster restore reconstructs an exact retained backup into a new recovery target identity/epoch and appends a restore event to the existing external ledger before activation. The event binds the restore authorization; exact backup artifact and backup-manifest roots; source target identity/epoch at capture; captured database receipt, external-event and release-attestation roots; the external authoritative head immediately before restore; the explicit lost-tail boundary and affected head inventory; observed restored receipt/schema/core/curation roots; and the new target identity/epoch. The new epoch is externally allocated and cannot be copied from the backup. A clone presenting only a valid historical receipt prefix is therefore not an authorized recovery target. Restore does not delete later historical events or claim that time ran backward.
  • Break glass is an incident-only externally authorized operation under ADR-021. It records exact commands/bytes and before/after evidence, leaves no retained broad credential and must be reconciled by a reviewed forward migration/release.

No recovery path may update or delete accepted core, curated history, shared anchors, public identity claims, migration definitions, attempt events or release attestations. Restoring exact rows is disaster recovery; changing their meaning is successor or curation work.

Operational interfaces distinguish:

  • accepted migration definition and chain lookup;
  • exact database installed head and drift status;
  • attempt history including failed, partial, uncertain and reconciled outcomes;
  • release candidate authorization status;
  • exact historical release attestation verification; and
  • current channel activation resolution.

Migration ledgers, private evidence and credentials are not exposed through PostgREST or public OpenAPI. Public metadata never reveals target locators, usernames, key custody, private incident details or reusable authorization material. Absence, unauthorized, unsigned, stale, revoked, partial, uncertain and integrity-error states remain distinct internally and follow ADR-010 disclosure policy externally.

F3 and F10 implementations must provide executable gates for:

  1. Exact-byte migration hash and predecessor-chain tamper, insertion, deletion, reorder, duplicate-ID and filename-order negative fixtures.
  2. Canonical envelope/root fixtures across independent implementations, including NFC, key order, genesis, chain-link and invalid-number/unknown-field cases.
  3. External event-chain append-only behavior, compare-and-swap non-equivocation and private database receipt constraints, including inability of the DB receipt to self-authorize.
  4. Control-plane lease plus database-lock races, stale head, lock loss, retry and single-winner behavior.
  5. Authorization reservation/consumption before mutation, external result-write failure and receipt-based reconciliation without SQL replay.
  6. prod_migrator, release session, owner scope, secret handling and postflight zero-transient-membership/credential enforcement.
  7. Empty install and every supported upgrade with exact schema, owner, ACL, RLS, routine, extension, role and default-privilege parity.
  8. Transaction abort and uncertain-commit reconciliation with no blind SQL replay.
  9. Non-transactional checkpoint and backfill fault injection, quarantine, resume, coverage and no-partial-activation behavior.
  10. Out-of-band DDL, forged receipt, wrong target, unresolved attempt and fingerprint drift rejection without synthetic baselining.
  11. Core reconstruction plus immutable/curated root equality before and after every migration not declaring a valid successor release.
  12. Candidate manifest and signature tampering, missing/extra signer, wrong policy, revoked key, wrong authorization/start/result root, predecessor/channel/target/ recovery epoch, replay and unsigned-row rejection.
  13. Migration-success/postflight-failure and signing/activation-failure fixtures that retain the installed head but do not expose an unauthorized release.
  14. Forward-fix, compatible application rollback, compensation, quarantine, exact disaster restore, lost-tail/clone rejection and break-glass reconciliation drills.
  15. Offline verification from retained public keys, canonical manifests, ledgers and evidence hashes without trusting mutable CI labels or the target database.
  16. Public artifact, log, docs and OpenAPI scans proving no credentials, target-private metadata, internal ledger surface or reusable authorization token leaks.
  • Every production schema state is traceable to exact reviewed bytes and one external authorization chain.
  • Failed and uncertain attempts remain visible even when PostgreSQL rolls back its own transaction.
  • Single-writer enforcement is structural rather than an operator convention.
  • Immutable corpus and curation history survive schema recovery without ID churn.
  • Application rollback remains possible when compatibility permits, without lying about the installed database chain.
  • Release infrastructure must retain an external ledger, signing verification state, database receipts, fingerprints and recovery evidence.
  • Non-transactional changes and large backfills require more design and fault testing.
  • A migrated but unattested database may require a temporary compatibility window or channel freeze until final authorization succeeds.
  • Key custody, rotation and incident response become explicit operational duties.

Rejected. It may omit project semantic fields, failed attempts, exact source bytes, external authorization and historical legacy facts. A target DB cannot certify its own release authority.

Rejected. Duplicate date prefixes already exist in legacy material; rename, squash and branch history do not prove application order or target state.

Give the deployment job permanent owner or service-role credentials

Bölüm başlığı “Give the deployment job permanent owner or service-role credentials”

Rejected. This collapses ownership, bypasses review/target binding and turns credential theft into unrestricted persistent mutation.

Rejected. Failed, partial and uncertain attempts are required for safe retry, reconciliation and incident analysis.

Rejected. Destructive inverse SQL cannot restore external effects, dropped information, issued identities or accepted history. Reviewed forward correction is safer and honest.

Rejected. Restore changes recovery identity and can lose a known tail. It must retain later history and state its RPO rather than rewriting the ledger head.

Let a successful migration activate the release automatically

Bölüm başlığı “Let a successful migration activate the release automatically”

Rejected. Postflight, final signature, policy and compatible code/data/docs can still fail. Activation remains a separate compare-and-activate decision.

  • F2-018 defines compatibility impact classes, exact two-person/threshold approval matrix and emergency classification.
  • F2-019 fixes physical source, run, transformation, evidence and curation-event fields shared by migration evidence.
  • F3 defines migration/receipt SQL, object-owner execution mechanism, canonical-profile conforming storage/validation and schema fingerprint implementation.
  • F10 selects signature envelope algorithm, key custody/rotation service, immutable external ledger storage, release runner and incident drills.

Revisit this ADR only if PostgreSQL can provide an equivalent externally authenticated atomic release ledger, the deployment platform cannot issue bounded non-exportable sessions, a required migration class cannot be represented by the three execution modes, or retained signature verification cannot survive key/provider replacement. Revisit by successor ADR; never weaken historical chain or authorization rules in place.