Reach's Wiki Gets a Verified Backup — and Railway's IaC Nearly Upgraded Four Live Databases on the Way
Outline's database now dumps weekly to R2, proven by forcing a run and reading the archive back. The plan that created it also quietly proposed bumping every Postgres in the project to 18, which would have taken them all down.
Why Care?
Reach University's wiki went live three days ago and immediately started holding real work. Until today its database had no backup — Reach's R2 bucket covered Twenty only. Now Outline dumps weekly alongside it, on the same Friday cron.
More importantly: we did not take the deployment's word for it. A green cron service proves nothing, because a cron service doesn't run when you deploy it — it builds, reports SUCCESS, and waits for Friday, with empty logs and an empty bucket. The run was forced, watched, and the resulting archive downloaded from R2 and opened.
What's New?
pg-dump-outlinein thereach-eduproject —railwayapp-templates/postgres-s3-backups, cron0 5 * * 5,SINGLE_SHOT_MODE, restartNEVER, writing tobackups/outline-postgres/in the existingreach-eduR2 bucket.Its DB URL is a Railway reference variable, not a hardcoded connection string.
pg-dump-twentyduplicates the postgres password inline; this one resolves it fromoutline-postgresat runtime.Proof, not assumption: forced run logged
pg_dump (PostgreSQL) 16.11and "Backup archive file is valid", 37.82 kB uploaded. The object was pulled back out of R2 and inspected — pg_dump directory format, 42 TABLE DATA entries, Outline's schema present (users,documents,collections,teams,revisions,attachments,stars,shares).
The near-miss worth reading
Creating that service needed Railway's IaC, because railway add -r <github repo>
returns Unauthorized even with a valid session. So: railway config pull, add
one service, railway config plan. The plan came back:
Plan: 1 to add, 7 to change, 0 to destroy
+ Create service pg-dump-outline
~ Update twenty-postgres source.image (unset → "postgres:18")
~ Update outline-postgres source.image (unset → "postgres:18")
~ Update postiz-postgres source.image (unset → "postgres:18")
~ Update temporal-postgres source.image (unset → "postgres:18")
~ …and both redis → redis:8Those run 16 / 17-alpine / redis 7 on live volumes. A Postgres major bump is not an upgrade — the data directory is incompatible and the server won't start. Applying would have taken down every database in Reach's project, CRM included.
The cause: railway config pull re-declares each postgres()/redis() with no
image, and Railway resolves "no image" to latest at apply time. Pinning all
seven to their real versions brought the plan to 1 to add, 0 to change,
0 to destroy — which is the only shape worth applying.
Written up as a reminder: context-v/reminders/Railway-IaC-Pull-Does-Not-Pin-Database-Images.md.
Gotchas
A cron service does not run on deploy. Green build, empty logs, empty bucket. Force a run by dropping
cronSchedule, applying, watching it exit, then restoring the cron.IaC deletes variables it doesn't declare — anything set via
railway variable setafter a pull vanishes on the next apply. Declare secrets aspreserve(). The plan does say- Delete variable …, so read it.railway configneeds therailwaynpm package — not@railway/sdk, notrailway-ts-sdk, both of which 404.Check the pg client against the server. The template built
postgresql16-clientand outline-postgres is 16 — matched. When they don't match, pg_dump writes an empty archive and still exits zero (that's how twf's postiz backup failed on 2026-08-05).
Left open
restore-runbook.md for Reach's Outline. A backup proven to write is not yet
a backup proven to restore, and that distinction is the whole point.
