SECTION 01
Make editorial publication a database state transition
Draft creation, question editing, validation, publication, and audit recording use locks and transactions so readers never observe half a release.
Creating a draft takes a table lock, copies the active release's modules and lessons, increments every question revision, rewrites template revision references, and records an audit event inside one transaction. Editors can change only a draft. The update path locks the release row, applies structured options, key, explanation, and difficulty, then writes an audit event before commit. A failure rolls back both the edit and its claimed evidence.
Publishing locks release transitions, row-locks the selected draft, executes the full database validator on that same transaction client, archives the former published release, stamps and publishes the draft, and records validation counts. If validation finds a broken graph, incomplete item, wrong inventory, or malformed simulation template, the endpoint returns an error and no status changes commit. Curriculum readers continue resolving the prior published release throughout.
- Published
Current catalogue for pages and new sessions.
- Draft clone
Editable question revisions and matching template references.
- Validate
Graph, inventory, source, item, and template checks.
- New published
Stamped active release after one commit.
- Archived previous
Former release retained but no longer selected.
SECTION 02
Ship versioned, health-checked containers to pve21
Deployment copies the current workspace with selected exclusions, builds a versioned image, backs up the database, publishes seeded content explicitly, verifies application version, and only then advances the current symlink.
The deploy script validates its release identifier and absolute remote root, refuses to overwrite an existing release directory, and excludes environment variants, dependencies, build output, backups, and workspace metadata from transfer. It archives the current workspace rather than enforcing a clean Git tree. On pve21 it maintains versioned release directories plus a shared private environment file. Before replacing an existing version it takes a logical backup, then builds the web image and starts PostgreSQL to health.
Bootstrap applies migrations and seeds the new content with publication explicitly enabled. Compose then starts the web service and waits for health; an HTTP check must report the expected application version before the current symlink moves. The runtime uses an unprivileged UID, a read-only root filesystem, bounded temporary filesystems, dropped Linux capabilities, no-new-privileges, resource and PID limits, log rotation, and health checks. These are defense-in-depth controls, not a replacement for the trusted-LAN boundary described earlier.
| Checkpoint | Evidence | If it fails |
|---|---|---|
| Transfer | New validated release directory | Do not overwrite prior files |
| Backup | List-readable custom-format dump | Abort before publication |
| Database | Compose health check succeeds | Keep or restore prior service |
| Application | Health JSON reports expected version | Attempt automatic previous-version restore |
| Activation | current symlink targets release | Prior symlink remains or is restored |
SECTION 03
Validate backup completion and make restore destructive on purpose
Backup writes privately to a partial file, validates its archive table of contents, and renames only after success; restore requires explicit confirmation and one transaction.
The backup script sets a restrictive umask, writes pg_dump custom format to a timestamped partial path, and installs a trap that removes an incomplete file. It pipes the partial archive through pg_restore --list before an atomic rename removes the suffix. A systemd timer invokes the script on the pve21 release path. This verifies archive readability, though it is not the same as restoring into a clean database and running application checks.
Restore is intentionally harder to trigger. It requires an existing file plus either an interactive RESTORE token or an explicit noninteractive confirmation variable after the operator verifies the target. The script stops web traffic, runs pg_restore with clean, if-exists, no-owner, single-transaction, and exit-on-error options, then starts web again. A trap attempts to restart web if restoration exits early. The repository does not define retention, encryption, off-host replication, or routine restore drills; operations should not imply those controls exist.
pg_dump -Fc > "$backup_file.partial"
pg_restore --list < "$backup_file.partial" > /dev/null
mv -- "$backup_file.partial" "$backup_file"SECTION 04
Document what rollback does not reverse
Versioned images and symlinks make application rollback fast, but schema migration and data recovery have distinct procedures and risk.
An explicit rollback validates the target release directory and image. When the current release and its backup script are available, rollback first takes a fresh logical backup. It then updates the shared application version, starts the target release's database and web services, verifies health and the running image, and moves the current symlink. If that operation fails, a trap attempts to restart the formerly active version. The manual path changes application code and image; it does not select a historical curriculum or restore a database dump.
Failed deployment has a slightly broader compensating path: it attempts to restart the previous image and reselect the previously published curriculum ID. It still does not reverse database migrations or restore the pre-deployment dump. Migrations may be forward compatible, but the script does not prove or enforce that property. When a schema or data change requires reversal, operators need a separately reviewed migration or the explicit restore workflow, with the outage and data-loss window understood before acting.
- Application rollback: switch to an existing versioned image and release directory.
- Failed-deploy compensation: also attempt to reselect the prior curriculum release.
- Neither path reverses applied database migrations.
- Neither path automatically restores a logical backup.
- Retention and off-host backup policy are not implemented in this repository.
REFERENCES
Sources and further reading
- TransactionsPostgreSQL Global Development Group
Database reference for all-or-nothing release changes, session creation, grading, and auditable state transitions.
- pg_dumpPostgreSQL Global Development Group
Database reference for the custom-format logical backups produced before deployments and by the scheduled backup unit.
- pg_restorePostgreSQL Global Development Group
Database reference for listing backup contents and performing explicit, single-transaction restores.
- Deploying a Next.js applicationNext.js
Framework deployment reference for the standalone Node.js output used by the container image.
KEEP EXPLORING