Skip to content

P7-C REBUILD: portable persistence, backup & PostgreSQL DR - #274

Merged
jnhu76 merged 12 commits into
masterfrom
feat/p7-c-portable-backup-recovery
Aug 10, 2026
Merged

jnhu76 merged 12 commits into
masterfrom
feat/p7-c-portable-backup-recovery

Conversation

@jnhu76

@jnhu76 jnhu76 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

P7-C REBUILD — Portable Persistence, Backup & PostgreSQL Disaster Recovery

Full rebuild of P7-C off origin/master (baseline 2a1a9eb). PR #273 is
treated as historical/reference only — it is NOT the baseline and was not
repaired, cherry-picked, or simplified. Executed C1 → C2 → C3 serially in
this branch, one commit per phase, then a corrective pass that collapsed
PITR into the canonical Compose and renamed the deployment verification to
capability-named suites.

Verdict: P7-C PORTABLE PERSISTENCE / BACKUP / POSTGRESQL RECOVERY READY FOR HUMAN REVIEW

Full evidence: docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md.

Authority stores (reconfirmed at C0)

  • PostgreSQL = sole authoritative durable store (bind-mounted at ${EXAM_DATA_ROOT}/postgres, operator-visible)
  • Redis = non-authoritative (rate-limit counters only, TTL-bounded)
  • App filesystem = no durable writes

Final structure — one canonical Compose

docker-compose.yml              ← the ONE production/operator entry point
scripts/backup/
  cold-filesystem-backup.sh     ← C1 cold copy (stopped PGDATA, ownership preserved)
  cold-filesystem-restore.sh    ← C1 cold restore (fresh data root)
  postgres-logical-backup.sh    ← C2 pg_dump -Fc online backup
  postgres-logical-restore.sh   ← C2 clean restore (DROP … WITH (FORCE) + template0)
  pg-basebackup.sh              ← C3 pg_basebackup -X stream + pg_verifybackup
  postgres-enable-pitr.sh       ← C3 WAL archiving — PITR is DB configuration
                                  (ALTER SYSTEM), NOT an alternate Compose topology
tests/deployment/               ← capability-named verification suites
  compose-smoke.sh  launchpad-bootstrap.sh  persistence-and-cold-restore.sh
  logical-backup-restore.sh  pitr.sh

Phase summary

Phase Commit Ships
C1 portable persistence + cold backup + Launchpad df2d07df Bind-mount portable data root; cold-filesystem backup/restore scripts; Launchpad first-install surface (init-gate FIRST, constant-time token compare, shares bootstrapAdminOnFreshDb)
C2 logical backup + clean restore e164fd30 pg_dump -Fc online backup; clean restore (DROP DATABASE … WITH (FORCE) + template0 + pg_restore --no-owner --exit-on-error) — NOT --clean --if-exists into a dirty DB
C3 physical backup + PITR 0df4ba72 pg_basebackup -X stream + pg_verifybackup; postgres-enable-pitr.sh WAL continuous archiving (idempotent non-overwriting archive_command, archive_timeout 60s, persisted via ALTER SYSTEM); PITR to recovery_target_lsn/time/xid
Closeout 0ebdf6eb Closeout doc + roadmap repair
Corrective pass fbea2a55a8767dfe PITR collapsed into the canonical Compose (no PITR override file); launchpad bootstrap wiring + first-install hardening; operator contract alignment; closeout pin; final corrective pass — corrected PITR failure proof, capability-named deployment tests
Review closeout fab5b7f55c4fc997 Merge-readiness fixes from human review (below)

Review closeout (fab5b7f55c4fc997)

  • Operator runbooks now show the source as "${EXAM_DATA_ROOT:-./data}" instead of a hardcoded ./data (a hardcoded path could silently back up the wrong directory on a relocated deployment).
  • Removed the alpine:latest helper-image dependency from the backup scripts: the cold backup/restore scripts reuse the deployment's own pinned postgres:18.4-bookworm (which ships sh/cp/find), and pg-basebackup.sh dropped its cosmetic du size display.
  • Repository ctx convention: defaultOrganizationExists(ctx) now takes ctx like every other repo method.
  • postgres-logical-restore.sh DROP now uses WITH (FORCE) (terminates lingering connections; robustness only — the stop-API-first contract is unchanged).
  • Test-harness hardening (d15702f9): safe_temp_root now records every created path in a registry and cleanup_temp_root removes ONLY recorded roots — TMPDIR location never matters and arbitrary paths can never be removed; run_compose consumes an explicit empty first argument; wait_for_postgres / psql_exec derive POSTGRES_USER/POSTGRES_DB from the running db container; PG_IMAGE/PG_MAJOR are derived from docker-compose.yml so a postgres version bump updates the tests automatically.
  • launchpad-bootstrap.sh: per-run unique RUN_ID for Compose project names (seconds-only timestamps collided across runs); health-timeout path uses fail() so the summary stays consistent; Stack B asserts the running app container carries no LAUNCHPAD_SETUP_TOKEN env.
  • pitr.sh: archive-idempotency cases named explicitly (absent-target / identical-retry / byte-collision); F1 no-promotion window shortened to 30s (exceeds any incorrect-promotion time); PGDATA paths and postgres image references use the derived PG_MAJOR/PG_IMAGE.
  • compose-smoke.sh data root is cleaned container-assisted (host rm -rf cannot delete postgres-owned files).
  • deployment-topology-contract.mjs: a missing tests/deployment/ directory is now a contract error (ENOENT); other errors propagate.
  • Temp-root cleanup actually runs (5c4fc997): the registry introduced in d15702f9 was INERT — VAR="$(safe_temp_root …)" command substitution runs the function in a subshell, so the registry append was lost and cleanup_temp_root always bailed; every suite leaked its temp roots (verified: a full suite run left all ~25 roots behind). safe_temp_root now takes the caller's variable NAME and assigns via printf -v in the caller's scope (all 14 call sites converted; a missing name is a hard error, so a future command-substitution call fails instead of leaking). launchpad-bootstrap.sh was the one suite d15702f9 missed — it still used the old /tmp/… regex guard + host rm -rf (which cannot delete postgres-owned PGDATA files) — and is converted to cleanup_temp_root like the other four suites. cleanup_temp_root now WARNs when a root survives. Verified: full pnpm test:deployment passes with ZERO temp-root leftovers.
  • Docs: closeout scopes runtime credential derivation to the online PostgreSQL scripts and documents the cold scripts' path/ownership checks; archive_timeout is described as an archival-freshness bound for active workloads (recovery depends on available archived WAL + the selected target type), not a target-granularity limit; F1 heading reads "missing required WAL"; pg_verifybackup wording drops the inaccurate mtime validation (checksums + manifest checksum only), also fixed in the pg-basebackup.sh comment.

Verification suites (deterministic, isolated, all PASS on final head)

  • compose-smoke.sh — production Compose parses, starts, and honors the environment contract (isolated project + temp data root).
  • launchpad-bootstrap.sh — first-install contract A1–A7 + B1: fresh stack uninitialized; wrong token 403; correct token creates first Admin + default org; never reopens (409); login works; exactly 1 Admin / 0 Candidates / admin.bootstrap audit evidence; register disabled; no-token disables launchpad.
  • persistence-and-cold-restore.sh — container-recreation persistence, stopped-directory relocation, cold backup/restore round-trip (identical invariants after each step).
  • logical-backup-restore.sh — A-present / B-absent clean logical restore.
  • pitr.sh — archive idempotency (absent / identical retry / byte collision); happy PITR (A + A1 + B present, C absent, promoted); F1 missing REQUIRED archived WAL (loud — stays in archive recovery, no promotion); F2 corrupt base backup (pg_verifybackup rejects); F3 invalid recovery target (refuses to start).

Product path == test path. The operator path and the test path are the
SAME scripts and the SAME Compose — recovery clusters start from
docker-compose.yml with isolated EXAM_DATA_ROOT /
EXAM_WAL_ARCHIVE_HOST_PATH / COMPOSE_PROJECT_NAME. No test privately
configures PostgreSQL through a second hidden method.

Gates

  • pnpm verify ✅ (format:check, lint family, copy/arch/env-contract lint, typecheck, openapi check, coverage, build)

Scope discipline (what was NOT built — "Delete ceremony, not safety")

NO custom relocation manifest protocol · NO Docker image identity framework · NO OCI digest authority subsystem · NO cross-runner docker-save bundle · NO migration-history preflight framework · NO historical-migration omission allowlist · NO custom recoveryEpoch · NO generic reconciler/job engine · NO Kubernetes/Patroni/HA · NO Admin restore button · NO Desktop · NO PG18 incremental backups · NO retention engine · NO PITR automation in base compose · NO P7-E control plane. No backup-helper image management framework — the scripts reuse the deployment's own pinned postgres image.

PR #273 is NOT recreated under different names.

ADR-016 boundary

Container restart / C1 relocation = same authoritative history. C2 logical restore / C3 physical restore / PITR = authoritative history replacement. No schema change marks these events. The exam system's authoritative state is whatever PostgreSQL currently holds.

P7-E handoff (deferred, not started)

RPO/RTO profile automation · retention automation · Admin backup visibility (read-only; restore stays operator-owned) · files/settings backup beyond PostgreSQL · cross-PG-major upgrade playbook.

P0–P3

None outstanding.


Do NOT merge automatically. Do NOT start P7-E.

Summary by CodeRabbit

  • New Features

    • Added a token-protected Launchpad for first-time organization and Administrator setup.
    • Added installation-status checks, validation, clear errors, and safe handling of repeated or simultaneous setup attempts.
    • Added PostgreSQL backup, restore, cold-copy, physical backup, and point-in-time recovery tools.
    • Added persistent host-mounted data and optional WAL archiving for portable deployments.
  • Documentation

    • Expanded deployment, backup, recovery, setup, and data-persistence guidance.
    • Clarified safe shutdown and destructive data-deletion procedures.
  • Tests

    • Added end-to-end verification for setup, persistence, backup, restore, and recovery workflows.

jnhu76 added 4 commits August 10, 2026 03:37
P7-C1 rebuild: portable persistence + cold filesystem backup/recovery +
first-install Launchpad, starting from origin/master (baseline
2a1a9eb). Not derived from PR #273.

Compose + isolation (C1.1-C1.3):
- docker-compose.yml: authoritative state moved from named volumes to
  operator-visible host bind mounts under ${EXAM_DATA_ROOT:-./data}
  (postgres + redis). Top-level named-volume declarations removed. The
  deployment-topology-contract guard still passes (it pins service
  presence + security invariants, not volume type).
- .gitignore: /data/ added so runtime data is never committed.
- p6-corr1-compose-smoke.sh: sets EXAM_DATA_ROOT to a per-run mktemp
  directory with trap cleanup (path-guarded rm). All 16 checks still pass.

Persistence + relocation proofs (C1.4-C1.5):
- scripts/deployment/p7-c1-persistence-smoke.sh: down/up restart
  persistence + cold directory relocation (container-assisted cp -a of
  the COMPLETE data root while PostgreSQL is stopped) into a new project
  with identical business invariants. PASS.
- Documents the PGDATA-files-owned-by-uid-999 reality: cold copy uses a
  helper container (host cp -a fails with EACCES).

Cold backup/restore (C1.6-C1.8):
- scripts/backup/cold-filesystem-backup.sh + cold-filesystem-restore.sh:
  operator helpers that refuse unsafe/empty paths, validate PGDATA
  shape, preserve ownership/mode/symlinks, and require explicit
  confirmation. Backup layout mirrors the deployment data root.
- scripts/deployment/p7-c1-cold-backup-restore-drill.sh: permanent
  automated drill proving a fresh working Exam deployment with identical
  authoritative state is produced from a cold backup. Closes P2-2/P2-3
  for the cold-filesystem path. PASS.

Launchpad first-install (C1.9):
- runtimeConfig.ts: LaunchpadConfig { setupToken } resolved from
  LAUNCHPAD_SETUP_TOKEN (unset/empty = disabled; NOT fail-fast).
- packages/contracts: launchpad.ts (status + bootstrap request/response
  schemas); messageRegistry error codes LAUNCHPAD_ALREADY_INITIALIZED /
  LAUNCHPAD_INVALID_SETUP_TOKEN.
- apps/api/src/routes/launchpad.ts: GET /api/launchpad/status (public,
  init-state only, no token oracle) + POST /api/launchpad/bootstrap
  (public, rate-limited, init-gate FIRST then constant-time token check,
  delegates to the canonical bootstrapAdminOnFreshDb atomic mutation —
  no duplicated irreversible logic). Role is not selectable (Admin only).
- packages/db organizationRepo.defaultOrganizationExists(): the init gate
  (NOT activeAdminCount==0; removing last Admin never reopens launchpad).
- Frontend: /launchpad public route (LaunchpadPage), redirect to /login
  when initialized, success redirect to /login, full i18n (zh-CN).
- Tests: 6 route integration tests (init gate, token disabled/mismatch,
  atomic success + no token leak, second-bootstrap refused) + 5 page
  tests (redirect-when-initialized, form render, validation, submit,
  error banner).

Docs (C1.10): docs/deployment/backup-and-recovery.md (new canonical
guide), README + runbook updated with the bind-mount layout, Launchpad
option, and the validated cold-backup/restore path.

Gates: format/lint/arch/copy/ui-gates/eslint/typecheck/openapi all
PASS; p6-corr1 smoke (16), persistence smoke, cold-backup drill PASS.

C2 (logical pg_dump) and C3 (physical pg_basebackup + PITR) follow in
separate commits on this branch.
Routine online pg_dump logical backup + clean-target restore producing a
fresh working Exam with the expected state. Closes P7-C0 P2-2 (logical
backup/restore UNVALIDATED) and P2-3 (exact historical replacement
UNPROVEN).

Backup (C2.1-C2.4):
- postgres-logical-backup.sh: pg_dump -Fc custom format, --no-owner,
  via the db container (PostgreSQL stays ONLINE; API may be down).
  Fails non-zero on error; password never on argv (PGPASSWORD env).
  Refuses empty/partial artifact: non-empty + PGDMP magic +
  pg_restore --list OK (-i so stdin reaches the container).

Clean restore (C2.5-C2.6):
- postgres-logical-restore.sh: clean-target contract — DROP target DB +
  CREATE DATABASE ... TEMPLATE template0 (truly empty), then
  pg_restore --no-owner --exit-on-error. EXACT match of the dump, NOT a
  merge, fixing the --clean --if-exists-into-dirty-target gap. Requires
  typing the target DB name to confirm; refuses system DBs; operator-only.

