# Disaster recovery ## Canonical restore flow ```text fresh host / control plane → checkout the exact ModelForge commit recorded in the BackupSet → provision PostgreSQL of a compatible major version → verify the BackupSet (manifest hash, payload hashes, decryption, archive structure) → restore the database into an empty destination → run only the forward migrations this build knows → supply secrets; rotate what cannot be restored → start the control plane against the restored database → reconcile: clear current truth, suppress restored alerts, surface in-flight operations → register or re-enrol the node → rehydrate missing artifacts from their exact revisions → validate, compare fingerprints, measure RPO and RTO → operator declares recovery complete ``` ## RestorePlan `RestorePlan` is first class: backup set, mode, target environment and label, redacted database destination, artifact strategy, secret strategy, node strategy, expected ModelForge version, preflight evidence and validation requirements. Plans are idempotent — the same backup, mode, destination and label return the same plan rather than a second one. ```text VALIDATION isolated rehearsal; never a production environment REPLACEMENT an operator-declared maintenance target DISASTER_RECOVERY the only mode permitted to target production, and only when explicitly enabled ``` ## Preflight Eleven checks run before any byte moves, and again at execution time: ```text backup_verified payload_hashes schema_compatibility encryption_key destination_isolation destination_reachable destination_empty postgres_compatibility disk_space source_control_revision external_dependencies ``` A failing preflight moves the plan to `PREFLIGHT_FAILED` and `start` is refused with `restore_preflight_required`. ## RestoreOperation The journal is durable and append-only. Each phase writes a `RestoreOperationEvent` with its evidence, and phase durations accumulate into the measured RTO. ```text PLANNED → PREFLIGHT → RESTORING_DATABASE → RESTORING_CONFIGURATION → REHYDRATING_ARTIFACTS → RECONCILING → VALIDATING → READY ↘ FAILED | MANUAL_INTERVENTION_REQUIRED ``` Retry resumes the journalled operation rather than creating a second one. A completed database phase is never replayed onto live data: the destination-empty guard turns a retry into `DESTINATION_NOT_EMPTY` instead of a merge. A restore interrupted mid-phase is moved to `MANUAL_INTERVENTION_REQUIRED` by startup reconciliation, never silently resumed. ## Reconciliation Reconciliation runs exactly once per restore operation, keyed by a marker audit event carrying that operation's id. Matching on the action alone would be wrong: a backup legitimately contains the reconciliation events of *earlier* restores, and an unscoped check made every later restore believe it had already reconciled. Reconciliation clears every current-truth table, suppresses restored firing alerts so a stale alert is not presented as present-day truth while its history is retained, and reports in-flight lifecycle operations and migration cutovers by id and stage with the typed outcome `RECOVERY_RECONCILIATION_REQUIRED`. It never guesses external state. ## In-flight operations A backup taken while a lifecycle promotion or a migration cutover is mid-flight restores that operation intact, with its idempotency key, expected version and generation. On startup the lifecycle reconciler conservatively rolls the incomplete operation back from its immutable snapshot exactly once; a second start changes nothing. Migration cutovers are deliberately *not* auto- resolved, because external alias truth cannot be inferred after a crash — they stay in their intermediate stage and are reported as requiring reconciliation. ## RPO and RTO Both are measured, never assumed. RPO is the gap between the newest audit event in the restored database and the newest in the source at validation time; the restore point is the backup's completion timestamp. RTO is the sum of the measured phase durations. ## Rehearsal discipline A rehearsal that reuses the live database volume is not a rehearsal. `docker-compose.dr.yml` provisions a separate PostgreSQL 17 with its own volume and a second control plane bound to it, and every rehearsal starts from a destroyed and recreated volume. The production database, ExampleRAG's Qdrant, ExampleVision's recognizer and external GPU workloads are never written to. ## Rehearsal catalogue | | Scenario | Outcome | | --- | --- | --- | | A | Database loss | full restore into a fresh PostgreSQL, measured RTO/RPO | | B | Corrupt backup | payload and manifest tampering both fail closed | | C | Artifact loss | exact-revision rehydration into a disposable location | | D | Node credential loss | revoke, re-enrol, identity preserved | | E | Control-plane rebuild | isolated plane serving restored truth | | F | In-flight lifecycle | reconciled exactly once | | G | In-flight migration | surfaced, never auto-resolved | | H | Observability history | history restored, current state re-derived | See `docs/quality/M15_VERIFICATION.md` for the measured results and `docs/operations/RUNBOOK_FULL_DR.md` for the operator procedure.