Drill (C2.5 — the program's central property):
- p7-c2-logical-restore-drill.sh: isolated deploy -> bootstrap + State-A
  marker -> pg_dump -Fc -> mutate to State B (marker B) -> clean-restore
  State A -> assert A present, B ABSENT, business invariants (org/admin/
  audit) restored, restored Admin row + password hash survived.
  ALL CHECKS PASSED. Proves a backup creates a fresh working Exam with
  the expected state and dump-absent objects are removed.

Cluster globals (C2.3):
- Audited: bundled Compose db service creates role/db at image init
  from POSTGRES_USER/POSTGRES_DB/POSTGRES_PASSWORD, so they are recreated
  by Docker/bootstrap config and NOT required in the dump. No cluster
  roles/tablespaces/globals are application-defined. pg_dumpall
  --globals-only NOT included for the bundled path (documented §7.3).

Docs (C2.7): backup-and-recovery.md §7 (logical backup + clean restore +
cluster-globals decision); decision tree updated; runbook §17 cross-refs
the validated C2 path and demotes the old one-liner to reference.

Gates: format/lint/arch/stale-ui-docs PASS; C2 drill PASS.
PostgreSQL-native physical backup, WAL archiving, and point-in-time
recovery (P7-C phase 3):

- scripts/backup/pg-basebackup.sh: online physical base backup via
  pg_basebackup (-X stream -c fast -Fp --manifest-checksums SHA256), then
  pg_verifybackup on the manifest. Required WAL is streamed at backup time
  so the base backup is internally consistent on its own; the manifest
  gives backup-integrity evidence (NOT a business-restore proof; a drill
  is still required).
- docker-compose.pitr.yml + docker/pitr/wal-archive.conf: optional
  PostgreSQL-native continuous archiving override. archive_mode=on with a
  non-overwriting archive_command ('test ! -f /wal-archive/%f && cp ...')
  so a WAL filename collision fails VISIBLY rather than silently
  overwriting. wal_level stays at replica (sufficient for PITR).
- scripts/deployment/p7-c3-pitr-drill.sh: deterministic PITR happy-path.
  base backup -> marker A -> marker B (capture LSN) -> destructive marker
  C -> recover to the captured LSN -> assert A present, B present, C
  absent. LSN-based target is clock-skew-independent. PASS.
- scripts/deployment/p7-c3-pitr-failure-drill.sh: three failure modes
  that MUST fail loudly:
    F1 missing WAL segment -> recovery surfaces the missing segment;
    F2 corrupt base backup -> pg_verifybackup rejects the tampered file;
    F3 invalid recovery_target_lsn -> recovery cluster refuses to start.
  All three PASS.
- docs/deployment/backup-and-recovery.md: full decision tree (C1+C2+C3)
  with comparison table; new section 8 documents pg_basebackup, WAL
  archive, PITR procedure (incl. recovery_target_lsn/time/xid guidance),
  what C3 does NOT do, minimal operator-owned retention, and drill
  evidence; new section 10 codifies the ADR-016 boundary
  (relocation = same history; C2/C3 restore = history replacement; no
  schema change).
- README + runbook cross-references updated; the runbook's stale "C3
  forthcoming" note is replaced with the shipped procedures.

C3 scope discipline: no PG18 incremental base backups, no retention
engine, no desktop recoveryEpoch, no schema change, no PITR automation in
the base compose (the override is opt-in).

Tests: route registry conformance anchor updated (+2 public launchpad
routes registered in C1; total now 115 = 99 protected + 16 non-protected).
All 2019 vitest tests pass; pnpm verify:static + pnpm build pass.
P7-C rebuild closeout. Adds the authoritative closeout document and
repairs the roadmap to the rebuilt C0/C1/C2/C3 shape.

- docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md: single source
  of truth for the rebuild. Records baseline SHA, final head SHA,
  per-phase evidence (C1/C2/C3), the full backup/restore matrix, downtime
  requirements, version compatibility, the off-host requirement, drill
  evidence (5/5 drills PASS on final head), the §7 adversarial matrix,
  scope discipline (what was NOT built), the ADR-016 boundary, known
  limitations, P0-P3 (none outstanding), and the P7-E handoff. Ends with
  the program verdict.
- docs/roadmap/current.md: P7 row updated (P7-C rebuilt & shipped);
  execution-order block records the rebuilt phase sequence and the
  operator-owned-restore scope discipline; Workstream-4 backup note
  carries the post-rebuild status and the P7-E deferral.
- docs/roadmap/P7-system-readiness-and-exam-modes.md: Workstream C
  re-scoped to the shipped portable-persistence + backup + PostgreSQL DR
  program (C0 closed; C1 portable + cold + Launchpad; C2 logical; C3
  physical + PITR). The pre-rebuild C1=config-taxonomy / C2=settings-
  service / C3=settings-UI framing is explicitly superseded; those
  config-control-plane items now live under Workstream E and are NOT
  started. The sequence block records P7-C as shipped and P7-E
  (RPO/RTO + retention + Admin visibility) as the future control plane.

PR #273 remains historical/reference only. No ceremony was reintroduced.

pnpm verify:static PASS.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds a token-protected first-install Launchpad flow, atomic Admin bootstrap handling, host-mounted PostgreSQL persistence, PostgreSQL backup and recovery scripts, PITR deployment drills, and updated deployment documentation.

Changes

Launchpad bootstrap

Layer / File(s) Summary
Launchpad contracts and API flow
packages/contracts/..., apps/api/src/routes/..., apps/api/src/scripts/...
Adds status and bootstrap contracts, public API routes, setup-token validation, atomic first-Admin creation, advisory-lock serialization, source-aware audit records, and conflict responses.
Launchpad web flow
apps/web/src/pages/..., apps/web/src/App.tsx, apps/web/src/lib/..., apps/web/src/i18n/...
Adds the /launchpad page, status checks, form validation, bootstrap submission, redirects, localized labels, and error states.
Launchpad validation
apps/api/src/routes/launchpad.test.ts, apps/api/src/scripts/bootstrap-admin.test.ts, apps/web/src/pages/LaunchpadPage.test.tsx, apps/api/openapi.json, apps/api/src/authz/...
Adds API, UI, OpenAPI, and authorization coverage for initialization, invalid tokens, successful setup, repeat setup, and concurrent requests.

PostgreSQL persistence and recovery

Layer / File(s) Summary
Portable Compose persistence
docker-compose.yml, .env.example, .gitignore, scripts/repository-contract/..., tests/deployment/...
Replaces named volumes with host bind mounts, forwards LAUNCHPAD_SETUP_TOKEN, adds WAL archive storage, and standardizes isolated deployment test data roots.
Backup and restore tooling
scripts/backup/...
Adds cold filesystem, logical, physical, WAL archiving, and restore helpers with validation, safety checks, artifact verification, and operator confirmations.
Recovery validation
tests/deployment/logical-backup-restore.sh, tests/deployment/persistence-and-cold-restore.sh, tests/deployment/pitr.sh, package.json
Adds deployment suites for persistence, relocation, cold restore, logical restore, PITR, archive behavior, and recovery failure cases.
Deployment and recovery guidance
README.md, docs/deployment/..., docs/audits/..., docs/roadmap/...
Documents the single Compose topology, authoritative PostgreSQL storage, backup and restore procedures, PITR boundaries, Launchpad operation, audit findings, and roadmap status.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • jnhu76/exam#273: Directly related Launchpad bootstrap, persistence, Compose, and deployment work.
  • jnhu76/exam#44: Introduced the bootstrap implementation extended with Launchpad support and concurrency handling.
  • jnhu76/exam#270: Related PostgreSQL persistence, backup, recovery, and PITR implementation work.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.53% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly summarizes the main changes: portable persistence, backup, and PostgreSQL disaster recovery.
Description check ✅ Passed The description is detailed and on-topic, covering scope, implementation, tests, verification, deferred work, and merge intent.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/p7-c-portable-backup-recovery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (12)
docs/deployment/mvp-deployment-runbook.md-179-183 (1)

179-183: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the Launchpad cross-reference.

backup-and-recovery.md §8 documents physical backup and PITR. The Launchpad procedure is in §11. Point this instruction to §11 so operators do not open the wrong procedure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/deployment/mvp-deployment-runbook.md` around lines 179 - 183, Update the
backup-and-recovery cross-reference in the Launchpad first-install instructions
to point to §11 instead of §8, leaving the rest of the setup procedure
unchanged.
docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md-140-146 (1)

140-146: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the required closeout report fields.

The closeout reports pnpm verify:static, test totals, build status, and limitations. It does not report modified files, new-test results as a dedicated field, coverage, or pnpm verify. Add those results, or state explicitly that each item was not collected or not run.

As per coding guidelines, every completed job must report modified files, new tests, coverage, pnpm verify results, and known limitations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md` around lines 140 -
146, Update the final closeout report fields around the existing “Final gates on
the final head” section to explicitly report modified files, new-test results,
coverage, and `pnpm verify` results, stating when any item was not collected or
not run. Preserve the existing gate results and limitations.

Source: Coding guidelines

docs/deployment/backup-and-recovery.md-267-276 (1)

267-276: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Do not describe a logical restore as byte-for-byte identical.

pg_restore recreates the dump’s logical database state. Physical page layout, relation files, and other storage details can differ. State that the clean restore matches the dump’s logical contents and validated business invariants.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/deployment/backup-and-recovery.md` around lines 267 - 276, Update the
clean-target contract description to remove “byte-for-byte” language and state
that the C2 restore via DROP DATABASE, CREATE DATABASE ... TEMPLATE template0,
and pg_restore reproduces the dump’s logical contents and validated business
invariants. Preserve the explanation that State-B-only data is absent and adjust
the automated drill wording accordingly.
docs/roadmap/current.md-15-15 (1)

15-15: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the configuration-control-plane ownership.

The companion roadmap moves the pre-rebuild configuration-control-plane work to Workstream E. P7-E covers RPO/RTO profiles, retention automation, and the Admin backup surface. Remove (P7-E) from config-control-plane or replace it with the correct Workstream E reference.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/roadmap/current.md` at line 15, Update the P7 roadmap entry’s
config-control-plane reference to use the correct Workstream E designation,
removing the incorrect “(P7-E)” association while preserving the rest of the
status and workstream text.
docs/deployment/backup-and-recovery.md-44-51 (1)

44-51: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Distinguish the host data root from PGDATA.

The Compose contract mounts ${EXAM_DATA_ROOT}/postgres at /var/lib/postgresql, while the image’s actual PGDATA is under .../postgres/18/docker. Call data/postgres the host PostgreSQL data root, and reserve PGDATA for the nested directory.

Proposed wording change
-    ├── postgres/      ← authoritative: the PostgreSQL data directory (PGDATA)
+    ├── postgres/      ← authoritative host PostgreSQL data root
...
-- `data/postgres/` is **required**. Deleting it destroys authoritative Exam
+- `data/postgres/` is the **required host bind mount**. Deleting it destroys authoritative Exam
   state.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/deployment/backup-and-recovery.md` around lines 44 - 51, Update the
deployment documentation to distinguish the host PostgreSQL data root from the
image-managed PGDATA directory: describe data/postgres as the host mount parent,
and reserve PGDATA for the nested postgres/18/docker directory used by the
PostgreSQL image. Preserve the warning that deleting the host data root destroys
authoritative Exam state.
scripts/deployment/p7-c3-pitr-failure-drill.sh-232-245 (1)

232-245: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Tighten the F3 fallback pattern.

Line 238 matches error, fatal, and stopped. Container logs contain these tokens in unrelated messages, so the branch can pass without the malformed recovery_target_lsn being the cause. Restrict the fallback to a FATAL or PANIC line that names the parameter or the configuration file.

Also rename INVALID_OK. The variable is set to yes when the cluster becomes ready, which is the failure condition for F3. A name such as REC_BECAME_READY states the meaning.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-pitr-failure-drill.sh` around lines 232 - 245, In
the F3 validation block, rename INVALID_OK to REC_BECAME_READY consistently and
update the condition so readiness remains the failure case. Tighten the fallback
grep in the docker compose logs check to accept only FATAL or PANIC lines that
also identify recovery_target_lsn or the relevant configuration file, removing
broad matches for error and stopped.
scripts/deployment/p7-c3-pitr-drill.sh-200-207 (1)

200-207: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Disable archiving in the recovered cluster.

The base backup carries the source postgresql.auto.conf, which contains archive_mode = 'on' and the archive_command that writes to /wal-archive. The recovery override mounts /wal-archive read-only at Line 217. After promotion, every archive_command invocation fails, WAL accumulates in pg_wal, and the archiver retries forever.

Append an explicit override so the recovered cluster does not archive.

🛠️ Proposed fix
 cat >> /pg/postgresql.auto.conf <<CONF
 restore_command = 'cp /wal-archive/%f %p'
 recovery_target_lsn = '${RECOVERY_LSN}'
 recovery_target_inclusive = on
 recovery_target_action = 'promote'
+archive_mode = 'off'
 CONF
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-pitr-drill.sh` around lines 200 - 207, Update the
recovery configuration block that appends to postgresql.auto.conf to explicitly
disable archiving in the recovered cluster, overriding the inherited
archive_mode/archive_command settings before promotion. Preserve the existing
restore_command and recovery target settings.
apps/web/src/pages/LaunchpadPage.tsx-75-88 (1)

75-88: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Client validation does not check the length rules the API enforces.

The bootstrap contract requires adminUsername with at least 3 characters and adminPassword with at least 8 characters. validate only checks for non-empty values. A short password therefore reaches the API, Fastify rejects it with a schema validation error, and getApiErrorMessage shows the generic launchpad.errors.bootstrapFailed banner. The operator receives no field-level reason on the only path that installs the product.

Add the length checks and matching zh-CN messages.

🛡️ Proposed fix
     if (!adminUsername.trim())
       errors.adminUsername = t("launchpad.adminUsernameRequired");
+    else if (adminUsername.trim().length < 3)
+      errors.adminUsername = t("launchpad.adminUsernameMinLength");
     if (!adminPassword.trim())
       errors.adminPassword = t("launchpad.adminPasswordRequired");
+    else if (adminPassword.length < 8)
+      errors.adminPassword = t("launchpad.adminPasswordMinLength");

Add to apps/web/src/i18n/locales/zh-CN.ts under launchpad:

adminUsernameMinLength: "管理员用户名至少 3 位",
adminPasswordMinLength: "管理员密码至少 8 位",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/pages/LaunchpadPage.tsx` around lines 75 - 88, Update the
validate function to add field-level minimum-length checks for adminUsername (3
characters) and adminPassword (8 characters), while preserving the existing
required checks. Add the corresponding launchpad.adminUsernameMinLength and
launchpad.adminPasswordMinLength translations to zh-CN.ts and use them for the
validation errors.
apps/api/src/routes/launchpad.ts-128-177 (1)

128-177: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Map a lost bootstrap race to 409 instead of 500.

The initialized gate at Line 132 and the mutation at Line 163 are separate steps. Two concurrent bootstrap requests can both read initialized === false. The database constraints serialize the writes, so the second transaction fails. bootstrapAdminOnFreshDb throws a plain Error in that path, which the error handler reports as 500. The operator then sees a server error for a state that is really "already initialized".

Re-check the gate after a failure and return 409.

🛡️ Proposed fix
-      const result = await bootstrapAdminOnFreshDb(
-        fastify.db,
-        {
-          username: data.adminUsername,
-          password: data.adminPassword,
-          name: data.adminName,
-        },
-        orgOptions,
-      );
+      let result;
+      try {
+        result = await bootstrapAdminOnFreshDb(
+          fastify.db,
+          {
+            username: data.adminUsername,
+            password: data.adminPassword,
+            name: data.adminName,
+          },
+          orgOptions,
+        );
+      } catch (err) {
+        // A concurrent bootstrap may have won the race. Re-read the gate so
+        // the loser reports the real state instead of a 500.
+        if (await isInstallationInitialized()) {
+          return reply
+            .code(409)
+            .send(
+              buildErrorResponse(request.id, "LAUNCHPAD_ALREADY_INITIALIZED"),
+            );
+        }
+        throw err;
+      }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/launchpad.ts` around lines 128 - 177, Wrap the
bootstrapAdminOnFreshDb call in the route handler with failure handling that
re-checks isInstallationInitialized() after an error; when the installation is
now initialized, return the existing 409 LAUNCHPAD_ALREADY_INITIALIZED response,
otherwise rethrow the original error so unrelated failures remain 500.
apps/api/src/routes/launchpad.ts-73-75 (1)

73-75: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align defaultOrganizationExists() with the repository ctx-first rule.

packages/db/src/repository/organizationRepo.ts documents defaultOrganizationExists() as a first-install probe and does not record it as an accepted ctx-less exception, while the repo contract requires ctx as the first argument. Keep this route path using the repository, but add a ctx parameter/acceptance if this read is intentionally pre-tenant.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/launchpad.ts` around lines 73 - 75, Update
isInstallationInitialized and the organization repository call so
defaultOrganizationExists follows the ctx-first repository contract: pass an
appropriate pre-tenant context or explicitly accept the ctx-less first-install
probe in the repository API if that behavior is intentional. Keep the route
using createOrganizationRepo and preserve its boolean initialization result.

Source: Coding guidelines

scripts/deployment/p7-c1-persistence-smoke.sh-92-97 (1)

92-97: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Cleanup guards assume mktemp returns a /tmp/... path. mktemp -d -t <prefix> creates the directory under $TMPDIR when that variable is set (this is the default on macOS and in many CI images). The /tmp/... pattern then never matches, cleanup silently skips, and every run leaks a full PostgreSQL data directory. Match against the prefix the script itself used instead of a hard-coded /tmp, for example by comparing $(dirname "${d}") with the mktemp parent and checking the basename prefix.

  • scripts/deployment/p7-c1-persistence-smoke.sh#L92-L97: replace the grep -Eq '/tmp/p7c1-persist-[AB]-...' test with a basename-prefix check against the directories recorded in CREATED_DIRS.
  • scripts/deployment/p6-corr1-compose-smoke.sh#L120-L124: replace the ^/tmp/p6corr1-smoke-data- test with the same basename-prefix check on EXAM_DATA_ROOT.
  • scripts/deployment/p7-c1-cold-backup-restore-drill.sh#L63-L72: replace the /tmp/p7c1-colddrill-... test with the same basename-prefix check.
  • scripts/deployment/p7-c2-logical-restore-drill.sh#L59-L66: replace the /tmp/p7c2-drill-... test with the same basename-prefix check.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c1-persistence-smoke.sh` around lines 92 - 97, Cleanup
guards incorrectly hard-code /tmp, so they skip directories created under
TMPDIR. In scripts/deployment/p7-c1-persistence-smoke.sh lines 92-97,
scripts/deployment/p6-corr1-compose-smoke.sh lines 120-124,
scripts/deployment/p7-c1-cold-backup-restore-drill.sh lines 63-72, and
scripts/deployment/p7-c2-logical-restore-drill.sh lines 59-66, replace the
absolute-path regexes with basename-prefix validation using each script’s
recorded directory variable, while retaining the existing directory and cleanup
safety checks.
scripts/backup/postgres-logical-backup.sh-108-113 (1)

108-113: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Remove the partial artifact when pg_dump fails.

The redirect at Line 113 creates ${DEST} before pg_dump writes anything. If pg_dump exits non-zero, set -e aborts the script immediately. The empty or truncated file remains, and the next run stops at the "destination already exists" check (Lines 72-75). Install a trap before the redirect.

TS (Line 108) is assigned but never used, and the header at Line 17 promises a timestamped dump. Either use TS in the artifact name or delete the variable.

🐛 Proposed fix
-TS="$(date -u +%Y%m%dT%H%M%SZ)"
+cleanup_partial() { rm -f "${DEST}"; }
+trap cleanup_partial EXIT
 docker exec \
   -e PGPASSWORD="${PGPASSWORD:-}" \
   "${DB_CONTAINER}" \
   sh -c 'pg_dump -U "$POSTGRES_USER" -d "$POSTGRES_DB" -Fc --no-owner' \
   > "${DEST}"

Clear the trap (trap - EXIT) after all verification steps succeed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/postgres-logical-backup.sh` around lines 108 - 113, Update the
backup flow around TS and the pg_dump redirect: use TS in the destination
artifact name to preserve the promised timestamped naming, then install an EXIT
trap before pg_dump that removes DEST when the command fails. Clear the trap
with trap - EXIT only after all verification steps succeed, while preserving the
existing destination-exists checks.

Source: Linters/SAST tools

🧹 Nitpick comments (19)
scripts/deployment/p7-c3-pitr-drill.sh (3)

131-134: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Fail explicitly when the app never becomes healthy.

The loop exits after 90 attempts without a status check. The script then runs bootstrap-admin.js against an unhealthy container. The resulting error does not identify the real cause.

♻️ Proposed change
-for i in $(seq 1 90); do
+APP_READY="no"
+for _ in $(seq 1 90); do
   docker exec "${PROJECT_SRC}-app-1" node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" >/dev/null 2>&1 && break
   sleep 1
 done
+docker exec "${PROJECT_SRC}-app-1" node -e "fetch('http://127.0.0.1:3000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" >/dev/null 2>&1 || {
+  echo "FAIL: app never became healthy." >&2
+  exit 1
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-pitr-drill.sh` around lines 131 - 134, Track whether
the health-check loop in the deployment script succeeded, and after the loop
explicitly fail with a clear error if the app never becomes healthy within 90
attempts. Only run bootstrap-admin.js after a successful health check.

61-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Split declaration and assignment.

export VAR="$(cmd)" masks the exit status of openssl. Shellcheck reports SC2155 on both lines.

♻️ Proposed change
-export POSTGRES_PASSWORD="p7c3-src-pg-$(openssl rand -hex 6)"
+POSTGRES_PASSWORD="p7c3-src-pg-$(openssl rand -hex 6)"
+export POSTGRES_PASSWORD
 SHARED_PG_PASSWORD="${POSTGRES_PASSWORD}"
-export JWT_SECRET="p7c3-src-jwt-$(openssl rand -hex 16)"
+JWT_SECRET="p7c3-src-jwt-$(openssl rand -hex 16)"
+export JWT_SECRET
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-pitr-drill.sh` around lines 61 - 63, Split the
declarations and command substitutions for POSTGRES_PASSWORD and JWT_SECRET in
the deployment script: assign each generated value first, then export the
variable separately. Preserve the existing generated prefixes and openssl
arguments while ensuring openssl failures are not masked.

Source: Linters/SAST tools


227-232: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use _ for the unused loop variable.

Shellcheck reports SC2034 because i is never read. The loop at Line 80 already uses _.

♻️ Proposed change
-for i in $(seq 1 90); do
+for _ in $(seq 1 90); do
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-pitr-drill.sh` around lines 227 - 232, Replace the
unused loop variable i in the pg_isready retry loop with _, matching the
existing loop convention elsewhere in the script and eliminating ShellCheck
SC2034.

Source: Linters/SAST tools

scripts/deployment/p7-c3-pitr-failure-drill.sh (2)

278-290: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add PROJECT_MISS to the cleanup trap.

The trap at Line 64 tears down only PROJECT_SRC and PROJECT_REC. PROJECT_MISS is torn down at Line 312 and again at Lines 315-318. If the script exits between Line 290 and Line 312, the F1 containers stay running. Declare PROJECT_MISS before the trap, add it to the loop in cleanup, and delete the redundant PROJECTS_EXTRA block.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-pitr-failure-drill.sh` around lines 278 - 290,
Declare PROJECT_MISS before the cleanup trap so cleanup can access it, add
PROJECT_MISS to the project loop in cleanup alongside PROJECT_SRC and
PROJECT_REC, and remove the later PROJECTS_EXTRA cleanup block and its duplicate
PROJECT_MISS teardown.

162-174: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Run pg_verifybackup once.

Lines 162 and 165 start the same container with the same arguments. The second run only re-derives the exit status. Capture the output and the status in a single invocation.

♻️ Proposed change
-  VRF_OUT="$(docker run --rm -v "${CORRUPT_DIR}:/d:ro" \
-    postgres:18.4-bookworm pg_verifybackup /d 2>&1 || true)"
-  # pg_verifybackup exits non-zero and emits a message about the mismatch.
-  if docker run --rm -v "${CORRUPT_DIR}:/d:ro" \
-      postgres:18.4-bookworm pg_verifybackup /d >/dev/null 2>&1; then
+  VRF_STATUS=0
+  VRF_OUT="$(docker run --rm -v "${CORRUPT_DIR}:/d:ro" \
+    postgres:18.4-bookworm pg_verifybackup /d 2>&1)" || VRF_STATUS=$?
+  if [ "${VRF_STATUS}" -eq 0 ]; then
     fail "F2 corrupt-backup detection — pg_verifybackup accepted a tampered file"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-pitr-failure-drill.sh` around lines 162 - 174,
Update the F2 corrupt-backup verification flow to invoke pg_verifybackup only
once, capturing both its combined output and exit status from that invocation.
Use the captured status for the acceptance/failure check while preserving the
existing diagnostic-message matching and pass messages.
docker-compose.pitr.yml (1)

31-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove the empty POSTGRES_INITDB_ARGS override and the misleading comment.

The comment states that this block enables continuous archiving. Setting POSTGRES_INITDB_ARGS to an empty string does not enable archiving. It only overwrites any value inherited from the base compose file or the environment.

♻️ Proposed cleanup
   db:
-    environment:
-      # Enable continuous archiving. archive_mode is a postmaster-level
-      # setting (requires restart, not just reload).
-      POSTGRES_INITDB_ARGS: ""
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.pitr.yml` around lines 31 - 34, Remove the empty
POSTGRES_INITDB_ARGS entry and its misleading continuous-archiving comment from
the environment block; leave inherited or externally provided configuration
untouched.
apps/web/src/pages/LaunchpadPage.tsx (1)

117-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Extract the shared page shell.

The loading branch at Lines 120-134 and the main return at Lines 138-146 repeat the same main, PageContainer, Card, CardHeader, and BrandHeader structure. A later change to the shell must be applied twice.

Extract a local Shell wrapper and render the loading state as <Shell /> and the form as <Shell>{form}</Shell>.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/pages/LaunchpadPage.tsx` around lines 117 - 146, In the
LaunchpadPage component, extract the duplicated
main/PageContainer/Card/CardHeader/BrandHeader structure into a local Shell
wrapper. Replace the loading branch with <Shell /> and wrap the existing form
content with <Shell>{form}</Shell>, preserving the current loading and form
behavior.
apps/api/src/routes/launchpad.test.ts (1)

36-72: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The hardcoded table list will drift from the schema.

The TRUNCATE statement names 28 tables literally. A new table added later is not truncated, so a stale row can leave the installation in an initialized state and make the bootstrap tests fail in a way that is hard to diagnose.

Derive the list from information_schema.tables and exclude the Drizzle metadata tables, or reuse the existing truncateBusinessTables helper referenced in the doc comment.

♻️ Proposed refactor
 async function resetToUninitialized(ctx: TestContext): Promise<void> {
-  await ctx.db.execute(
-    sql.raw(`
-      TRUNCATE
-        organizations,
-        ...
-      RESTART IDENTITY CASCADE
-    `),
-  );
+  await ctx.db.execute(
+    sql.raw(`
+      DO $$
+      DECLARE stmt text;
+      BEGIN
+        SELECT string_agg(format('%I.%I', schemaname, tablename), ', ')
+          INTO stmt
+          FROM pg_tables
+         WHERE schemaname = 'public'
+           AND tablename NOT LIKE '__drizzle%';
+        IF stmt IS NOT NULL THEN
+          EXECUTE 'TRUNCATE ' || stmt || ' RESTART IDENTITY CASCADE';
+        END IF;
+      END $$;
+    `),
+  );
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/launchpad.test.ts` around lines 36 - 72, Update
resetToUninitialized to avoid its hardcoded table list: reuse the existing
truncateBusinessTables helper if available, or derive tables from
information_schema.tables while excluding Drizzle metadata tables, then truncate
the resulting business-table list with identity reset and cascade.
apps/api/src/routes/launchpad.ts (1)

24-33: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Compare fixed-length digests instead of raw buffers.

The length-mismatch branch returns before the real comparison. The self-compare uses bBuf, so its duration follows the configured token length and not the candidate length. The comment claims wall-clock independence from length, which the current code does not provide.

A SHA-256 digest comparison produces two 32-byte buffers, so timingSafeEqual always runs on equal lengths and no length branch is needed. The practical risk here is low because the token is high entropy and the route is rate limited, but the simpler form also matches the documented intent.

♻️ Proposed refactor
-import { timingSafeEqual } from "node:crypto";
+import { createHash, timingSafeEqual } from "node:crypto";
@@
 function constantTimeEqual(a: string, b: string): boolean {
-  const aBuf = Buffer.from(a, "utf8");
-  const bBuf = Buffer.from(b, "utf8");
-  if (aBuf.length !== bBuf.length) {
-    // Still do a comparison to keep wall-clock time independent of length.
-    timingSafeEqual(bBuf, bBuf);
-    return false;
-  }
-  return timingSafeEqual(aBuf, bBuf);
+  // Digests are always 32 bytes, so the comparison never branches on the
+  // secret length.
+  const aDigest = createHash("sha256").update(a, "utf8").digest();
+  const bDigest = createHash("sha256").update(b, "utf8").digest();
+  return timingSafeEqual(aDigest, bDigest);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/launchpad.ts` around lines 24 - 33, Update
constantTimeEqual to hash both input strings with SHA-256 and pass the resulting
fixed-length digests to timingSafeEqual. Remove the raw-buffer length check and
self-comparison so every call performs one equal-length digest comparison.
scripts/deployment/p7-c1-cold-backup-restore-drill.sh (1)

128-133: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert the application starts against the restored data root.

The drill only waits for the database after the restore. The stated goal (Lines 19-21) is a fresh working deployment. Call wait_for_app after wait_for_db so a failing application start fails the drill.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c1-cold-backup-restore-drill.sh` around lines 128 -
133, After wait_for_db in the RESTORE_ROOT startup flow, call wait_for_app
before capture so the drill fails when the application cannot start against
restored data.
scripts/backup/cold-filesystem-backup.sh (3)

119-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Create the destination after the confirmation prompt.

mkdir -p "${DEST}/postgres" runs at Line 127, before the prompt at Line 136. If the operator aborts with Ctrl-C, ${DEST} remains. The next attempt then fails at the "destination already exists" check (Lines 96-100) and the operator must delete the directory by hand. Move the mkdir call below the prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/cold-filesystem-backup.sh` around lines 119 - 136, Move the
mkdir -p "${DEST}/postgres" call in the cold-filesystem backup flow to after the
read -r _confirm prompt, leaving the destination-parent validation and
informational output before confirmation. Ensure aborting before confirmation
does not create or leave the destination directory.

144-148: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pin the helper image and pre-load it.

alpine:latest is unpinned and is pulled from Docker Hub on first use. See the consolidated comment for the required change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/cold-filesystem-backup.sh` around lines 144 - 148, Update the
Docker invocation in the cold filesystem backup flow to use the pinned Alpine
helper image version specified by the consolidated review guidance instead of
alpine:latest, and add the required pre-load step before docker run so the image
is available locally without an implicit Docker Hub pull.

Source: Coding guidelines


102-117: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Detect a running or uncleanly stopped cluster before copying.

The script documents that PostgreSQL must be stopped but never checks. The validation container already reads PGDATA, so add a postmaster.pid check there. If postmaster.pid exists, the cluster is running or was not shut down cleanly, and the resulting copy is not a trustworthy cold backup.

Also, the header comment at Lines 70-72 states that validate_path rejects .. traversal. No such check exists. Add the check or correct the comment.

🛡️ Proposed guard
 PGDATA_DIR="$(dirname "${PGDATA_SUBDIR}")"
+if docker run --rm -v "${SRC_PG}:/pg:ro" alpine:latest \
+  sh -c "test -f '${PGDATA_DIR}/postmaster.pid'" 2>/dev/null; then
+  echo "FAIL: ${PGDATA_DIR}/postmaster.pid exists." >&2
+  echo "       PostgreSQL is running or was not stopped cleanly. Stop it first." >&2
+  exit 2
+fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/cold-filesystem-backup.sh` around lines 102 - 117, Add a
cold-backup guard to the PGDATA validation flow: use the existing Docker helper
container and resolved PGDATA_DIR to fail when postmaster.pid exists, before
copying begins. Also reconcile the validate_path documentation with its
implementation by either adding the promised .. traversal rejection or
correcting the header comment if that validation is intentionally absent.
scripts/backup/pg-basebackup.sh (2)

137-146: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Pin the images used by the backup path.

pg_verifybackup runs postgres:18.4-bookworm and the size report runs alpine:latest. Both are pulled from Docker Hub when absent. See the consolidated comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/pg-basebackup.sh` around lines 137 - 146, Pin the Docker
images used in the backup verification and size-report commands instead of using
mutable tags. Update the postgres image in the pg_verifybackup invocation and
the alpine image in the SIZE calculation to immutable version or digest
references, preserving their existing command behavior.

Source: Coding guidelines


116-128: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Align the pg_basebackup docs with the actual host-based replication path.

The script starts a separate backup container and connects with -h 127.0.0.1 -U "${PGUSER:-exam}", so this is TCP replication authentication, not a container-local peer path. Update the L19-23/L95-100 comments to match the implementation: the bundled single-node path uses the db container loopback namespace with the configured exam superuser, while external deployments should use a scope-limited replication role.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/pg-basebackup.sh` around lines 116 - 128, Update the comments
around the bundled and external backup authentication paths to match the
pg_basebackup invocation: document that the bundled single-node flow uses the
backup container’s database loopback namespace and configured exam superuser
over TCP, while external deployments should use a scope-limited replication role
rather than container-local peer authentication.
scripts/deployment/p7-c1-persistence-smoke.sh (1)

88-97: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Host-side rm -rf cannot delete the uid-999 PGDATA files.

The script correctly uses a container to copy and inspect these files (Lines 217-221, Lines 242-243), but cleanup removes them from the host. A non-root host user gets EACCES, and || true hides it, so each run leaks a full data directory. Use the same container-assisted removal as scripts/deployment/p7-c1-cold-backup-restore-drill.sh (Lines 66-70).

The /tmp/... guard also assumes TMPDIR is unset; see the consolidated comment.

♻️ Container-assisted removal
   for d in "${CREATED_DIRS[@]}"; do
     if [ -n "${d}" ] && [ -d "${d}" ] \
       && printf '%s\n' "${d}" | grep -Eq '/tmp/p7c1-persist-[AB]-[A-Za-z0-9_-]+$'; then
+      docker run --rm -v "${d}:/d" alpine:latest \
+        sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true' > /dev/null 2>&1 || true
       rm -rf "${d}" > /dev/null 2>&1 || true
     fi
   done
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c1-persistence-smoke.sh` around lines 88 - 97, Update
the cleanup loop over CREATED_DIRS to remove validated temporary directories
through the same container-assisted removal mechanism used by
p7-c1-cold-backup-restore-drill.sh, so uid-999 PGDATA files are deleted without
relying on host permissions. Preserve the existing non-empty, directory, and
script-created-path safety checks, and ensure the validation supports the
configured TMPDIR rather than assuming /tmp.
scripts/deployment/p7-c2-logical-restore-drill.sh (2)

172-181: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Compare the restored state against the captured State A.

The drill captures STATE_A at Line 130 and STATE_B at Line 144, then asserts hard-coded counts of 1. Both variables are otherwise unused. Compare capture_state output against STATE_A (with the B marker expected absent) so the assertion follows the recorded baseline instead of constants that drift when bootstrap behavior changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c2-logical-restore-drill.sh` around lines 172 - 181,
Update the restored-state validation after the RESTORED_ORGS, RESTORED_ADMINS,
and RESTORED_AUDIT queries to compare the capture_state output against the
recorded STATE_A baseline instead of hard-coded counts. Reuse STATE_A for the
organization, admin, and audit expectations, while separately verifying the
STATE_B marker is absent; remove the unused-state issue and preserve failure
diagnostics for mismatches.

149-149: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Delete the no-op line.

ADMIN_USER is already used at Line 124 and Line 192, so this statement suppresses nothing. It is a debug artifact.

♻️ Proposed removal
-echo "${ADMIN_USER%%_*}" >/dev/null  # no-op to keep shell lint happy
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c2-logical-restore-drill.sh` at line 149, Remove the
no-op echo statement involving ADMIN_USER. Keep the existing ADMIN_USER usages
elsewhere unchanged.
scripts/deployment/p6-corr1-compose-smoke.sh (1)

113-124: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Cleanup cannot remove the PGDATA files it created.

The bind-mounted PostgreSQL files are owned by the container postgres user (uid 999). A host-side rm -rf by a non-root user fails with EACCES, and || true hides the failure. Each smoke run then leaks a full data directory. scripts/deployment/p7-c1-cold-backup-restore-drill.sh (Lines 66-70) already uses a container-assisted removal; apply the same approach here.

The ^/tmp/ guard is also unreliable when TMPDIR is set; see the consolidated comment.

♻️ Container-assisted removal
   if [ -n "${EXAM_DATA_ROOT:-}" ] \
     && [ -d "${EXAM_DATA_ROOT}" ] \
     && printf '%s\n' "${EXAM_DATA_ROOT}" | grep -q '^/tmp/p6corr1-smoke-data-'; then
+    docker run --rm -v "${EXAM_DATA_ROOT}:/d" "${HELPER_IMAGE}" \
+      sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true' > /dev/null 2>&1 || true
     rm -rf "${EXAM_DATA_ROOT}" > /dev/null 2>&1 || true
   fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p6-corr1-compose-smoke.sh` around lines 113 - 124, Update
the cleanup block in scripts/deployment/p6-corr1-compose-smoke.sh to remove
EXAM_DATA_ROOT through a temporary container, matching the container-assisted
removal approach used by p7-c1-cold-backup-restore-drill.sh so root-owned
PostgreSQL files are deleted. Replace the hard-coded ^/tmp/p6corr1-smoke-data-
validation with a guard that validates the actual mktemp-created path under the
configured TMPDIR and prevents empty, repository-root, or unowned paths from
being removed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docker-compose.pitr.yml`:
- Line 45: Rename the WAL archive seed from docker/pitr/wal-archive.conf to
docker/pitr/wal-archive.sql, convert its leading # comments to SQL -- comments,
and update the mount target at docker-compose.pitr.yml lines 45-45 to
/docker-entrypoint-initdb.d/99-pitr-wal-archive.sql. Verify end to end with a
fresh PITR cluster that archive_mode is on and a WAL segment is actually
archived; do not rely on file contents alone.

In `@docker-compose.yml`:
- Around line 100-103: Document the existing-install migration in the operator
runbook: before starting the updated stack, stop PostgreSQL, copy the contents
of the existing pgdata named volume into ${EXAM_DATA_ROOT}/postgres, and then
start the stack with the bind mount. Ensure the runbook clearly warns that
skipping this preserves data only in the orphaned volume while initializing an
empty cluster.

In `@docker/pitr/wal-archive.conf`:
- Around line 26-28: Document the host-side preparation required for
/wal-archive in both this configuration and
docs/deployment/backup-and-recovery.md: ensure the bind-mounted directory is
created with ownership by PostgreSQL uid 999 (or an equivalently restrictive
writable setup) before starting the container, rather than relying on 0777
permissions. Keep the existing archive_command and ALTER SYSTEM settings
unchanged.

In `@docs/deployment/backup-and-recovery.md`:
- Line 183: Update the cold backup command in
docs/deployment/backup-and-recovery.md lines 183-183 to pass
"${EXAM_DATA_ROOT:-./data}" instead of ./data; make the same replacement in
docs/deployment/mvp-deployment-runbook.md lines 804-809 so both examples use the
resolved EXAM_DATA_ROOT with the existing fallback.
- Around line 539-551: Remove administrator passwords from command-line
arguments in the bootstrap and reset procedures:
docs/deployment/backup-and-recovery.md lines 539-551 must use an interactive
prompt or protected stdin/file input for both commands, and
docs/deployment/mvp-deployment-runbook.md lines 174-177 must update its
bootstrap fallback to the same safe input method.

In `@packages/db/src/repository/organizationRepo.ts`:
- Around line 118-124: Add ctx: RequestContext as the first parameter of
defaultOrganizationExists, preserving its existing query behavior. In
apps/api/src/routes/launchpad.ts, create the request context and pass it to
defaultOrganizationExists(ctx), ensuring the repository call follows the
standard contract.

In `@scripts/backup/cold-filesystem-restore.sh`:
- Around line 127-131: Introduce one overridable EXAM_BACKUP_HELPER_IMAGE
variable with a digest-pinned default and use it in cold-filesystem-restore.sh
lines 127-131, cold-filesystem-backup.sh lines 144-148 and its validation
containers at lines 106 and 113, and the du container in pg-basebackup.sh lines
137-146; keep the postgres image for pg_verifybackup digest-pinned separately.
Before any helper image use, run docker image inspect and fail clearly if it is
unavailable locally, then document docker save/docker load preloading in
docs/deployment/backup-and-recovery.md so recovery remains offline-capable.

In `@scripts/backup/postgres-logical-backup.sh`:
- Line 110: Update the Docker invocation around the backup commands to avoid
expanding PGPASSWORD into docker exec arguments. Pass the variable by name
through the existing environment or use a securely permissioned temporary
env-file when value expansion is required, and apply the same fix to both
affected invocations.

In `@scripts/backup/postgres-logical-restore.sh`:
- Around line 112-126: Validate TARGET_DB before any SQL or shell interpolation,
allowing only a plain PostgreSQL identifier with an appropriate safe character
pattern and rejecting all other values; retain the existing reserved-name
checks. Update the DROP DATABASE statement in the restore flow to use WITH
(FORCE), preserving the recreate-and-restore sequence and preventing connected
clients from interrupting it.

In `@scripts/deployment/p7-c1-cold-backup-restore-drill.sh`:
- Around line 98-102: Update the capture function to invoke psql with
ON_ERROR_STOP=1 and validate that its result is non-empty, failing the drill
when the query errors or returns no evidence. Ensure the before/after comparison
and PASS path around INV_BEFORE and INV_AFTER cannot treat two empty captures as
a successful restore.

In `@scripts/deployment/p7-c3-pitr-failure-drill.sh`:
- Around line 297-311: Tighten the F1 validation around MISS_LOGS and pg_isready
so it requires evidence of an actual restore_command/WAL retrieval failure
rather than generic recovery terms. Record or match the concrete missing-segment
failure message, and assert that the recovery node remains unavailable; do not
pass solely because broad log patterns appear or because the node starts with
non-specific restore errors.

---

Minor comments:
In `@apps/api/src/routes/launchpad.ts`:
- Around line 128-177: Wrap the bootstrapAdminOnFreshDb call in the route
handler with failure handling that re-checks isInstallationInitialized() after
an error; when the installation is now initialized, return the existing 409
LAUNCHPAD_ALREADY_INITIALIZED response, otherwise rethrow the original error so
unrelated failures remain 500.
- Around line 73-75: Update isInstallationInitialized and the organization
repository call so defaultOrganizationExists follows the ctx-first repository
contract: pass an appropriate pre-tenant context or explicitly accept the
ctx-less first-install probe in the repository API if that behavior is
intentional. Keep the route using createOrganizationRepo and preserve its
boolean initialization result.

In `@apps/web/src/pages/LaunchpadPage.tsx`:
- Around line 75-88: Update the validate function to add field-level
minimum-length checks for adminUsername (3 characters) and adminPassword (8
characters), while preserving the existing required checks. Add the
corresponding launchpad.adminUsernameMinLength and
launchpad.adminPasswordMinLength translations to zh-CN.ts and use them for the
validation errors.

In `@docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md`:
- Around line 140-146: Update the final closeout report fields around the
existing “Final gates on the final head” section to explicitly report modified
files, new-test results, coverage, and `pnpm verify` results, stating when any
item was not collected or not run. Preserve the existing gate results and
limitations.

In `@docs/deployment/backup-and-recovery.md`:
- Around line 267-276: Update the clean-target contract description to remove
“byte-for-byte” language and state that the C2 restore via DROP DATABASE, CREATE
DATABASE ... TEMPLATE template0, and pg_restore reproduces the dump’s logical
contents and validated business invariants. Preserve the explanation that
State-B-only data is absent and adjust the automated drill wording accordingly.
- Around line 44-51: Update the deployment documentation to distinguish the host
PostgreSQL data root from the image-managed PGDATA directory: describe
data/postgres as the host mount parent, and reserve PGDATA for the nested
postgres/18/docker directory used by the PostgreSQL image. Preserve the warning
that deleting the host data root destroys authoritative Exam state.

In `@docs/deployment/mvp-deployment-runbook.md`:
- Around line 179-183: Update the backup-and-recovery cross-reference in the
Launchpad first-install instructions to point to §11 instead of §8, leaving the
rest of the setup procedure unchanged.

In `@docs/roadmap/current.md`:
- Line 15: Update the P7 roadmap entry’s config-control-plane reference to use
the correct Workstream E designation, removing the incorrect “(P7-E)”
association while preserving the rest of the status and workstream text.

In `@scripts/backup/postgres-logical-backup.sh`:
- Around line 108-113: Update the backup flow around TS and the pg_dump
redirect: use TS in the destination artifact name to preserve the promised
timestamped naming, then install an EXIT trap before pg_dump that removes DEST
when the command fails. Clear the trap with trap - EXIT only after all
verification steps succeed, while preserving the existing destination-exists
checks.

In `@scripts/deployment/p7-c1-persistence-smoke.sh`:
- Around line 92-97: Cleanup guards incorrectly hard-code /tmp, so they skip
directories created under TMPDIR. In
scripts/deployment/p7-c1-persistence-smoke.sh lines 92-97,
scripts/deployment/p6-corr1-compose-smoke.sh lines 120-124,
scripts/deployment/p7-c1-cold-backup-restore-drill.sh lines 63-72, and
scripts/deployment/p7-c2-logical-restore-drill.sh lines 59-66, replace the
absolute-path regexes with basename-prefix validation using each script’s
recorded directory variable, while retaining the existing directory and cleanup
safety checks.

In `@scripts/deployment/p7-c3-pitr-drill.sh`:
- Around line 200-207: Update the recovery configuration block that appends to
postgresql.auto.conf to explicitly disable archiving in the recovered cluster,
overriding the inherited archive_mode/archive_command settings before promotion.
Preserve the existing restore_command and recovery target settings.

In `@scripts/deployment/p7-c3-pitr-failure-drill.sh`:
- Around line 232-245: In the F3 validation block, rename INVALID_OK to
REC_BECAME_READY consistently and update the condition so readiness remains the
failure case. Tighten the fallback grep in the docker compose logs check to
accept only FATAL or PANIC lines that also identify recovery_target_lsn or the
relevant configuration file, removing broad matches for error and stopped.

---

Nitpick comments:
In `@apps/api/src/routes/launchpad.test.ts`:
- Around line 36-72: Update resetToUninitialized to avoid its hardcoded table
list: reuse the existing truncateBusinessTables helper if available, or derive
tables from information_schema.tables while excluding Drizzle metadata tables,
then truncate the resulting business-table list with identity reset and cascade.

In `@apps/api/src/routes/launchpad.ts`:
- Around line 24-33: Update constantTimeEqual to hash both input strings with
SHA-256 and pass the resulting fixed-length digests to timingSafeEqual. Remove
the raw-buffer length check and self-comparison so every call performs one
equal-length digest comparison.

In `@apps/web/src/pages/LaunchpadPage.tsx`:
- Around line 117-146: In the LaunchpadPage component, extract the duplicated
main/PageContainer/Card/CardHeader/BrandHeader structure into a local Shell
wrapper. Replace the loading branch with <Shell /> and wrap the existing form
content with <Shell>{form}</Shell>, preserving the current loading and form
behavior.

In `@docker-compose.pitr.yml`:
- Around line 31-34: Remove the empty POSTGRES_INITDB_ARGS entry and its
misleading continuous-archiving comment from the environment block; leave
inherited or externally provided configuration untouched.

In `@scripts/backup/cold-filesystem-backup.sh`:
- Around line 119-136: Move the mkdir -p "${DEST}/postgres" call in the
cold-filesystem backup flow to after the read -r _confirm prompt, leaving the
destination-parent validation and informational output before confirmation.
Ensure aborting before confirmation does not create or leave the destination
directory.
- Around line 144-148: Update the Docker invocation in the cold filesystem
backup flow to use the pinned Alpine helper image version specified by the
consolidated review guidance instead of alpine:latest, and add the required
pre-load step before docker run so the image is available locally without an
implicit Docker Hub pull.
- Around line 102-117: Add a cold-backup guard to the PGDATA validation flow:
use the existing Docker helper container and resolved PGDATA_DIR to fail when
postmaster.pid exists, before copying begins. Also reconcile the validate_path
documentation with its implementation by either adding the promised .. traversal
rejection or correcting the header comment if that validation is intentionally
absent.

In `@scripts/backup/pg-basebackup.sh`:
- Around line 137-146: Pin the Docker images used in the backup verification and
size-report commands instead of using mutable tags. Update the postgres image in
the pg_verifybackup invocation and the alpine image in the SIZE calculation to
immutable version or digest references, preserving their existing command
behavior.
- Around line 116-128: Update the comments around the bundled and external
backup authentication paths to match the pg_basebackup invocation: document that
the bundled single-node flow uses the backup container’s database loopback
namespace and configured exam superuser over TCP, while external deployments
should use a scope-limited replication role rather than container-local peer
authentication.

In `@scripts/deployment/p6-corr1-compose-smoke.sh`:
- Around line 113-124: Update the cleanup block in
scripts/deployment/p6-corr1-compose-smoke.sh to remove EXAM_DATA_ROOT through a
temporary container, matching the container-assisted removal approach used by
p7-c1-cold-backup-restore-drill.sh so root-owned PostgreSQL files are deleted.
Replace the hard-coded ^/tmp/p6corr1-smoke-data- validation with a guard that
validates the actual mktemp-created path under the configured TMPDIR and
prevents empty, repository-root, or unowned paths from being removed.

In `@scripts/deployment/p7-c1-cold-backup-restore-drill.sh`:
- Around line 128-133: After wait_for_db in the RESTORE_ROOT startup flow, call
wait_for_app before capture so the drill fails when the application cannot start
against restored data.

In `@scripts/deployment/p7-c1-persistence-smoke.sh`:
- Around line 88-97: Update the cleanup loop over CREATED_DIRS to remove
validated temporary directories through the same container-assisted removal
mechanism used by p7-c1-cold-backup-restore-drill.sh, so uid-999 PGDATA files
are deleted without relying on host permissions. Preserve the existing
non-empty, directory, and script-created-path safety checks, and ensure the
validation supports the configured TMPDIR rather than assuming /tmp.

In `@scripts/deployment/p7-c2-logical-restore-drill.sh`:
- Around line 172-181: Update the restored-state validation after the
RESTORED_ORGS, RESTORED_ADMINS, and RESTORED_AUDIT queries to compare the
capture_state output against the recorded STATE_A baseline instead of hard-coded
counts. Reuse STATE_A for the organization, admin, and audit expectations, while
separately verifying the STATE_B marker is absent; remove the unused-state issue
and preserve failure diagnostics for mismatches.
- Line 149: Remove the no-op echo statement involving ADMIN_USER. Keep the
existing ADMIN_USER usages elsewhere unchanged.

In `@scripts/deployment/p7-c3-pitr-drill.sh`:
- Around line 131-134: Track whether the health-check loop in the deployment
script succeeded, and after the loop explicitly fail with a clear error if the
app never becomes healthy within 90 attempts. Only run bootstrap-admin.js after
a successful health check.
- Around line 61-63: Split the declarations and command substitutions for
POSTGRES_PASSWORD and JWT_SECRET in the deployment script: assign each generated
value first, then export the variable separately. Preserve the existing
generated prefixes and openssl arguments while ensuring openssl failures are not
masked.
- Around line 227-232: Replace the unused loop variable i in the pg_isready
retry loop with _, matching the existing loop convention elsewhere in the script
and eliminating ShellCheck SC2034.

In `@scripts/deployment/p7-c3-pitr-failure-drill.sh`:
- Around line 278-290: Declare PROJECT_MISS before the cleanup trap so cleanup
can access it, add PROJECT_MISS to the project loop in cleanup alongside
PROJECT_SRC and PROJECT_REC, and remove the later PROJECTS_EXTRA cleanup block
and its duplicate PROJECT_MISS teardown.
- Around line 162-174: Update the F2 corrupt-backup verification flow to invoke
pg_verifybackup only once, capturing both its combined output and exit status
from that invocation. Use the captured status for the acceptance/failure check
while preserving the existing diagnostic-message matching and pass messages.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b4d3bc05-2535-4ed9-973c-ce486b646f80

📥 Commits

Reviewing files that changed from the base of the PR and between 2a1a9eb and 0ebdf6e.

📒 Files selected for processing (37)
  • .gitignore
  • README.md
  • apps/api/openapi.json
  • apps/api/src/authz/routeRegistryConformanceWholeApp.test.ts
  • apps/api/src/config/runtimeConfig.ts
  • apps/api/src/routes/launchpad.test.ts
  • apps/api/src/routes/launchpad.ts
  • apps/api/src/routes/registerApiRoutes.ts
  • apps/web/src/App.tsx
  • apps/web/src/i18n/locales/zh-CN.ts
  • apps/web/src/lib/pageMeta.ts
  • apps/web/src/lib/routes.ts
  • apps/web/src/pages/LaunchpadPage.test.tsx
  • apps/web/src/pages/LaunchpadPage.tsx
  • docker-compose.pitr.yml
  • docker-compose.yml
  • docker/pitr/wal-archive.conf
  • docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md
  • docs/deployment/backup-and-recovery.md
  • docs/deployment/mvp-deployment-runbook.md
  • docs/roadmap/P7-system-readiness-and-exam-modes.md
  • docs/roadmap/current.md
  • packages/contracts/src/index.ts
  • packages/contracts/src/launchpad.ts
  • packages/contracts/src/messageRegistry.ts
  • packages/db/src/repository/organizationRepo.ts
  • scripts/backup/cold-filesystem-backup.sh
  • scripts/backup/cold-filesystem-restore.sh
  • scripts/backup/pg-basebackup.sh
  • scripts/backup/postgres-logical-backup.sh
  • scripts/backup/postgres-logical-restore.sh
  • scripts/deployment/p6-corr1-compose-smoke.sh
  • scripts/deployment/p7-c1-cold-backup-restore-drill.sh
  • scripts/deployment/p7-c1-persistence-smoke.sh
  • scripts/deployment/p7-c2-logical-restore-drill.sh
  • scripts/deployment/p7-c3-pitr-drill.sh
  • scripts/deployment/p7-c3-pitr-failure-drill.sh

Comment thread docker-compose.pitr.yml Outdated
Comment thread docker-compose.yml Outdated
Comment on lines +100 to +103
# P7-C1: authoritative state is an operator-visible host bind mount.
# Containers are disposable; this host directory is not. The official
# postgres image owns its internal PGDATA layout under this path.
- ${EXAM_DATA_ROOT:-./data}/postgres:/var/lib/postgresql

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Document the named-volume to bind-mount migration for existing deployments.

Existing production stacks keep authoritative data in the pgdata named volume. After this change, Compose mounts an empty host directory and the postgres image initializes a new cluster. The old data remains in the orphaned volume and the deployment looks empty. Confirm that the upgrade procedure (copy pgdata contents into ${EXAM_DATA_ROOT}/postgres while PostgreSQL is stopped) is documented in the operator runbook before release.

#!/bin/bash
# Verify the volume -> bind-mount migration is documented for existing installs.
fd -t f -e md . docs README.md 2>/dev/null | xargs rg -n -C3 -i 'pgdata|named volume|docker volume|EXAM_DATA_ROOT' || true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.yml` around lines 100 - 103, Document the existing-install
migration in the operator runbook: before starting the updated stack, stop
PostgreSQL, copy the contents of the existing pgdata named volume into
${EXAM_DATA_ROOT}/postgres, and then start the stack with the bind mount. Ensure
the runbook clearly warns that skipping this preserves data only in the orphaned
volume while initializing an empty cluster.

Comment thread docker/pitr/wal-archive.conf Outdated
Comment thread docs/deployment/backup-and-recovery.md Outdated
Comment on lines +539 to +551
```bash
docker compose exec app \
node dist/scripts/bootstrap-admin.js \
--username admin --password '<STRONG_OPERATOR_PASSWORD>' \
--name 'System Admin' --organization-name 'My Organization'
```

### 11.3 Reset an Admin's password

```bash
docker compose exec app \
node dist/scripts/reset-admin-password.js \
--username admin --password '<NEW_STRONG_PASSWORD>'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Remove administrator passwords from command-line arguments. Both procedures expose credentials through shell history and process inspection.

  • docs/deployment/backup-and-recovery.md#L539-L551: update bootstrap and password-reset procedures to use an interactive prompt or protected stdin/file input.
  • docs/deployment/mvp-deployment-runbook.md#L174-L177: update the bootstrap fallback to use the same safe input method.
📍 Affects 2 files
  • docs/deployment/backup-and-recovery.md#L539-L551 (this comment)
  • docs/deployment/mvp-deployment-runbook.md#L174-L177
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/deployment/backup-and-recovery.md` around lines 539 - 551, Remove
administrator passwords from command-line arguments in the bootstrap and reset
procedures: docs/deployment/backup-and-recovery.md lines 539-551 must use an
interactive prompt or protected stdin/file input for both commands, and
docs/deployment/mvp-deployment-runbook.md lines 174-177 must update its
bootstrap fallback to the same safe input method.

Comment on lines +127 to +131
docker run --rm \
-v "${SRC_PG}:/from:ro" \
-v "${DEST_PG}:/to" \
alpine:latest \
sh -c 'cp -a /from/. /to/'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Backup and recovery scripts pull unpinned images from Docker Hub. All three helpers call docker run with alpine:latest or postgres:18.4-bookworm. Docker pulls the image when it is not cached locally, so the procedures require internet access at the moment of recovery and produce non-reproducible results. Introduce a single overridable variable (for example EXAM_BACKUP_HELPER_IMAGE) with a digest-pinned default, verify the image exists locally with docker image inspect before use, and document the pre-load step (docker save / docker load) in docs/deployment/backup-and-recovery.md.

  • scripts/backup/cold-filesystem-restore.sh#L127-L131: replace alpine:latest with the pinned helper image variable and fail early with a clear message when the image is absent locally.
  • scripts/backup/cold-filesystem-backup.sh#L144-L148: use the same pinned helper image variable for the copy step and for the validation containers at Lines 106 and 113.
  • scripts/backup/pg-basebackup.sh#L137-L146: pin the postgres image used for pg_verifybackup by digest and use the shared helper image variable for the du container.

As per coding guidelines: "The product must remain LAN/on-premise and offline-capable; do not add cloud services, CDNs, external APIs, telemetry, or other runtime internet dependencies."

📍 Affects 3 files
  • scripts/backup/cold-filesystem-restore.sh#L127-L131 (this comment)
  • scripts/backup/cold-filesystem-backup.sh#L144-L148
  • scripts/backup/pg-basebackup.sh#L137-L146
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/cold-filesystem-restore.sh` around lines 127 - 131, Introduce
one overridable EXAM_BACKUP_HELPER_IMAGE variable with a digest-pinned default
and use it in cold-filesystem-restore.sh lines 127-131,
cold-filesystem-backup.sh lines 144-148 and its validation containers at lines
106 and 113, and the du container in pg-basebackup.sh lines 137-146; keep the
postgres image for pg_verifybackup digest-pinned separately. Before any helper
image use, run docker image inspect and fail clearly if it is unavailable
locally, then document docker save/docker load preloading in
docs/deployment/backup-and-recovery.md so recovery remains offline-capable.

Source: Coding guidelines

Comment thread scripts/backup/postgres-logical-backup.sh
Comment on lines +112 to +126
docker exec -i -e PGPASSWORD="${PGPASSWORD:-}" "${DB_CONTAINER}" \
sh -c 'psql -U "$POSTGRES_USER" -d postgres -v ON_ERROR_STOP=1' <<SQL
DROP DATABASE IF EXISTS "${TARGET_DB}";
CREATE DATABASE "${TARGET_DB}" TEMPLATE template0;
SQL

echo "Restoring dump into clean '${TARGET_DB}'..."
# pg_restore into the recreated database. --no-owner (matches the dump) so the
# restore is portable across the role that owns objects. --exit-on-error makes
# any restore error fail the script immediately (no partial silent restore).
docker exec \
-e PGPASSWORD="${PGPASSWORD:-}" \
-i "${DB_CONTAINER}" \
sh -c 'pg_restore -U "$POSTGRES_USER" -d '"${TARGET_DB}"' --no-owner --exit-on-error' \
< "${DUMP}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Validate TARGET_DB before it reaches SQL and the container shell.

TARGET_DB is spliced into an unquoted heredoc at Lines 114-115 and into the sh -c command string at Line 125. Only three system names are rejected (Lines 63-68). A value containing ;, ", backticks, or $(...) becomes executable SQL or shell code inside the db container, in a script that already runs destructive statements as the PostgreSQL superuser. Restrict the argument to a plain identifier.

DROP DATABASE also fails while any session is connected. app and email-worker use restart: unless-stopped, so one reconnecting client makes the restore fail midway. Use WITH (FORCE) (PostgreSQL 13+).

🔒 Proposed fix
 case "${TARGET_DB}" in
   postgres|template0|template1)
     echo "FAIL: refusing to DROP the system database '${TARGET_DB}'." >&2
     exit 2
     ;;
 esac
+if ! printf '%s' "${TARGET_DB}" | grep -Eq '^[A-Za-z_][A-Za-z0-9_]{0,62}$'; then
+  echo "FAIL: TARGET_DB '${TARGET_DB}' is not a plain SQL identifier." >&2
+  exit 2
+fi
-DROP DATABASE IF EXISTS "${TARGET_DB}";
+DROP DATABASE IF EXISTS "${TARGET_DB}" WITH (FORCE);
 CREATE DATABASE "${TARGET_DB}" TEMPLATE template0;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/postgres-logical-restore.sh` around lines 112 - 126, Validate
TARGET_DB before any SQL or shell interpolation, allowing only a plain
PostgreSQL identifier with an appropriate safe character pattern and rejecting
all other values; retain the existing reserved-name checks. Update the DROP
DATABASE statement in the restore flow to use WITH (FORCE), preserving the
recreate-and-restore sequence and preventing connected clients from interrupting
it.

Comment on lines +98 to +102
capture() {
# orgs : admin_bootstrap_audit : probe label
docker exec "${PROJECT}-db-1" psql -U exam -d exam -tAc \
"SELECT count(*)||':'||(SELECT count(*) FROM audit_logs WHERE action='admin.bootstrap')||':'||(SELECT label FROM p7c1_probe.marker WHERE id=1) FROM organizations;"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The drill can report PASS on two empty results.

capture() runs psql without ON_ERROR_STOP=1 and without a non-empty assertion. If the probe schema or a table is missing after restore, both INV_BEFORE and INV_AFTER can be empty, the comparison at Line 135 succeeds, and the drill reports PASS without evidence. Add ON_ERROR_STOP=1 and reject an empty capture.

💚 Proposed fix
 capture() {
   # orgs : admin_bootstrap_audit : probe label
-  docker exec "${PROJECT}-db-1" psql -U exam -d exam -tAc \
+  local out
+  out="$(docker exec "${PROJECT}-db-1" psql -v ON_ERROR_STOP=1 -U exam -d exam -tAc \
     "SELECT count(*)||':'||(SELECT count(*) FROM audit_logs WHERE action='admin.bootstrap')||':'||(SELECT label FROM p7c1_probe.marker WHERE id=1) FROM organizations;"
+  )"
+  if [ -z "${out}" ]; then
+    echo "FAIL: invariant capture returned no rows." >&2
+    exit 1
+  fi
+  printf '%s\n' "${out}"
 }

Also applies to: 135-138

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c1-cold-backup-restore-drill.sh` around lines 98 - 102,
Update the capture function to invoke psql with ON_ERROR_STOP=1 and validate
that its result is non-empty, failing the drill when the query errors or returns
no evidence. Ensure the before/after comparison and PASS path around INV_BEFORE
and INV_AFTER cannot treat two empty captures as a successful restore.

Comment on lines +297 to +311
MISS_LOGS="$(docker compose -p "${PROJECT_MISS}" -f "${COMPOSE_FILE}" logs --tail=200 db 2>&1 || true)"
if printf '%s' "${MISS_LOGS}" | grep -Eqi 'could not receive data|not found|restore_command failed|could not open file|record|WAL segment|invalid record|unexpected|could not locate| Consistency state not yet reached'; then
pass "F1 missing WAL segment — recovery surfaced the missing segment loudly"
elif ! docker exec "${PROJECT_MISS}-db-1" pg_isready -U exam -d exam >/dev/null 2>&1; then
pass "F1 missing WAL segment — recovery cluster refused to serve (loud failure)"
else
# The cluster did start. Check whether it reached a consistent state. If
# pg_isready says yes but the log still shows repeated restore_command
# failures, the recovery is at least NOT silent. Otherwise treat as fail.
if printf '%s' "${MISS_LOGS}" | grep -Eqi 'restore_command|cp:.*No such|archive'; then
pass "F1 missing WAL segment — restore_command failures visible in logs (loud)"
else
fail "F1 missing WAL segment — recovery silently started with incomplete history"
fi
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== script excerpt == "
sed -n '270,320p' scripts/deployment/p7-c3-pitr-failure-drill.sh || true

echo
echo "== related pg/docker compose references == "
rg -n "pg (18|16|15)|postgres|COMPOSE_FILE|PROJECT_MISS|project_miss|archive recovery|restore_command|F1" docs scripts package.json pnpm-lock.yaml docker-compose.yml docker-compose.*.ya* 2>/dev/null || true

echo
echo "== postgres image versions from project config/files == "
rg -n "postgres:|postgres:" package.json pnpm-lock.yaml *docker*.* 2>/dev/null || true

Repository: jnhu76/exam

Length of output: 50367


🌐 Web query:

PostgreSQL 18 archive recovery "starting archive recovery" "could not restore file" "restore_command" recovery log

💡 Result:

You’re seeing archive recovery start, but PostgreSQL cannot restore the next required file from the WAL archive (“could not restore file … from archive …”), and/or the log shows issues while replaying WAL (e.g., failures related to restoring timeline history such as “00000004.history” / “RECOVERYHISTORY”). In PostgreSQL, archive recovery depends entirely on restore_command to fetch WAL segments and related timeline/history files; misconfiguration or wrong failure semantics in restore_command will stop startup. What the error means 1) In archive recovery (PITR / replica catch-up from archive), PostgreSQL repeatedly requests specific WAL segment and/or timeline history files it needs, and it expects restore_command to retrieve them into the server’s pg_wal/ directory path (the %p destination). The docs state restore_command is “absolutely must specify” and it tells PostgreSQL how to retrieve archived WAL segments, using %f (archived file name) and %p (local destination path) [1]. 2) PostgreSQL determines success/failure by the shell command’s exit status. The docs specify: the command should return a zero exit status only if it succeeded; it “will be asked for file names that are not present in the archive” and must return nonzero in that case; but if the command is “terminated by a signal” or “an error by the shell (such as command not found), then recovery will abort and the server will not start up” [1]. Common root causes for “archive recovery could not restore file …” A) restore_command points to a tool that isn’t installed / not on PATH - A typical log is “could not restore file …: command not found”, which happens if your restore_command references pgbackrest/another tool but that tool isn’t available where PostgreSQL runs (or it needs an absolute path). A real example shows exactly this for “00000004.history” and pgbackrest: the fix was to install it on the standby or use the correct absolute path, or remove stale recovery parameters [2]. B) restore_command returns 0 even though it failed to place the file on disk - Sometimes you can see behavior like “restore_command returned a zero exit status, but stat failed” and then PostgreSQL may later keep trying. One example explains that PostgreSQL tries to restore the timeline history file (e.g., .history) and names it RECOVERYHISTORY locally; if restore_command returns 0 but it doesn’t actually create the destination file in pg_wal, recovery will fail in an unusual way [3]. - So your restore_command must both (1) succeed with exit code 0 only when it really copied/created the requested file at %p, and (2) return nonzero when the requested archived file is not available. C) Wrong/insufficient archive contents (missing WAL timeline/history files) - Archive recovery requires a continuous sequence of archived WAL files that extends back far enough for the requested recovery start. The docs emphasize you need the archive to contain the needed WAL history back to at least the recovery start time, otherwise recovery cannot proceed [1]. - Additionally, for timeline history, PostgreSQL requests the next “.history” file. If it’s removed by retention (or never archived), restore_command cannot restore it. D) Archive cleanup / retention removed needed files - For standby/log shipping scenarios, the warm-standby docs caution about archive_cleanup_command: you must retain files needed to recover (e.g., at least as far back as required by the latest base backup / to support restarting) [4]. If your cleanup policy is too aggressive, you’ll observe missing files during archive recovery. E) restore_command uses a script/remote copy that fails with an exit code that PostgreSQL treats as fatal - Because PostgreSQL treats “error by the shell (such as command not found)” as abort-worthy, and because exit-status behavior matters, you must ensure your restore_command’s failure mode matches PostgreSQL expectations. Discussion notes that nonzero exit codes in general are used for “not present in archive,” but shell termination/error can abort startup; ensure the command properly handles connection failures/availability and returns a nonzero value rather than triggering shell-level errors [5][6]. What to do (practical checks) 1) Verify restore_command syntax and placeholders - Ensure restore_command uses %f and %p correctly (and %p is where PostgreSQL expects the file to appear, relative to the data directory) [1]. 2) Ensure restore_command can run in the PostgreSQL server runtime environment - Confirm the referenced command exists (PATH/absolute paths) and required credentials/network access are available for the PostgreSQL process user. 3) Ensure restore_command return codes are correct - Return 0 only if the requested file was successfully placed at the requested destination (%p) [1]. - Return nonzero if the file is not present in the archive or otherwise cannot be restored (as PostgreSQL may ask for files that aren’t present) [1]. 4) Confirm the requested files exist in the archive storage - Look at the log line(s) mentioning the specific file name PostgreSQL cannot restore (e.g., a WAL segment like 0000000X... or a timeline history like 00000004.history). Then check your archive directory/object store for that exact file. 5) Check archive retention/cleanup - If you use archive_cleanup_command or archive retention policies, ensure you didn’t delete required WAL/history files for the recovery point you’re trying to reach [4]. If you paste the exact PostgreSQL log lines (especially the “could not restore file … from archive: …” portion and the restore_command you’re using), I can map it to the most likely bucket above and suggest a corrected restore_command pattern. Key references - PostgreSQL 18 docs: continuous archiving/PITR and restore_command semantics including exit codes and required %f/%p behavior [1]. - PostgreSQL 18 warm-standby docs: archive cleanup/retention guidance [4]. - Example failure modes involving “command not found” for timeline history restores [2]. - Example failure mode where restore_command returns 0 but file isn’t actually created at pg_wal destination [3].

Citations:


🌐 Web query:

PostgreSQL source could not restore file \"%s\" from archive

💡 Result:

PostgreSQL error “FATAL: could not restore file "%s" from archive …” almost always means archive recovery (WAL/history restore) called restore_command to fetch a needed WAL segment (or timeline history file) and the restore_command failed or produced an unusable result. Most common causes and what to check 1) restore_command uses a tool that isn’t installed (or isn’t found in PATH) If PostgreSQL logs show something like “command not found” for the invoked program, recovery aborts when PostgreSQL cannot execute the restore command. For example, one case shows the log line “pgbackrest: not found” alongside “could not restore file "00000004.history" from archive: command not found” [1]. Fix: ensure the command exists on the standby (or recovery host), and use the absolute path if the tool is installed in a non-standard location [1]. 2) restore_command exit status / behavior doesn’t match what PostgreSQL expects Even if the restore_command returns success (exit code 0), PostgreSQL may still fail if the file it expects was not actually created in the target location. A 2025 report shows PostgreSQL logging: “restore_command returned a zero exit status, but stat failed” while it was trying to restore “pg_wal/RECOVERYHISTORY” [2]. This indicates the script returned 0 but did not leave the required file where PostgreSQL expects it. Fix: make sure your restore_command creates (or copies) the file to “%p” successfully whenever it returns 0; if the file doesn’t exist, return a non-zero status (or otherwise ensure correctness for the specific placeholder) so PostgreSQL retries/fails in a controlled way [2]. 3) archive recovery requires timeline history files (.history); they must be retrievable The error often references a “.history” file (timeline history) such as “00000001.history” [3] or “00000004.history” [1]. If your restore_command only knows how to fetch WAL segments but not timeline history files, startup will fail. Fix: ensure restore_command handles both WAL segments (%f like 000000…/000000…) and the corresponding “.history” requests, and that the archive storage actually contains those files (or are fetchable from your archive manager) [3][1]. 4) external archive transport failures (ssh, barman, network partitions, etc.) If restore_command depends on network access and the network is down, the child process can exit and recovery will abort. A report describes a replica crashing with “could not restore file … from archive: child process exited with exit code 255” during network maintenance; after the network returned, recovery/restore_command worked again [4]. Fix: ensure restore_command is robust to transient failures, and (if using ssh) consider wrapping the command so that failure modes don’t behave like fatal signals in a way that kills recovery; also verify connectivity and authentication [4]. 5) Not a pg_dump/pg_restore error (important distinction) The message above is for archive recovery / restore_command during WAL replay, not for restoring a logical dump (pg_restore) [5]. If you are using pg_restore and see different errors, they may be unrelated. If you share your exact log lines (especially the full “could not restore file …” line plus the preceding restore_command-related output) and your restore_command, I can pinpoint which of the above categories applies and suggest a specific corrected restore_command.

Citations:


🌐 Web query:

PostgreSQL wal_retrieve retry archive recovery waiting for archive segment log line

💡 Result:

The log message you are referring to occurs when a PostgreSQL server is in recovery mode (e.g., a standby server) and is unable to immediately locate the required Write-Ahead Log (WAL) segment from any of its configured sources [1]. In this state, the PostgreSQL recovery process searches for the next needed WAL segment from available sources, typically in this order: 1. Streaming replication (if configured) [1] 2. The local pg_wal directory 3. The WAL archive (via restore_command or recovery_target_timeline) [1][2] When the requested WAL data is not available in any of these locations, the recovery process pauses to wait for the data to become available [1]. The duration of this pause is controlled by the wal_retrieve_retry_interval parameter [1]. Once this interval expires, the system retries the retrieval process [1]. Key points regarding this behavior: - Configuration: The wal_retrieve_retry_interval parameter (default: 5000 milliseconds, or 5 seconds) determines how long the server waits between these retry attempts [1][3]. - Optimization: In environments where you want faster detection of new WAL files during archive recovery, you can reduce this value [1]. Conversely, in cloud environments where infrastructure access is metered or charged, you might increase this value to reduce the frequency of archive requests during periods of low activity [1]. - Context: This behavior is normal during archive recovery or standby operations when the primary server has not yet generated or shipped the next WAL segment [1]. It simply indicates that the standby is currently "caught up" and is waiting for more data from the primary [1]. If you see these messages frequently and the system is not making progress, it often indicates an issue with the connectivity between the standby and the primary, or a failure in the archiving process (e.g., the primary is failing to archive segments to the location the standby is checking) [1][4].

Citations:


🌐 Web query:

PostgreSQL 18 could not restore file \"000000010000000000000016\" from archive "record"

💡 Result:

The error "could not restore file from archive" in PostgreSQL, including references to specific Write-Ahead Log (WAL) files like "000000010000000000000016," generally indicates that the server's recovery process is attempting to fetch a file from the configured archive location via the restore_command and is failing to do so [1][2]. This is a normal part of the recovery process when it reaches the end of available WAL files in the archive. However, if the server considers the failure fatal (aborting startup), it is usually due to one of the following reasons: 1. External Command Failure: If the restore_command itself fails for a reason other than the file being missing (e.g., the command is not found, or there is a permission issue), PostgreSQL will abort the recovery [3][1][2]. For example, a "command not found" error often occurs if a backup tool (like pgbackrest) is configured in the restore_command but is not installed or available in the system's path [4][5]. 2. Improper Exit Status: PostgreSQL expects the restore_command to return a non-zero exit status when a requested file is genuinely missing from the archive (which is normal and not an error) [1][2]. If your script returns a non-zero status for reasons other than a missing file (or if it crashes), the database may interpret this as a fatal failure [3]. 3. Misconfigured Recovery Targets: If you have recovery parameters set (e.g., recovery_target_time, recovery_target_lsn), the server will continue to look for WAL files until it reaches that target [6][1]. If those files are not in the archive, or if there is a mismatch in timelines, the process will fail [7][8]. 4. Stale Configuration: If you are performing a standard startup but left restore_command or other recovery settings in your postgresql.conf or postgresql.auto.conf from a previous recovery attempt, the server will continue to try to enter recovery mode [4][5]. To troubleshoot this: * Check your logs for the specific exit code or error message accompanying the failure [3]. * Verify that your restore_command script is executable, installed correctly, and returns the correct exit codes [4][1]. * Ensure you have cleared out old recovery configuration settings if you are no longer attempting a point-in-time recovery [4][5]. * If you are intentionally performing recovery, ensure the required WAL segments are actually present in the specified archive directory [9][8].

Citations:


Tighten F1 so the failure assertion is reachable.

The current F1 patterns accept generic terms that can appear in recovery logs, so they do not prove the missing WAL segment was surfaced as a failure. Match or record an actual recovery failure message, such as the restore_command retrieval failure, and assert the node remained unavailable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-pitr-failure-drill.sh` around lines 297 - 311,
Tighten the F1 validation around MISS_LOGS and pg_isready so it requires
evidence of an actual restore_command/WAL retrieval failure rather than generic
recovery terms. Record or match the concrete missing-segment failure message,
and assert that the recovery node remains unavailable; do not pass solely
because broad log patterns appear or because the node starts with non-specific
restore errors.

@jnhu76

jnhu76 commented Aug 10, 2026

Copy link
Copy Markdown
Owner Author

P7-C Rebuild — Adversarial Audit: PG Backup Simplicity & Config Sync

Repository: jnhu76/exam
Branch: feat/p7-c-portable-backup-recovery
Audit target: P7-C rebuild (C1 portable + cold + Launchpad, C2 logical, C3 physical + PITR)
Commits audited (2a1a9eb3..0ebdf6eb):

0ebdf6eb docs(p7-c): close portable backup and recovery program
0df4ba72 feat(p7-c3): physical backup and PITR
e164fd30 feat(p7-c2): logical backup and verified clean restore
df2d07df feat(p7-c1): portable persistence, cold backup and launchpad

Audit date: 2026-08-10
Audit method: adversarial code/proof review focused on (a) whether the
PostgreSQL backup/restore tooling follows PostgreSQL's documented contract
and is as simple as it can be, and (b) whether the configuration surface
(.env* / docker-compose*.yml / scripts / docs) is internally
synchronized. PostgreSQL behavior was verified against the official
PostgreSQL 18 documentation via Context7; the docker-entrypoint.sh
extension handling was verified against the official postgres image source.

Scope note: this is not a re-run of the earlier PR #273 audit
(docs/audits/P7-C1-ADVERSARIAL-PORTABLE-DEPLOYMENT-AUDIT.md, which targets
a different, abandoned PR). That document is retained as history. This audit
covers the rebuilt C1/C2/C3 on this branch only.


1. Executive verdict

Request changes — the backup/restore mechanics are sound and follow
PostgreSQL's documented contract, but two configuration-synchronization
defects make a documented operator path silently inert:

  1. Critical (config sync) — PITR initdb seed is silently dead.
    docker-compose.pitr.yml mounts docker/pitr/wal-archive.conf into
    /docker-entrypoint-initdb.d/99-pitr-wal-archive.conf. The official
    postgres image ignores .conf files in that directory (verified
    against the image's docker-entrypoint.sh: only .sh, .sql,
    .sql.gz, .sql.xz, .sql.zst are processed; .conf hits the *)
    branch → ignoring). The documented fresh-start path
    (docker compose -f docker-compose.yml -f docker-compose.pitr.yml up -d)
    therefore does not enable WAL archiving at first init. The drills
    bypass this by applying the same settings via ALTER SYSTEM, so the
    broken compose path is never exercised. An operator following the docs
    for a fresh PITR-enabled deployment will believe archiving is on when it
    is not — the first base backup taken will have no WAL chain forward
    and PITR will silently not be possible. (§3.1)

  2. Critical (config sync) — Launchpad token is never forwarded to the app
    container (P2-1, carried over from the prior audit).
    docker-compose.yml
    has no LAUNCHPAD_SETUP_TOKEN in the app service environment:
    block and no env_file:. The README, the runbook, and
    docs/deployment/backup-and-recovery.md §11.1 all instruct the operator
    to "set LAUNCHPAD_SETUP_TOKEN in .env". Compose uses .env for
    interpolation only — a value there is never injected into a container
    unless an environment: entry references it. With the token in .env,
    the app container sees LAUNCHPAD_SETUP_TOKEN unset, so
    runtimeConfig.ts disables launchpad (setupToken: ""), and
    POST /api/launchpad/bootstrap always returns 403
    LAUNCHPAD_INVALID_SETUP_TOKEN. The headline C1.6 first-install UX is
    inert in the bundled deployment; only the bootstrap-admin CLI path
    works. (§4.1)

The backup scripts themselves (C1 cold copy, C2 pg_dump -Fc + clean
restore, C3 pg_basebackup + pg_verifybackup) are faithful to PostgreSQL's
documented patterns and are simpler than the rebuilt program claims to be —
see §2. Several lower-severity config-sync and PG-convention issues are in
§3/§4.


2. PostgreSQL backup simplicity & PG-convention review

2.1 What is genuinely good (and simple)

Tool PG-contract adherence Notes
cold-filesystem-backup.sh / -restore.sh Correct Refuses a live copy; validates PG_VERSION + postgresql.conf; container-assisted cp -a to preserve uid-999 ownership; refuses to overwrite; path-guarded. Matches PG's "cold physical backup = stopped server + complete PGDATA copy" contract.
postgres-logical-backup.sh (pg_dump -Fc) Correct -Fc (custom format) + -X stream-equivalent self-consistency + --no-owner; never puts the password on argv; verifies artifact with non-empty + PGDMP magic + pg_restore --list. This is the routine online backup PG documents.
postgres-logical-restore.sh Correct & notably well-reasoned DROP DATABASE + CREATE DATABASE ... TEMPLATE template0 before pg_restore is the PG-documented way to get an exact match of the dump (avoids the --clean --if-exists merge trap where dump-absent objects survive). --exit-on-error fails fast. Refuses postgres/template0/template1.
pg-basebackup.sh Correct -X stream -c fast -Fp --manifest-checksums SHA256; pg_verifybackup on the manifest before success; --no-sync deliberately not used.
WAL archive_command Correct test ! -f /wal-archive/%f && cp %p /wal-archive/%f is the PG-documented idiom that refuses to silently overwrite a colliding segment.
PITR recovery config Correct recovery.signal + postgresql.auto.conf (restore_command + recovery_target_lsn + recovery_target_inclusive = on + recovery_target_action = 'promote'). recovery_target_lsn is the clock-skew-independent choice PG documents. Verified against PG18 docs.

The program is appropriately simple: it uses PG-native tools, adds no
custom protocol, and the negative-space contract (§9 of the closeout) is
honest. This is the right shape.

2.2 PG-convention issues (ordered by severity)

(Required) — pg-basebackup.sh claims "no replication password needed"
but connects over TCP (-h 127.0.0.1), which the official image authenticates
with scram-sha-256 by default.
scripts/backup/pg-basebackup.sh:96-100, 119-122:

# The local connection uses peer/trust auth for the bootstrap superuser
# (POSTGRES_USER), so no replication password is needed for the bundled
# single-node path.
...
docker run --rm \
  --network "container:${DB_CONTAINER}" \
  -e PGPASSWORD="${PGPASSWORD:-}" \
  postgres:18.4-bookworm \
  pg_basebackup -h 127.0.0.1 -U "${PGUSER:-exam}" ...

The comment is wrong. The official postgres image's default pg_hba.conf
authenticates TCP connections (host all all all scram-sha-256); trust
applies only to Unix-socket local connections. -h 127.0.0.1 is TCP, so
pg_basebackup will be challenged for a password. The script reads
PGPASSWORD from the host environment (${PGPASSWORD:-}); if the
operator has not export PGPASSWORD=<POSTGRES_PASSWORD> on the host, the
connection fails with password authentication failed. The drill works only
because the drill exports POSTGRES_PASSWORD and the operator would have to
also export PGPASSWORD — but the script header explicitly tells them they
do not need to. Two clean fixes, either is fine:

  • connect over the Unix socket (-h /var/run/postgresql, no -h, or
    PGHOST=/var/run/postgresql) inside the db container's network namespace
    — then trust/peer actually applies and the comment becomes true; or
  • drop the "no password needed" claim and document that the operator must
    export PGPASSWORD=<POSTGRES_PASSWORD> on the host (and use
    PGUSER="${PGUSER:-$POSTGRES_USER}" — see next item).

Note: pg_basebackup requires a role with REPLICATION or SUPERUSER
(verified against PG18 app-pgbasebackup.html + warm-standby.html). The
POSTGRES_USER superuser satisfies this, so authority is fine; the defect is
purely the auth-method/password claim.

(Nit) — PGUSER default exam diverges from POSTGRES_USER when the
operator customizes the latter.
pg-basebackup.sh:122 hardcodes
-U "${PGUSER:-exam}". The C2 backup script correctly reads
-U "$POSTGRES_USER" from inside the container. If the deployment uses
POSTGRES_USER=appdb, pg-basebackup.sh connects as the wrong/nonexistent
role. Mirror the C2 pattern or default PGUSER="${PGUSER:-exam}" and
document that PGUSER must equal POSTGRES_USER.

(Nit) — cosmetic POSTGRES_INITDB_ARGS: "" in the PITR override is dead
config.
docker-compose.pitr.yml:34 sets POSTGRES_INITDB_ARGS: "". The
base compose does not set it, so the official image default (empty) already
applies. Setting "" is a no-op that reads as if an arg (e.g.
--data-checksums) was intended and dropped. Remove it or document why it is
there. (This is not the cause of the .conf-ignored defect in §3.1 — that is
the file extension.)

(FYI) — cold backup does not explicitly check postmaster.pid absence.
A clean docker compose down removes it, and the restore script refuses a
populated destination, so this is not reachable in the supported flow. PG
will refuse to start on a stale postmaster.pid anyway (loud). Recorded as a
known-safe gap; no action required.


3. Critical config-sync defect: PITR initdb seed is silently ignored

3.1 The defect

docker-compose.pitr.yml:45:

- ./docker/pitr/wal-archive.conf:/docker-entrypoint-initdb.d/99-pitr-wal-archive.conf:ro

docker/pitr/wal-archive.conf contains ALTER SYSTEM SET archive_mode = 'on';
etc. The official postgres image's docker-entrypoint.sh processes
/docker-entrypoint-initdb.d/ files with this case (verified verbatim
against docker-library/postgres master):

case "$f" in
    *.sh)      ... ;;
    *.sql)     ... docker_process_sql -f "$f" ... ;;
    *.sql.gz)  ... gunzip -c "$f" | docker_process_sql ... ;;
    *.sql.xz)  ... xzcat "$f" | docker_process_sql ... ;;
    *.sql.zst) ... zstd -dc "$f" | docker_process_sql ... ;;
    *)         printf '%s: ignoring %s\n' "$0" "$f" ;;
esac

A .conf file matches *) and is ignored with a log line. archive_mode
is not enabled. The override's own header even hedges: "For a
fresh start with this override, the entrypoint-init WAL config below seeds it
at first init." — but it does not, because of the extension.

3.2 Why the drills do not catch it

Both PITR drills (p7-c3-pitr-drill.sh, p7-c3-pitr-failure-drill.sh)
ignore the compose override entirely and instead apply archiving via
ALTER SYSTEM against the running cluster:

psql_src -c "ALTER SYSTEM SET archive_mode = 'on';"
psql_src -c "ALTER SYSTEM SET archive_command = 'test ! -f /wal-archive/%f && cp %p /wal-archive/%f';"

So the documented operator path (docker compose -f docker-compose.yml -f docker-compose.pitr.yml up -d) is never exercised by any test or drill.
The closeout's §7 "5/5 drills PASS" claim is true but the drills prove a
different code path than the one the docs tell operators to use.

3.3 Impact (operator-visible)

  1. Operator sets up a fresh PITR-enabled deployment per the docs.
  2. archive_mode is silently off. SHOW archive_mode would reveal off,
    but nothing prompts the operator to check.
  3. Operator takes a base backup via pg-basebackup.sh. -X stream makes the
    base backup internally consistent, so pg_verifybackup passes.
  4. Some time later, operator needs PITR. They restore the base backup + point
    restore_command at the WAL archive. The archive is empty (archiving
    was never on). Recovery cannot replay forward and PITR fails — or, worse,
    recovers only to the base-backup checkpoint and the operator does not
    immediately notice the data loss.

3.4 Fix (smallest acceptable)

Rename the seed file so the extension is processed:

git mv docker/pitr/wal-archive.conf docker/pitr/wal-archive.sql
# update the mount in docker-compose.pitr.yml:
#   ./docker/pitr/wal-archive.sql:/docker-entrypoint-initdb.d/99-pitr-wal-archive.sql:ro

ALTER SYSTEM ... ; is valid SQL and runs fine through docker_process_sql.
Then add a fresh-start drill (or extend the existing one) that actually
boots docker compose -f docker-compose.yml -f docker-compose.pitr.yml up -d
on an empty data root and asserts SHOW archive_mode returns on. Until
that drill exists, the documented PITR init path is unverified.


4. Critical config-sync defect: Launchpad token never reaches the container

4.1 The defect (P2-1, carried over and still present)

docker-compose.yml app service environment: block (lines 41-64) has
no LAUNCHPAD_SETUP_TOKEN entry and the service has no env_file:. Yet:

  • README.md:215: "set LAUNCHPAD_SETUP_TOKEN=<openssl rand -hex 32> in
    .env ... navigate to /launchpad".
  • docs/deployment/backup-and-recovery.md:524: "Set LAUNCHPAD_SETUP_TOKEN
    in .env before the first docker compose up".
  • docs/deployment/mvp-deployment-runbook.md:180: "LAUNCHPAD_SETUP_TOKEN=...
    in .env BEFORE step 4".
  • apps/api/src/config/runtimeConfig.ts:902:
    setupToken: (env.LAUNCHPAD_SETUP_TOKEN ?? "").trim() — empty → disabled.

Compose reads .env for variable substitution only. A value in .env
is injected into a container only when an environment: entry references
it (e.g. LAUNCHPAD_SETUP_TOKEN: ${LAUNCHPAD_SETUP_TOKEN:-}). With no such
entry, the token stays on the host and the container sees nothing.

4.2 Runtime consequence

  • LAUNCHPAD_SETUP_TOKEN set in .env → app container env unset →
    runtimeConfig.launchpad.setupToken === "" → launchpad disabled.
  • GET /api/launchpad/status{ initialized: false } (renders the form).
  • POST /api/launchpad/bootstrap with the correct token →
    !configuredToken is true → 403 LAUNCHPAD_INVALID_SETUP_TOKEN.

The documented first-install UX cannot succeed. The operator must either
hand-edit the compose (undocumented) or fall back to the bootstrap-admin
CLI. The C1.6 deliverable is inert in the bundled deployment.

4.3 Fix (one line + contract awareness)

In docker-compose.yml app service environment::

LAUNCHPAD_SETUP_TOKEN: ${LAUNCHPAD_SETUP_TOKEN:-}

The empty default keeps launchpad disabled for a bare docker compose up
(preserves the "not fail-fast at boot" contract in runtimeConfig.ts). Then
add a smoke assertion that a token-bearing .env yields a working
/launchpad/bootstrap. Also add LAUNCHPAD_SETUP_TOKEN (commented, with the
openssl rand -hex 32 guidance) to .env.example so the single source of
configuration truth actually lists it — today .env.example does not mention
it at all, while the README/runbook/backup-guide do.

Check the topology contract (scripts/repository-contract/...) does not
reject new environment: keys before/after this change; if it does, extend
the allowlist rather than weakening the guard.


5. Other config-sync & doc-consistency findings

(Required) — .env.example is missing LAUNCHPAD_SETUP_TOKEN while three
docs reference it.
.env.example is the documented single source of
runtime configuration (AGENTS.md local-DB-discipline: "copy from
.env.example"). Its absence there while the README, runbook, and
backup-guide all instruct setting it is a config-surface desynchronization.
Add it (commented, with entropy guidance), alongside the fix in §4.3.

(Nit) — pg_isready -U exam -d exam is hardcoded in restore/backup scripts
while the real connection uses $POSTGRES_USER/$POSTGRES_DB.

postgres-logical-restore.sh:83, postgres-logical-backup.sh:89,
pg-basebackup.sh:85, and the PITR drills. pg_isready does not actually
authenticate (it only checks the postmaster accepts connections), so this is
cosmetic, not functional — but it misleads readers about which user/db the
script targets. Either parameterize (-U "${POSTGRES_USER:-exam}") or add a
comment that the ready-check user is arbitrary.

(Nit) — PITR drills hardcode -U exam -d exam for source and recovery
probes.
p7-c3-pitr-drill.sh:72,78,228,240-242 and the failure drill. The
drills set POSTGRES_PASSWORD but never POSTGRES_USER, so the default
exam is correct for the bundled path — but the recovery cluster's
POSTGRES_USER is inherited from the base compose (${POSTGRES_USER:-exam})
and would diverge if an operator customized it. Low priority (drills are
throwaway), but worth a note.

(Nit) — archive_timeout differs between the compose seed (60s), the docs
(§8.2: 60s), and the drills (30s).
Not a correctness issue (both satisfy
"bounded archive window for low-write clusters"), but the drills do not
exercise the documented value. Pick one and align, or document that the drill
uses an aggressive value for speed.

(FYI) — docker-compose.pitr.yml mounts the postgres bind a second time
identically to the base.
Line 43 (${EXAM_DATA_ROOT:-./data}/postgres:/var/lib/postgresql)
duplicates the base db volume. Compose merges these (same target path), so
it is harmless, but it is noise. The only additions the override needs are
the WAL archive mount and the initdb seed. Consider dropping the duplicated
postgres line.


6. Drill-vs-documented-path gap (process finding)

The five drills are well-constructed and path-isolated, but none of them
boots the documented operator compose invocation
. The drills:

  • boot docker compose -p <project> -f docker-compose.yml up -d (base only);
  • enable archiving via ALTER SYSTEM + a temp WAL-mount override;
  • never apply -f docker-compose.pitr.yml.

This is why §3.1 went undetected. The closeout's "5/5 drills PASS" is
accurate for what the drills test, but the drills test a different PITR
activation path
than the one operators are told to use. Recommendation:
add a sixth drill (or extend the happy-path drill) that boots the literal
-f docker-compose.yml -f docker-compose.pitr.yml on an empty data root and
asserts archive_mode = on. This is the single highest-leverage process fix.


7. Summary table

Area Verdict Severity
C1 cold backup/restore PG contract Sound, simple
C2 pg_dump/pg_restore clean-target contract Sound, well-reasoned
C3 pg_basebackup + pg_verifybackup PG contract Sound
C3 WAL archive_command (non-overwrite idiom) Correct
C3 PITR recovery_target_lsn + recovery.signal Correct (PG18-verified)
pg-basebackup.sh "no password needed" claim vs TCP/scram Misleading / will fail without host PGPASSWORD Required
wal-archive.conf mounted into initdb.d is ignored (.conf extension) Documented fresh-PITR path silently dead Critical
LAUNCHPAD_SETUP_TOKEN not forwarded to app container Documented first-install UX inert Critical
LAUNCHPAD_SETUP_TOKEN absent from .env.example Config-surface desync Required
PGUSER default exam vs POSTGRES_USER Diverges on customization Nit
POSTGRES_INITDB_ARGS: "" dead config Noise Nit
pg_isready -U exam -d exam hardcoded Cosmetic Nit
archive_timeout 60s (docs/compose) vs 30s (drills) Align or document Nit
Duplicated postgres bind in PITR override Noise Nit
No drill exercises the documented PITR compose path Process gap (masks the .conf bug) Required

8. Required corrective actions

  1. (Critical) Rename docker/pitr/wal-archive.conf.sql (or .sh)
    and update the mount in docker-compose.pitr.yml. Add a drill that boots
    -f docker-compose.yml -f docker-compose.pitr.yml on an empty root and
    asserts SHOW archive_mode = on. (§3)
  2. (Critical) Add LAUNCHPAD_SETUP_TOKEN: ${LAUNCHPAD_SETUP_TOKEN:-} to
    the app service environment: in docker-compose.yml; verify the
    topology contract still passes. Add a smoke assertion that a token-bearing
    .env yields a working /launchpad/bootstrap. (§4)
  3. (Required) Add LAUNCHPAD_SETUP_TOKEN (commented, entropy guidance) to
    .env.example. (§5)
  4. (Required) Fix pg-basebackup.sh: either connect over the Unix socket
    (so the "trust/no password" claim becomes true) or correct the comment and
    require the operator to export PGPASSWORD. (§2.2)
  5. (Required) Add a drill that exercises the documented PITR compose
    invocation (closes the drill-vs-doc gap that hid fix: candidate CSV import — headers, dedup, strict schema, tests #1). (§6)

Nits (§5) are optional cleanup; address at author discretion.


9. Verification notes

  • PostgreSQL behavior (archive_command, restore_command, recovery_target_*,
    recovery.signal, pg_basebackup replication authority, template0
    restore contract) verified against the PostgreSQL 18 documentation via
    Context7 (/websites/postgresql_18, continuous-archiving.html,
    runtime-config-wal.html, app-pgbasebackup.html, warm-standby.html).
  • docker-entrypoint.sh extension handling verified against the official
    docker-library/postgres master docker-entrypoint.sh source — only
    .sh/.sql/.sql.gz/.sql.xz/.sql.zst are processed; *) logs ignoring.
  • No runtime mutations were performed. All findings are from source/config
    inspection + official-doc cross-check. The two Critical findings are
    statically determinable and do not require a live cluster to confirm
    (though a live check of SHOW archive_mode after the documented compose up
    would refute or confirm §3.1 directly).

jnhu76 added 4 commits August 10, 2026 12:35
One production Compose entry point: delete docker-compose.pitr.yml and
docker/pitr/wal-archive.conf. PITR is a database capability enabled by the
canonical scripts/backup/postgres-enable-pitr.sh (ALTER SYSTEM persisted in
postgresql.auto.conf), not an alternate Docker topology. WAL archive mount
is always present but inert until enabled.

- idempotent archive_command (absent->copy, identical->cmp, collision->fail)
- pg_basebackup derives deployment PGUSER/PGPASSWORD from the db container
  env; auth comment matches reality (loopback TCP + scram-sha-256, password
  via PGPASSWORD, never argv)
- cold backup refuses an obviously running source (postmaster.pid)
- logical restore reworded to clean logical reconstruction (no byte-identity
  overclaim)
- C3 drills now exercise the canonical enable script (product path == test
  path); new archive-idempotency drill
- topology guard: forbids production Compose variants, requires
  LAUNCHPAD_SETUP_TOKEN wiring in the app environment
- bootstrapAdminOnFreshDb takes a transaction-scoped pg_advisory_xact_lock
  at entry, shared by the HTTP launchpad adapter and the bootstrap-admin
  CLI, so a concurrent first-install has exactly one winner on a fresh DB
- new p7-c1-launchpad-compose-drill.sh proves the production Compose path
  end to end: fresh status uninitialized; wrong token 403; correct token
  creates the first Admin; correct token again 409 (installation never
  reopens and is not a token oracle); Admin login; DB invariants (1 Admin,
  0 Candidates, default org, bootstrap audit); register disabled (API 403
  stub + no client route); unset token disables launchpad entirely
- backup-and-recovery.md: One-Compose model read-first section; PITR
  enabled via scripts/backup/postgres-enable-pitr.sh with restrictive
  permissions and pg_stat_archiver proof; idempotent archive_command
  contract with three-case table; corrected retention rule (base history
  must precede T + all WAL through the window, no automatic pruning);
  base-backup-before-WAL ordering; WAL-G/pgBackRest future boundary;
  cold/logical terminology without byte-identity overclaim
- mvp-deployment-runbook.md: replace docker-compose.pitr.yml references
  with the canonical enable script
- README: one-compose invariant, advisory-lock first-install note
- closeout: corrective-pass section with correction table, drill and
  static-gate evidence
@github-actions

Copy link
Copy Markdown

AI review done up to commit: 567e0ad

AI Review Summary:

The pull request introduces a new Launchpad feature for initial system setup and significantly overhauls the deployment, backup, and recovery mechanisms for PostgreSQL and Redis. This includes implementing portable persistence using host bind mounts, cold filesystem backup/restore, logical online backup with clean restore, and physical backup with WAL archiving and point-in-time recovery (PITR). Many of these changes directly address critical findings from a comprehensive adversarial audit.

The quality of the changes is high. The new features and infrastructure are well-tested with a combination of unit tests, integration tests, and deterministic shell drills, covering a wide range of scenarios including happy paths, edge cases, and failure modes. Security considerations, such as constant-time token comparisons, prevention of token oracles, and secure handling of database credentials, have been diligently addressed. The documentation has been thoroughly updated with detailed guides, closeout reports, and adversarial audit records, ensuring transparency and providing clear, actionable instructions for operators. A key improvement for deployment simplicity and reliability is the adoption of a "One-Compose model" and explicit forwarding of environment variables in docker-compose.yml.

One minor observation:

  • In docs/deployment/backup-and-recovery.md, line 524, the instruction for LAUNCHPAD_SETUP_TOKEN mentions setting it in .env. While docker-compose.yml is configured to pick up the value from either .env or the shell environment, it might be slightly clearer to explicitly state both options for users. However, given that .env.example has been updated and the docker-compose.yml handles it correctly, this is a very minor point and doesn't represent a bug or logical error.

Overall, the changes represent a significant improvement in the system's robustness, deployability, and recoverability, with thorough validation and documentation.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/deployment/backup-and-recovery.md (1)

54-61: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Separate authoritative state from deployment identity.

data/postgres is the authoritative durable Exam state. It is not the only material required to restore the same running deployment. Preserve .env secrets, Compose configuration, and the exact application/database image identity. Replace “the only bytes you must preserve” with “the only durable Exam state you must preserve.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/deployment/backup-and-recovery.md` around lines 54 - 61, Update the
backup-and-recovery documentation to distinguish authoritative durable Exam
state from deployment identity: revise the claim that the PostgreSQL data
directory is the only bytes required, stating instead that it is the only
durable Exam state to preserve. Also document preserving .env secrets, Compose
configuration, and exact application/database image identity for restoring the
same deployment.
🧹 Nitpick comments (7)
scripts/deployment/p7-c3-archive-idempotency-drill.sh (3)

96-97: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Do not discard the output of the canonical enable-PITR script.

Line 96 redirects both stdout and stderr to /dev/null. If postgres-enable-pitr.sh fails, set -e aborts the drill with no diagnostic text. The operator then sees an exit code and nothing else. p7-c3-pitr-drill.sh pipes the same call through sed 's/^/ /'; match that behavior.

♻️ Proposed change to keep the diagnostics
-bash "${ENABLE_PITR_SH}" "${PROJECT}" "${COMPOSE_FILE}" >/dev/null 2>&1
+bash "${ENABLE_PITR_SH}" "${PROJECT}" "${COMPOSE_FILE}" 2>&1 | sed 's/^/    /'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-archive-idempotency-drill.sh` around lines 96 - 97,
Update the canonical enable-PITR invocation in the archive idempotency drill to
preserve and indent its stdout and stderr, matching the p7-c3-pitr-drill.sh
behavior with sed. Remove the /dev/null redirection while keeping the existing
wait_db sequence unchanged.

62-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Split the declaration and the assignment.

export VAR="$(openssl ...)" returns the exit status of export, not of openssl. If openssl fails, the drill continues with a truncated secret instead of stopping. Shellcheck reports SC2155 on both lines.

♻️ Proposed change
-export POSTGRES_PASSWORD="p7c3idem-pg-$(openssl rand -hex 6)"
-export JWT_SECRET="p7c3idem-jwt-$(openssl rand -hex 16)"
+POSTGRES_PASSWORD="p7c3idem-pg-$(openssl rand -hex 6)"
+JWT_SECRET="p7c3idem-jwt-$(openssl rand -hex 16)"
+export POSTGRES_PASSWORD JWT_SECRET
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-archive-idempotency-drill.sh` around lines 62 - 63,
Split the POSTGRES_PASSWORD and JWT_SECRET declarations from their command
substitutions in the deployment drill: assign each generated value to the
variable first, then export it separately. Preserve the existing OpenSSL
commands and variable values while ensuring command-substitution failures
propagate under the script’s error handling.

Source: Linters/SAST tools


149-163: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Consider asserting that the target bytes stay unchanged in CASE 3.

The drill checks only the exit status. The stated risk is a silent overwrite, so the strongest assertion is that /wal-archive/IDEM-CASE1 still matches /tmp/idem-source after the collision attempt. Add a cmp -s /tmp/idem-source /wal-archive/IDEM-CASE1 check after the case runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/deployment/p7-c3-archive-idempotency-drill.sh` around lines 149 -
163, The CASE 3 validation currently checks only that the archive command fails,
not that it preserves existing data. Update the CASE3 flow in
scripts/deployment/p7-c3-archive-idempotency-drill.sh to run cmp -s between
/tmp/idem-source and /wal-archive/IDEM-CASE1 after the collision attempt, and
pass only when the command fails and the target bytes remain unchanged.
scripts/backup/postgres-enable-pitr.sh (2)

180-209: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider failing when the archiver reports failures.

The loop accepts the presence of /wal-archive/${SWITCHED_FILE} as proof. That file can exist from an earlier successful archive attempt while the current archive_command fails repeatedly. Reading failed_count and last_failed_error from pg_stat_archiver makes the enablement report trustworthy.

Line 205 also recomputes AFTER_COUNT after the segment-existence path succeeds. The report can then print 0 → 0, which contradicts the "archived_count" wording.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/postgres-enable-pitr.sh` around lines 180 - 209, Update the
archiver verification loop to read and track pg_stat_archiver failed_count and
last_failed_error, and fail instead of accepting stale segment evidence when the
current archive attempt reports failures. Also preserve the count captured
during verification when the segment-existence path succeeds, rather than
recomputing AFTER_COUNT afterward, so the final archived_count report reflects
the evidence used.

62-75: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Resolve the db container through Compose instead of building its name.

Line 70 builds ${PROJECT}-db-1 by string concatenation. A container_name uses its exact value, and a scaled db service uses container names such as db_1 or db_2, so this script may fail with "db container not found" even after the stack is running. Build names may also fail when the Compose-project name is invalid.

Use docker compose -p "${PROJECT}" -f "${COMPOSE_FILE}" ps -q db to resolve the container id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/postgres-enable-pitr.sh` around lines 62 - 75, Replace the
hard-coded DB_CONTAINER name construction with Compose-based resolution using
`docker compose -p "${PROJECT}" -f "${COMPOSE_FILE}" ps -q db`. Validate that
the returned container ID is non-empty, then use it in the existing `docker
inspect` check and preserve the current failure messages and exit behavior.
docs/audits/P7-C1-ADVERSARIAL-PORTABLE-DEPLOYMENT-AUDIT.md (1)

385-421: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a language identifier to the final fenced block.

The static check reports MD040 for the block starting at Line 385. Mark it text or the correct language so the audit passes the repository Markdown checks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/audits/P7-C1-ADVERSARIAL-PORTABLE-DEPLOYMENT-AUDIT.md` around lines 385
- 421, Update the final fenced code block in the audit document to include a
`text` language identifier, preserving its contents unchanged so the MD040
Markdown check passes.

Source: Linters/SAST tools

docs/audits/P7-C-REBUILD-ADVERSARIAL-PG-BACKUP-CONFIG-AUDIT.md (1)

8-13: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add language identifiers to the fenced blocks.

The static check reports MD040 for these blocks. Mark the commit list as text and the shell example as bash, or use the correct language for each block.

Also applies to: 99-109

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/audits/P7-C-REBUILD-ADVERSARIAL-PG-BACKUP-CONFIG-AUDIT.md` around lines
8 - 13, Add language identifiers to every fenced code block in the audit
document, including the commit list near the shown diff and the shell example
around the additionally referenced section; mark the commit list as text and the
shell example as bash to resolve MD040.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.env.example:
- Around line 150-152: Update the Launchpad comment in .env.example to qualify
that it never reopens after supported Admin-removal operations, while noting
that destructive database edits or history-replacing restores may alter the
freshness evidence. Keep the existing operator CLI recovery guidance unchanged.

In `@docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md`:
- Around line 364-370: Add the missing release-evidence fields to the static
gates report: modified files, new tests, coverage, the `pnpm verify` result, and
known limitations. Alternatively, link this table to an authoritative report
containing all required fields, while preserving the existing gate results.

In `@docs/audits/P7-C-REBUILD-ADVERSARIAL-PG-BACKUP-CONFIG-AUDIT.md`:
- Line 85: Update the WAL archive_command verdict in the audit table to indicate
that `test ! -f /wal-archive/%f && cp %p /wal-archive/%f` prevents overwrites
but is not idempotent, aligning it with the corrective finding and replacement
documented near line 337.
- Around line 82-83: Revise the audit descriptions for
postgres-logical-backup.sh and postgres-logical-restore.sh to use precise
PostgreSQL terminology: describe pg_dump -Fc using its actual consistent
snapshot and custom archive semantics without mentioning -X stream, and replace
“exact match” with wording that limits the claim to dumped database objects
while acknowledging configuration objects may differ.

In `@docs/audits/P7-C1-ADVERSARIAL-PORTABLE-DEPLOYMENT-AUDIT.md`:
- Line 225: Reconcile the HYP-8 severity classification across the summary,
findings around HYP-8, and merge recommendation. Either use one consistent
severity everywhere or explicitly distinguish P3 as the clean-host coverage debt
and P2-2 as the inaccurate “permanently guarded” claim, including that
distinction in the relevant sections.

---

Outside diff comments:
In `@docs/deployment/backup-and-recovery.md`:
- Around line 54-61: Update the backup-and-recovery documentation to distinguish
authoritative durable Exam state from deployment identity: revise the claim that
the PostgreSQL data directory is the only bytes required, stating instead that
it is the only durable Exam state to preserve. Also document preserving .env
secrets, Compose configuration, and exact application/database image identity
for restoring the same deployment.

---

Nitpick comments:
In `@docs/audits/P7-C-REBUILD-ADVERSARIAL-PG-BACKUP-CONFIG-AUDIT.md`:
- Around line 8-13: Add language identifiers to every fenced code block in the
audit document, including the commit list near the shown diff and the shell
example around the additionally referenced section; mark the commit list as text
and the shell example as bash to resolve MD040.

In `@docs/audits/P7-C1-ADVERSARIAL-PORTABLE-DEPLOYMENT-AUDIT.md`:
- Around line 385-421: Update the final fenced code block in the audit document
to include a `text` language identifier, preserving its contents unchanged so
the MD040 Markdown check passes.

In `@scripts/backup/postgres-enable-pitr.sh`:
- Around line 180-209: Update the archiver verification loop to read and track
pg_stat_archiver failed_count and last_failed_error, and fail instead of
accepting stale segment evidence when the current archive attempt reports
failures. Also preserve the count captured during verification when the
segment-existence path succeeds, rather than recomputing AFTER_COUNT afterward,
so the final archived_count report reflects the evidence used.
- Around line 62-75: Replace the hard-coded DB_CONTAINER name construction with
Compose-based resolution using `docker compose -p "${PROJECT}" -f
"${COMPOSE_FILE}" ps -q db`. Validate that the returned container ID is
non-empty, then use it in the existing `docker inspect` check and preserve the
current failure messages and exit behavior.

In `@scripts/deployment/p7-c3-archive-idempotency-drill.sh`:
- Around line 96-97: Update the canonical enable-PITR invocation in the archive
idempotency drill to preserve and indent its stdout and stderr, matching the
p7-c3-pitr-drill.sh behavior with sed. Remove the /dev/null redirection while
keeping the existing wait_db sequence unchanged.
- Around line 62-63: Split the POSTGRES_PASSWORD and JWT_SECRET declarations
from their command substitutions in the deployment drill: assign each generated
value to the variable first, then export it separately. Preserve the existing
OpenSSL commands and variable values while ensuring command-substitution
failures propagate under the script’s error handling.
- Around line 149-163: The CASE 3 validation currently checks only that the
archive command fails, not that it preserves existing data. Update the CASE3
flow in scripts/deployment/p7-c3-archive-idempotency-drill.sh to run cmp -s
between /tmp/idem-source and /wal-archive/IDEM-CASE1 after the collision
attempt, and pass only when the command fails and the target bytes remain
unchanged.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 53eb5997-89a7-4e92-9709-96cc8386337a

📥 Commits

Reviewing files that changed from the base of the PR and between 0ebdf6e and 567e0ad.

📒 Files selected for processing (18)
  • .env.example
  • README.md
  • apps/api/src/scripts/bootstrap-admin.ts
  • docker-compose.yml
  • docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md
  • docs/audits/P7-C-REBUILD-ADVERSARIAL-PG-BACKUP-CONFIG-AUDIT.md
  • docs/audits/P7-C1-ADVERSARIAL-PORTABLE-DEPLOYMENT-AUDIT.md
  • docs/deployment/backup-and-recovery.md
  • docs/deployment/mvp-deployment-runbook.md
  • scripts/backup/cold-filesystem-backup.sh
  • scripts/backup/pg-basebackup.sh
  • scripts/backup/postgres-enable-pitr.sh
  • scripts/backup/postgres-logical-restore.sh
  • scripts/deployment/p7-c1-launchpad-compose-drill.sh
  • scripts/deployment/p7-c3-archive-idempotency-drill.sh
  • scripts/deployment/p7-c3-pitr-drill.sh
  • scripts/deployment/p7-c3-pitr-failure-drill.sh
  • scripts/repository-contract/deployment-topology-contract.mjs
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/deployment/mvp-deployment-runbook.md
  • README.md
  • scripts/backup/postgres-logical-restore.sh
  • scripts/backup/pg-basebackup.sh
  • scripts/deployment/p7-c3-pitr-failure-drill.sh

Comment thread .env.example
Comment on lines +150 to +152
# Once the installation is initialized, /launchpad redirects to /login and
# never reopens — removing the last Admin does NOT reopen it (Admin recovery
# is operator CLI: bootstrap-admin / reset-admin-password).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Qualify the “never reopens” guarantee.

The supported removal flow keeps Launchpad completed, but destructive database edits or history-replacing restores can change the freshness evidence. State that this guarantee applies to supported Admin-removal operations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.env.example around lines 150 - 152, Update the Launchpad comment in
.env.example to qualify that it never reopens after supported Admin-removal
operations, while noting that destructive database edits or history-replacing
restores may alter the freshness evidence. Keep the existing operator CLI
recovery guidance unchanged.

Comment thread docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md Outdated
Comment on lines +82 to +83
| `postgres-logical-backup.sh` (`pg_dump -Fc`) | Correct | `-Fc` (custom format) + `-X stream`-equivalent self-consistency + `--no-owner`; never puts the password on argv; verifies artifact with non-empty + `PGDMP` magic + `pg_restore --list`. This is the routine online backup PG documents. |
| `postgres-logical-restore.sh` | Correct & notably well-reasoned | `DROP DATABASE` + `CREATE DATABASE ... TEMPLATE template0` before `pg_restore` is the PG-documented way to get an **exact** match of the dump (avoids the `--clean --if-exists` merge trap where dump-absent objects survive). `--exit-on-error` fails fast. Refuses `postgres`/`template0`/`template1`. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

PostgreSQL pg_dump -Fc custom format -X stream option pg_basebackup documentation

💡 Result:

There is no "stream" option for the custom format in pg_dump, nor is there a "custom format" in pg_basebackup. The two utilities serve different purposes and operate in distinct ways [1][2]. pg_dump The pg_dump utility performs a logical backup of a single database [2][3]. When using the custom format (-Fc), it creates a compressed, archive-style file intended for use with pg_restore [4][5]. This format is not a stream in the sense of continuous data flow for replication; rather, it is a structured archive that allows for selective restoration of objects and parallel restoration [4][6]. The custom format is compressed by default [4][7]. pg_basebackup The pg_basebackup utility performs a physical, binary backup of an entire PostgreSQL database cluster [8][9][2]. It does not have a "custom format" like pg_dump. Instead, it typically outputs files in the format of the database cluster itself (or in a tar archive format using the -Ft option) [9][10]. Regarding "streaming": While pg_dump does not have a "stream" option, pg_basebackup includes an option called -X (or --wal-method), which can be set to stream [9][10]. This option controls how Write-Ahead Log (WAL) files are handled during the backup. When -X stream is used, pg_basebackup streams the required WAL files along with the data files during the backup process, which helps ensure the resulting backup is consistent and ready for recovery without needing external WAL archives [9][1]. Summary of Differences pg_dump: Logical backup, database-level, portable across versions/architectures, slower for massive datasets [1][2][3]. pg_basebackup: Physical backup, cluster-level, requires identical major version/platform for restore, faster for large datasets, essential for Point-in-Time Recovery (PITR) and replication [2][11][3].

Citations:


Use precise PostgreSQL backup terminology.

pg_dump -Fc does not have a -X stream mode; use the actual consistency/archive semantics instead, and avoid “exact match” for logical restores because database configuration objects may differ.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/audits/P7-C-REBUILD-ADVERSARIAL-PG-BACKUP-CONFIG-AUDIT.md` around lines
82 - 83, Revise the audit descriptions for postgres-logical-backup.sh and
postgres-logical-restore.sh to use precise PostgreSQL terminology: describe
pg_dump -Fc using its actual consistent snapshot and custom archive semantics
without mentioning -X stream, and replace “exact match” with wording that limits
the claim to dumped database objects while acknowledging configuration objects
may differ.

| `postgres-logical-backup.sh` (`pg_dump -Fc`) | Correct | `-Fc` (custom format) + `-X stream`-equivalent self-consistency + `--no-owner`; never puts the password on argv; verifies artifact with non-empty + `PGDMP` magic + `pg_restore --list`. This is the routine online backup PG documents. |
| `postgres-logical-restore.sh` | Correct & notably well-reasoned | `DROP DATABASE` + `CREATE DATABASE ... TEMPLATE template0` before `pg_restore` is the PG-documented way to get an **exact** match of the dump (avoids the `--clean --if-exists` merge trap where dump-absent objects survive). `--exit-on-error` fails fast. Refuses `postgres`/`template0`/`template1`. |
| `pg-basebackup.sh` | Correct | `-X stream -c fast -Fp --manifest-checksums SHA256`; `pg_verifybackup` on the manifest before success; `--no-sync` deliberately not used. |
| WAL `archive_command` | Correct | `test ! -f /wal-archive/%f && cp %p /wal-archive/%f` is the PG-documented idiom that **refuses to silently overwrite** a colliding segment. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Align the WAL verdict with the corrective finding.

Line 85 labels test ! -f /wal-archive/%f && cp %p /wal-archive/%f as correct. Line 337 states that this command fails on an identical retry and was replaced. Mark the old form as non-overwriting but not idempotent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/audits/P7-C-REBUILD-ADVERSARIAL-PG-BACKUP-CONFIG-AUDIT.md` at line 85,
Update the WAL archive_command verdict in the audit table to indicate that `test
! -f /wal-archive/%f && cp %p /wal-archive/%f` prevents overwrites but is not
idempotent, aligning it with the corrective finding and replacement documented
near line 337.

- Static guards: 7 of 8 mutations caught (§6); M4 (PG 18→19) not caught statically — P3.
- `verify:static` wiring: `verify-static-includes-guards.mjs` asserts `lint:repo-contract` ∈ `verify:static`; `package.json` chains both contract scripts into `lint:repo-contract`. Verified.
- Unit tests: `preflight.test.ts` (12 cases), `launchpad.test.ts` (8 + concurrency). CI API coverage SUCCESS on final head.
- Clean-host workflow triggers: `workflow_dispatch` + push to `feat/p7-c1-portable-single-node-deployment` only — **after merge the clean-host proof never runs automatically (HYP-8 TRUE)**; the permanent gate is the local clean-root drill (`pnpm drill:p7-c1-relocation`, manual) + static contracts. Classified: P3 coverage debt (deliberate, documented; but note the workflow required 3 commits to pass, so its "manual drill" value depends on it being run — recommend at least a scheduled/manual-on-master trigger).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Make the HYP-8 severity distinction explicit.

Line 225 classifies the post-merge clean-host gate as P3. Lines 275-276 list the same issue as P2-2. Use one severity in the summary, findings, and merge recommendation, or explain that P3 describes coverage debt while P2 describes the inaccurate “permanently guarded” claim.

Also applies to: 275-276

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/audits/P7-C1-ADVERSARIAL-PORTABLE-DEPLOYMENT-AUDIT.md` at line 225,
Reconcile the HYP-8 severity classification across the summary, findings around
HYP-8, and merge recommendation. Either use one consistent severity everywhere
or explicitly distinguish P3 as the clean-host coverage debt and P2-2 as the
inaccurate “permanently guarded” claim, including that distinction in the
relevant sections.

…lity-named deployment tests

Corrective pass per PR #274 final review:

- Fix the invalid F1 'missing WAL' proof: the old test deleted pg_wal inside
  the base backup and treated normal end-of-archive file-not-found as
  failure. The corrected test removes ONE archived WAL segment required to
  reach an explicit recovery_target_lsn (untouched base backup) and
  asserts the target is UNREACHABLE: cluster stays in archive recovery
  (pg_controldata), restore_command failures for the missing segment are
  visible, and no promotion within a bounded window.
- Remove scripts/deployment/ as a permanent surface: phase-named drills
  moved to tests/deployment/ with capability names (compose-smoke,
  launchpad-bootstrap, persistence-and-cold-restore, logical-backup-restore,
  pitr) + shared lib.sh helpers; p6-corr1 compose smoke renamed and kept as
  the compose capability suite.
- No temporary Compose overrides in any test: recovery clusters start from
  the canonical docker-compose.yml with EXAM_DATA_ROOT /
  EXAM_WAL_ARCHIVE_HOST_PATH / POSTGRES_PASSWORD / COMPOSE_PROJECT_NAME.
- Fixed sleeps replaced with bounded polling (pg_isready, /api/health,
  archived-segment presence, log evidence, pg_controldata state).
- Operator scripts: derive POSTGRES_USER/POSTGRES_DB from the deployment
  (logical backup/restore), safe psql-variable TARGET_DB handling with a
  conservative name contract, removed the redundant backup prompt and
  numeric-UID claims, corrected pg_verifybackup 'manifest signature'
  wording (checksums, not a signature) and the false docker-inspect claim.
- Launchpad backend: canonical bootstrap accepts an explicit audit source
  (local_script | launchpad); the HTTP race loser maps to 409
  LAUNCHPAD_ALREADY_INITIALIZED via a typed AdminAlreadyExistsError (never
  500); deterministic concurrency tests (DB-level + HTTP-level).
- Closeout rewritten to describe ONE final architecture (no stale
  docker-compose.pitr.yml / wal-archive.conf claims, no self-referential
  final-head SHA); backup-and-recovery.md, runbook, roadmap, compose and
  env comments aligned to final reality.
- package.json gains test:deployment[:compose|launchpad|persistence|logical|pitr];
  deployment-topology-contract.mjs now patterns docker-compose.test*.yml
  and checks tests/deployment/ for developer-path leakage.
@github-actions

Copy link
Copy Markdown

AI review done up to commit: a8767df

AI Review Summary:

The pull request significantly refactors the deployment test suite, moving individual drill scripts into a consolidated and well-structured set of suites under tests/deployment/ and introducing a shared lib.sh helper script. This improves maintainability, readability, and robustness of the tests.

Key logical changes include:

  • Introducing AdminAlreadyExistsError for clearer error handling when attempting to bootstrap an admin when one already exists.
  • Enhancing the admin.bootstrap audit log with a source field to indicate whether the bootstrap originated from a local script or the Launchpad UI.
  • Implementing robust concurrency handling for Launchpad bootstrap requests using a PostgreSQL advisory lock, ensuring only one bootstrap succeeds and others gracefully return a 409 conflict.

Key security and robustness improvements include:

  • In postgres-logical-backup.sh and postgres-logical-restore.sh, PostgreSQL user and database names are now dynamically derived from the running container's environment, preventing hardcoding and correctly adapting to operator customizations.
  • In postgres-logical-restore.sh, the TARGET_DB parameter is now validated as a conservative PostgreSQL identifier and passed safely to psql using quoted-identifier interpolation, preventing SQL injection.
  • The deployment-topology-contract.mjs is updated to enforce the "ONE-COMPOSE MODEL" more strictly for production deployments, disallowing variant Compose files for operational capabilities like PITR, while offering more flexible patterns for development/test Compose files.

The documentation in docs/ and .env.example, README.md, docker-compose.yml is updated to reflect these changes, improving clarity and accuracy.

Overall, the changes are of high quality, addressing potential race conditions, improving auditability, enhancing security, and significantly improving the testing infrastructure. No major logical errors, bugs, or security vulnerabilities were found in the revised code.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/api/src/routes/launchpad.ts (1)

74-76: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Pass context to defaultOrganizationExists.

isInstallationInitialized invokes repository data access without ctx. This bypasses the route-to-repository context contract. Add an explicit context parameter to defaultOrganizationExists and pass the applicable public or system context from this route.

As per coding guidelines, all route data access must go through repo.method(ctx, ...).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/routes/launchpad.ts` around lines 74 - 76, Update
isInstallationInitialized to accept the applicable route context and pass it to
createOrganizationRepo(fastify.db).defaultOrganizationExists(ctx). Ensure the
route’s repository access follows the repo.method(ctx, ...) contract, using the
established public or system context for this route.

Source: Coding guidelines

♻️ Duplicate comments (1)
scripts/backup/postgres-logical-restore.sh (1)

143-150: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

DROP DATABASE still fails if any session is connected.

The identifier-injection half of the earlier review is resolved: Lines 79-84 bound TARGET_DB to a plain identifier, and Lines 145-149 pass it as a psql variable with :"target_db" quoted-identifier interpolation.

The second half is not addressed. DROP DATABASE IF EXISTS at Line 148 aborts when any backend is connected to the target. The app and email-worker services use restart: unless-stopped, so one reconnecting client fails the restore after the operator already typed the destructive confirmation. Add WITH (FORCE).

🐛 Proposed fix
-DROP DATABASE IF EXISTS :"target_db";
+DROP DATABASE IF EXISTS :"target_db" WITH (FORCE);
 CREATE DATABASE :"target_db" TEMPLATE template0;
PostgreSQL DROP DATABASE WITH FORCE supported version
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/backup/postgres-logical-restore.sh` around lines 143 - 150, Update
the DROP DATABASE statement in the restore heredoc to use PostgreSQL’s WITH
(FORCE) option, preserving the existing :"target_db" identifier interpolation
and CREATE DATABASE flow so connected clients are terminated before recreation.
🧹 Nitpick comments (5)
tests/deployment/lib.sh (1)

137-145: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider deriving the PostgreSQL role and database from the running container.

psql_exec and wait_for_postgres hard-code -U exam -d exam. scripts/backup/postgres-logical-backup.sh (Lines 91-99) and scripts/backup/postgres-logical-restore.sh (Lines 102-109) derive POSTGRES_USER and POSTGRES_DB from the container environment with an exam fallback. If an operator customizes those values, the backup scripts adapt and the drills do not. The drills set the environment themselves, so this is not a current defect; aligning the two paths removes a future divergence.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/deployment/lib.sh` around lines 137 - 145, Update psql_exec and
wait_for_postgres to derive POSTGRES_USER and POSTGRES_DB from the running
database container, using the same exam fallback behavior as the backup scripts,
and pass those values to psql instead of hard-coding exam.
tests/deployment/persistence-and-cold-restore.sh (1)

155-162: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The PostgreSQL major-version path is hard-coded.

Lines 157-158 assert /pg/18/docker/PG_VERSION. docker-compose.yml pins postgres:18.4-bookworm, so the path is correct today. A major-version bump silently breaks this assertion. tests/deployment/pitr.sh repeats the same postgres/18/docker literal at Lines 205, 208, 223, 224, and 330, and repeats the postgres:18.4-bookworm image tag at Lines 331 and 366.

Discover the directory instead of naming the version, or derive the version from the Compose image tag once and share it through tests/deployment/lib.sh.

♻️ Proposed refactor
-if ! docker run --rm -v "${ROOT_C}/postgres:/pg:ro" alpine:latest \
-  sh -c 'test -f /pg/18/docker/PG_VERSION && cat /pg/18/docker/PG_VERSION'; then
+if ! docker run --rm -v "${ROOT_C}/postgres:/pg:ro" alpine:latest \
+  sh -c 'f=$(find /pg -maxdepth 3 -name PG_VERSION -print -quit); test -n "$f" && cat "$f"'; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/deployment/persistence-and-cold-restore.sh` around lines 155 - 162,
Remove the hard-coded PostgreSQL major-version directory from the PGDATA
visibility check and related PITR paths. Add shared version discovery or
derivation in tests/deployment/lib.sh, then reuse that symbol in
persistence-and-cold-restore.sh and pitr.sh for PG_VERSION paths and PostgreSQL
image references so version bumps update the tests automatically.
scripts/repository-contract/deployment-topology-contract.mjs (1)

110-134: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider failing when tests/deployment/ is missing.

The catch block treats a missing directory as "no tests to validate". If the directory is deleted or renamed, the contract passes silently. The suite is now referenced by package.json scripts (test:deployment:*), so its absence is a real regression signal. Distinguish ENOENT from other read errors, or assert the directory exists.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/repository-contract/deployment-topology-contract.mjs` around lines
110 - 134, Update the deployment test discovery around deploymentTestsDir so a
missing tests/deployment directory records a contract error instead of being
treated as valid. Distinguish ENOENT from other filesystem failures, preserving
the existing validation for present directories and propagating unexpected read
errors.
tests/deployment/compose-smoke.sh (1)

191-204: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use run_compose for the remaining direct Compose invocations.

Lines 191, 202, 359, and 366 still build docker compose -p "${PROJECT}" -f "${COMPOSE_FILE}" ps -q email-worker by hand. lib.sh provides run_compose for exactly this. The behavior is identical today, but two invocation styles in one file make future Compose-flag changes easy to miss.

♻️ Proposed refactor
-WORKER_STATE=$(docker inspect "$(docker compose -p "${PROJECT}" -f "${COMPOSE_FILE}" ps -q email-worker 2>/dev/null)" --format '{{.State.Status}}' 2>/dev/null || echo "missing")
+WORKER_STATE=$(docker inspect "$(run_compose "${PROJECT}" ps -q email-worker 2>/dev/null)" --format '{{.State.Status}}' 2>/dev/null || echo "missing")

Also applies to: 359-366

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/deployment/compose-smoke.sh` around lines 191 - 204, Replace the
remaining hand-built docker compose ps invocations in the worker-state checks
and the Test 5b setup around WORKER_CONTAINER with the existing run_compose
helper, passing the same project, compose file, and ps -q email-worker
arguments. Apply the same change to the corresponding invocations near the later
referenced checks, preserving all existing output and error-handling behavior.
tests/deployment/pitr.sh (1)

306-326: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

The F1 window costs up to 150 seconds on the success path.

The first loop polls up to 60 seconds for restore-failure evidence and breaks early. The second loop polls up to 90 seconds for the promotion message and breaks only when promotion occurs. When the test passes, promotion never occurs, so the second loop always runs its full 90 seconds and issues 90 docker compose logs calls. Confirm this budget is acceptable for pnpm test:deployment. A shorter window that still exceeds the observed happy-path promotion time gives the same proof.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/deployment/pitr.sh` around lines 306 - 326, Shorten the second polling
window in the F1 recovery check around PROMOTED_EARLY so the successful
required-WAL-missing path does not spend the full 90 seconds polling. Retain
enough iterations to exceed the observed time for an incorrect promotion,
preserve detection of the “database system is ready to accept connections”
message, and keep the existing failure handling intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md`:
- Around line 165-167: Rewrite the PITR statement in the closeout document to
describe the 60-second archive_timeout as an archival-freshness delay bound for
active workloads, not a recovery-precision or target-granularity limit. State
that recovery depends on the available archived WAL and the selected recovery
target, such as recovery_target_time, recovery_target_lsn, recovery_target_name,
or recovery_target_xid.
- Around line 101-103: Update the statement in the audit closeout to scope
runtime credential derivation to “Online PostgreSQL scripts” instead of “All
scripts.” Add documentation for the cold-filesystem backup and restore scripts
describing their path and ownership checks, without implying they use a running
database container.

In `@docs/deployment/backup-and-recovery.md`:
- Line 574: Update the F1 heading text to use a single modal by replacing
“missing REQUIRED archived WAL” with “missing required WAL” or equivalent
wording such as “WAL required for recovery is missing,” while preserving the
rest of the heading.
- Around line 373-378: Update the pg_verifybackup description in the backup
verification section to remove file modification-time validation, stating that
verification covers file checksums and the manifest checksum. Retain SHA256 as
the checksum algorithm because the backup script passes --manifest-checksums
SHA256.

In `@tests/deployment/launchpad-bootstrap.sh`:
- Around line 109-110: Replace the direct exit after compose_logs in the
health-timeout path with a call to fail(), preserving log collection first so
timeout failures update FAIL_COUNT and use the standard failure summary.
- Around line 250-252: After Stack B becomes healthy, add a check against the
running app container’s environment to verify LAUNCHPAD_SETUP_TOKEN is unset or
empty, failing the test if a non-empty value is present. Keep the existing unset
LAUNCHPAD_SETUP_TOKEN setup and use the Stack B container-health flow to perform
this assertion.
- Around line 33-37: Update the run identifier initialization around TS,
PROJECT_A, and PROJECT_B to use a per-run unique value rather than a
seconds-only timestamp, and derive both Compose project names from that value.
Preserve the existing project-specific naming and ensure the identifier is
available before Compose launches so compose_down_best_effort receives unique
names.

In `@tests/deployment/lib.sh`:
- Around line 117-135: Update tests/deployment/lib.sh lines 117-135 so
safe_temp_root records or exposes each TMPDIR-aware path it creates, and
cleanup_temp_root safely removes only those roots. In
tests/deployment/compose-smoke.sh lines 106-109, replace the hard-coded /tmp
guard and host rm -rf with comparison to the returned safe_temp_root path
followed by cleanup_temp_root. Apply the same recorded-path comparisons in
tests/deployment/logical-backup-restore.sh lines 56-64,
tests/deployment/persistence-and-cold-restore.sh lines 63-73, and
tests/deployment/pitr.sh lines 68-79; in pitr.sh also remove the inert
CORRUPT_DIR entry added at line 350.
- Around line 24-39: Update run_compose so an empty first argument is consumed
before forwarding arguments to docker compose, while keeping project empty so
COMPOSE_PROJECT_NAME remains the fallback. Preserve the existing behavior for
non-empty project names and flag-first arguments, and ensure helpers such as
db_container and psql_exec no longer pass an empty argument through.

In `@tests/deployment/logical-backup-restore.sh`:
- Around line 42-43: Update the cleanup guards in the deployment test scripts
around ROOT, BACKUP_DIR_PARENT, and their corresponding cleanup blocks to
compare paths against the values returned by safe_temp_root rather than assuming
a /tmp prefix. Apply the same change to persistence-and-cold-restore.sh and
pitr.sh, ensuring cleanup_temp_root runs when TMPDIR points elsewhere.

In `@tests/deployment/persistence-and-cold-restore.sh`:
- Around line 63-73: Update cleanup() to validate created directories against
the actual paths returned by safe_temp_root, rather than requiring a hard-coded
/tmp prefix in its regex. Preserve the existing name-pattern restrictions for
persist roots and backups, while allowing any TMPDIR location so
cleanup_temp_root runs for all created directories.

In `@tests/deployment/pitr.sh`:
- Around line 68-79: Update cleanup() to validate registered directories against
the actual paths returned by safe_temp_root, without assuming a /tmp prefix,
while preserving the allowed PITR root-name pattern. Remove the CREATED_DIRS
registration of CORRUPT_DIR near its creation, since that path cannot satisfy
the cleanup guard.
- Around line 131-151: Update run_idem_case to accept an explicit case-name
argument in addition to src, tgt, and expect, and use that name in both pass and
fail messages instead of ${3}. Update each run_idem_case invocation to provide a
distinct label for the absent-target, identical-retry, and byte-collision cases
so their results remain distinguishable.

---

Outside diff comments:
In `@apps/api/src/routes/launchpad.ts`:
- Around line 74-76: Update isInstallationInitialized to accept the applicable
route context and pass it to
createOrganizationRepo(fastify.db).defaultOrganizationExists(ctx). Ensure the
route’s repository access follows the repo.method(ctx, ...) contract, using the
established public or system context for this route.

---

Duplicate comments:
In `@scripts/backup/postgres-logical-restore.sh`:
- Around line 143-150: Update the DROP DATABASE statement in the restore heredoc
to use PostgreSQL’s WITH (FORCE) option, preserving the existing :"target_db"
identifier interpolation and CREATE DATABASE flow so connected clients are
terminated before recreation.

---

Nitpick comments:
In `@scripts/repository-contract/deployment-topology-contract.mjs`:
- Around line 110-134: Update the deployment test discovery around
deploymentTestsDir so a missing tests/deployment directory records a contract
error instead of being treated as valid. Distinguish ENOENT from other
filesystem failures, preserving the existing validation for present directories
and propagating unexpected read errors.

In `@tests/deployment/compose-smoke.sh`:
- Around line 191-204: Replace the remaining hand-built docker compose ps
invocations in the worker-state checks and the Test 5b setup around
WORKER_CONTAINER with the existing run_compose helper, passing the same project,
compose file, and ps -q email-worker arguments. Apply the same change to the
corresponding invocations near the later referenced checks, preserving all
existing output and error-handling behavior.

In `@tests/deployment/lib.sh`:
- Around line 137-145: Update psql_exec and wait_for_postgres to derive
POSTGRES_USER and POSTGRES_DB from the running database container, using the
same exam fallback behavior as the backup scripts, and pass those values to psql
instead of hard-coding exam.

In `@tests/deployment/persistence-and-cold-restore.sh`:
- Around line 155-162: Remove the hard-coded PostgreSQL major-version directory
from the PGDATA visibility check and related PITR paths. Add shared version
discovery or derivation in tests/deployment/lib.sh, then reuse that symbol in
persistence-and-cold-restore.sh and pitr.sh for PG_VERSION paths and PostgreSQL
image references so version bumps update the tests automatically.

In `@tests/deployment/pitr.sh`:
- Around line 306-326: Shorten the second polling window in the F1 recovery
check around PROMOTED_EARLY so the successful required-WAL-missing path does not
spend the full 90 seconds polling. Retain enough iterations to exceed the
observed time for an incorrect promotion, preserve detection of the “database
system is ready to accept connections” message, and keep the existing failure
handling intact.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ce4ead88-87f3-4a64-bbae-ad470f582ac1

📥 Commits

Reviewing files that changed from the base of the PR and between 567e0ad and a8767df.

📒 Files selected for processing (28)
  • .env.example
  • README.md
  • apps/api/src/audit/auditPolicy.ts
  • apps/api/src/routes/launchpad.test.ts
  • apps/api/src/routes/launchpad.ts
  • apps/api/src/scripts/bootstrap-admin.test.ts
  • apps/api/src/scripts/bootstrap-admin.ts
  • docker-compose.yml
  • docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md
  • docs/deployment/backup-and-recovery.md
  • docs/deployment/mvp-deployment-runbook.md
  • docs/roadmap/P7-system-readiness-and-exam-modes.md
  • package.json
  • packages/contracts/src/messageRegistry.ts
  • packages/domain/src/errors.ts
  • scripts/backup/cold-filesystem-backup.sh
  • scripts/backup/cold-filesystem-restore.sh
  • scripts/backup/pg-basebackup.sh
  • scripts/backup/postgres-enable-pitr.sh
  • scripts/backup/postgres-logical-backup.sh
  • scripts/backup/postgres-logical-restore.sh
  • scripts/repository-contract/deployment-topology-contract.mjs
  • tests/deployment/compose-smoke.sh
  • tests/deployment/launchpad-bootstrap.sh
  • tests/deployment/lib.sh
  • tests/deployment/logical-backup-restore.sh
  • tests/deployment/persistence-and-cold-restore.sh
  • tests/deployment/pitr.sh
🚧 Files skipped from review as they are similar to previous changes (11)
  • .env.example
  • docs/deployment/mvp-deployment-runbook.md
  • README.md
  • packages/contracts/src/messageRegistry.ts
  • docs/roadmap/P7-system-readiness-and-exam-modes.md
  • scripts/backup/postgres-logical-backup.sh
  • scripts/backup/pg-basebackup.sh
  • docker-compose.yml
  • scripts/backup/postgres-enable-pitr.sh
  • apps/api/src/routes/launchpad.test.ts
  • scripts/backup/cold-filesystem-restore.sh

Comment thread docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md Outdated
Comment thread docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md Outdated
Comment thread docs/deployment/backup-and-recovery.md
Comment thread docs/deployment/backup-and-recovery.md Outdated
Comment thread tests/deployment/launchpad-bootstrap.sh
Comment thread tests/deployment/lib.sh
Comment thread tests/deployment/logical-backup-restore.sh Outdated
Comment thread tests/deployment/persistence-and-cold-restore.sh
Comment thread tests/deployment/pitr.sh
Comment thread tests/deployment/pitr.sh
@github-actions

Copy link
Copy Markdown

AI review done up to commit: fab5b7f

AI Review Summary:

The changes in this pull request primarily focus on improving the robustness and clarity of deployment scripts and documentation, alongside a minor API modification to pass context.

Key changes include:

  • Updating cold-filesystem-backup.sh and cold-filesystem-restore.sh scripts to use a specific PostgreSQL helper image (postgres:18.4-bookworm) instead of alpine:latest, enhancing reliability for PostgreSQL data operations.
  • Modifying postgres-logical-restore.sh to include WITH (FORCE) when dropping a database, improving resilience against lingering connections during restores.
  • Updating documentation files (docs/deployment/backup-and-recovery.md, docs/deployment/mvp-deployment-runbook.md) to reflect changes in backup script usage and to provide more precise descriptions of backup verification and restore processes.
  • Adjusting the isInstallationInitialized function in the API to pass a PublicBrandingContext to the defaultOrganizationExists repository method, which introduces a new context parameter, though it remains unused in the current implementation of the repository method.

Overall quality assessment: The changes are well-intentioned, improving the robustness of backup/restore procedures and updating documentation for clarity. The introduction of PublicBrandingContext in the API seems to be a preparatory step for future functionality. No critical bugs, logical errors, or security vulnerabilities were found. The changes enhance the system's operational reliability and documentation accuracy.

… cleanup, PG version derivation

Review follow-up on PR #274:

- lib.sh: safe_temp_root now records every created path in a registry and
  cleanup_temp_root removes ONLY recorded roots (exact match), so TMPDIR
  location never matters and arbitrary paths can never be removed.
  run_compose consumes an explicit empty first argument (helpers no longer
  forward an empty arg). psql_exec / wait_for_postgres derive
  POSTGRES_USER/POSTGRES_DB from the running db container (exam fallback).
  PG_IMAGE / PG_MAJOR are derived from docker-compose.yml so a version bump
  updates the tests automatically.
- compose-smoke / launchpad-bootstrap / persistence-and-cold-restore /
  logical-backup-restore / pitr: cleanup guards now compare against the
  safe_temp_root registry instead of hard-coded /tmp regexes; the compose
  smoke data root is removed container-assisted (host rm -rf could not
  delete postgres-owned files). pitr drops the inert CORRUPT_DIR
  registration (nested under a registered root), names the archive
  idempotency cases explicitly (absent-target / identical-retry /
  byte-collision), shortens the F1 no-promotion window to 30s (exceeds any
  incorrect-promotion time), and uses the derived PG_MAJOR/PG_IMAGE in
  PGDATA paths and postgres image references.
- launchpad-bootstrap: per-run unique RUN_ID for Compose project names
  (seconds-only timestamp collides across runs), health-timeout path uses
  fail() so FAIL_COUNT/summary stay consistent, and Stack B now asserts the
  running app container's environment carries no LAUNCHPAD_SETUP_TOKEN.
- compose-smoke: remaining hand-built docker compose ps invocations routed
  through run_compose.
- deployment-topology-contract.mjs: a missing tests/deployment/ directory
  is now a contract error (ENOENT distinguished; other errors propagate).
- Docs: closeout scopes runtime credential derivation to the online
  PostgreSQL scripts and documents the cold scripts' path/ownership checks;
  archive_timeout described as an archival-freshness bound for active
  workloads (recovery depends on available WAL + the selected target type),
  not a target-granularity limit. backup-and-recovery.md F1 heading reads
  'missing required WAL'; pg_verifybackup wording drops the inaccurate
  mtime validation (checksums + manifest checksum only), also fixed in
  pg-basebackup.sh's comment.
Comment thread tests/deployment/pitr.sh
fi
compose_down_best_effort "${PROJECT_REC}-miss"

# ── 6. F2: corrupt base backup → pg_verifybackup rejects ─────────────────

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of this line is correct because CORRUPT_DIR is not created via safe_temp_root (and thus should not be managed by cleanup_temp_root). However, CORRUPT_DIR is created via mkdir -p at line 347 and is not explicitly cleaned up later in the script's cleanup function. This will lead to CORRUPT_DIR being left behind after the test run, which is a minor resource leak. Please add rm -rf "${CORRUPT_DIR}" to the cleanup function or ensure it is otherwise removed.

@github-actions

Copy link
Copy Markdown

AI review done up to commit: d15702f

AI Review Summary:

The pull request introduces significant improvements to the deployment test suite and related documentation. Key changes include:

  • Enhanced error handling for a missing tests/deployment/ directory, making the repository contract more robust.
  • Refactoring of test scripts to use shared helper functions (run_compose, cleanup_temp_root, pg_user_db, PG_MAJOR discovery), which centralizes logic, reduces duplication, and improves maintainability and safety.
  • Dynamic discovery of PostgreSQL user/database and version from the running containers/compose file, making tests resilient to credential/version changes.
  • Implementation of a registry for temporary directories created by safe_temp_root, ensuring that cleanup_temp_root only removes paths specifically managed by the testing framework, preventing accidental data loss.
  • Addition of a new security-related test to verify that the LAUNCHPAD_SETUP_TOKEN is not inadvertently exposed to the application container when it should be unset.
  • Refinement of documentation regarding PostgreSQL archive_timeout and recovery precision.

Overall, the quality of the changes is high. The refactoring leads to a more robust, secure, and flexible test suite. The new safety mechanisms for temporary directory cleanup are particularly noteworthy. I've only identified one minor resource leak where a temporary directory created in pitr.sh is not explicitly cleaned up.

The temp-root registry introduced in d15702f was INERT: safe_temp_root
was always called via command substitution (VAR="$(safe_temp_root ...)"),
which runs the function in a subshell, so the SAFE_TEMP_ROOTS append was
lost. cleanup_temp_root's registry check therefore always bailed
(found != yes) and NO suite ever cleaned up its temp roots — every
deployment-test run leaked gigabytes under /tmp. The suites passed
because cleanup failure is silent.

Fix: safe_temp_root now takes the caller's variable NAME and assigns via
printf -v in the caller's scope, so both the mktemp path and the registry
append survive. All 14 call sites across the five suites are converted
from command substitution to the variable-name convention (the function
fails fast on a missing name, so a future command-substitution call is a
hard error, not a silent leak).

Also:
- launchpad-bootstrap.sh was the ONE suite d15702f missed: its cleanup
  still used the old '/tmp/launchpad-' regex guard + host rm -rf (which
  cannot delete postgres-owned PGDATA files). Converted to
  cleanup_temp_root, matching the other four suites.
- cleanup_temp_root now WARNs to stderr when a root survives removal, so
  a future cleanup regression is visible instead of silent.

Verified: full pnpm test:deployment (compose-smoke, launchpad-bootstrap
13/13, persistence, logical, pitr 7/7) passes AND leaves zero temp roots;
launchpad re-run after conversion cleans its WORK root. All pre-fix
leftovers removed manually.
@github-actions

Copy link
Copy Markdown

AI review done up to commit: 5c4fc99

AI Review Summary:

The pull request refactors the temporary directory management in the deployment test suite. The safe_temp_root function in lib.sh was updated to accept a variable name for assigning the temporary path, and a new cleanup_temp_root function was added for robust cleanup of these directories. All relevant test scripts have been updated to use this new pattern. The changes significantly improve the reliability and safety of temporary resource handling, especially concerning file ownership within containers. The overall quality of the changes is good, with no apparent bugs or security vulnerabilities.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/deployment/lib.sh`:
- Around line 160-190: Update cleanup_temp_root to use the pinned ${PG_IMAGE}
container image instead of alpine:latest, and add Docker’s --pull=never option
so cleanup only uses an image already present locally without contacting a
registry.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cc06233f-ba5a-4135-b463-3e3f7a6469c7

📥 Commits

Reviewing files that changed from the base of the PR and between a8767df and 5c4fc99.

📒 Files selected for processing (16)
  • apps/api/src/routes/launchpad.ts
  • docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md
  • docs/deployment/backup-and-recovery.md
  • docs/deployment/mvp-deployment-runbook.md
  • packages/db/src/repository/organizationRepo.ts
  • scripts/backup/cold-filesystem-backup.sh
  • scripts/backup/cold-filesystem-restore.sh
  • scripts/backup/pg-basebackup.sh
  • scripts/backup/postgres-logical-restore.sh
  • scripts/repository-contract/deployment-topology-contract.mjs
  • tests/deployment/compose-smoke.sh
  • tests/deployment/launchpad-bootstrap.sh
  • tests/deployment/lib.sh
  • tests/deployment/logical-backup-restore.sh
  • tests/deployment/persistence-and-cold-restore.sh
  • tests/deployment/pitr.sh
🚧 Files skipped from review as they are similar to previous changes (13)
  • apps/api/src/routes/launchpad.ts
  • tests/deployment/logical-backup-restore.sh
  • tests/deployment/persistence-and-cold-restore.sh
  • scripts/repository-contract/deployment-topology-contract.mjs
  • docs/deployment/backup-and-recovery.md
  • scripts/backup/postgres-logical-restore.sh
  • scripts/backup/cold-filesystem-restore.sh
  • packages/db/src/repository/organizationRepo.ts
  • scripts/backup/pg-basebackup.sh
  • docs/deployment/mvp-deployment-runbook.md
  • tests/deployment/compose-smoke.sh
  • scripts/backup/cold-filesystem-backup.sh
  • docs/audits/P7-C-PORTABLE-BACKUP-RECOVERY-CLOSEOUT.md

Comment thread tests/deployment/lib.sh
Comment on lines +160 to +190
# Remove a temp root created by safe_temp_root. Files inside may be owned
# by the container postgres user (not host-readable), so removal is
# container-assisted. Paths NOT recorded by safe_temp_root are never
# touched.
cleanup_temp_root() {
local d="$1"
local i
local remaining=()
local found="no"
for i in "${SAFE_TEMP_ROOTS[@]}"; do
if [ "${i}" = "${d}" ]; then
found="yes"
else
remaining+=("${i}")
fi
done
if [ "${found}" != "yes" ]; then
return 0
fi
SAFE_TEMP_ROOTS=("${remaining[@]}")
if [ ! -d "${d}" ]; then
return 0
fi
docker run --rm -v "${d}:/d" alpine:latest \
sh -c 'rm -rf /d/* /d/.[!.]* 2>/dev/null || true' \
>/dev/null 2>&1 || true
rmdir "${d}" 2>/dev/null || rm -rf "${d}" 2>/dev/null || true
if [ -d "${d}" ]; then
echo "WARN: cleanup_temp_root could not fully remove ${d} (left in place)." >&2
fi
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "files:"
fd -a 'lib\.sh$|compose\.ya?ml$|Dockerfile$|docs/SPEC\.md|docs/roadmap/phase-roadmap\.md' . | sed 's#^\./##'

echo
echo "tests/deployment/lib.sh occurrences around safe_temp_root/cleanup_temp_root/PG_IMAGE:"
python3 - <<'PY'
from pathlib import Path
p=Path('tests/deployment/lib.sh')
lines=p.read_text(errors='replace').splitlines()
for i,l in enumerate(lines,1):
    if any(x in l for x in ['safe_temp_root','cleanup_temp_root','PG_IMAGE','docker run','alpine:latest','pull=never','alpine']):
        print(f"{i}: {l}")
PY

echo
echo "docker references in deployment scripts:"
rg -n "docker (run|build|compose)|pull=never|alpine:latest|PG_IMAGE|safe_temp_root" tests -S || true

echo
echo "compose image names mentioning postgres:"
rg -n "postres|postgres|image:|version:" -S compose* docker-compose* .github 2>/dev/null || true

Repository: jnhu76/exam

Length of output: 11939


🌐 Web query:

Docker run default pull policy missing image docker.com

💡 Result:

When using the docker run command, the default pull policy for a missing image is missing [1][2][3]. This means that Docker will only attempt to pull the image from the registry if it is not already present in the local image cache [1]. If the image is found locally, Docker will use the cached version instead of pulling it again [1]. This behavior can be controlled using the --pull flag, which supports the following options [1][3]: - missing (default): Pull the image only if it is not found in the local cache [1]. - always: Always pull the image from the registry before creating the container, even if it already exists locally [1]. - never: Never attempt to pull the image; the command will fail if the image is not found in the local cache [1]. It is important to distinguish this from Kubernetes, which has different default behaviors for image pull policies depending on the image tag (e.g.,:latest or missing tags default to Always) [4]. In contrast, the standard Docker CLI command consistently defaults to missing [1].

Citations:


Use a local-only image during temp-root cleanup.

cleanup_temp_root() calls Docker at line 183. If alpine:latest is not present locally, Docker pulls it by default, and latest can change. This adds a runtime registry dependency during offline-required deployment steps. Use a fully pinned image present on the deployment host, such as the pinned ${PG_IMAGE}, and pass --pull=never.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/deployment/lib.sh` around lines 160 - 190, Update cleanup_temp_root to
use the pinned ${PG_IMAGE} container image instead of alpine:latest, and add
Docker’s --pull=never option so cleanup only uses an image already present
locally without contacting a registry.

Source: Coding guidelines

@jnhu76
jnhu76 merged commit b4e18b2 into master Aug 10, 2026
9 checks passed
@jnhu76
jnhu76 deleted the feat/p7-c-portable-backup-recovery branch August 10, 2026 12:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant