Deferred from the PR #99 review. The Docker/compose artifact shipped in that release works, but is not production-shaped, and the Turso migration path lacks a written recovery procedure.
Container
- Runs as root.
Dockerfile has no USER node, and ships the full workspace including dev dependencies and the build toolchain.
compose
- No TLS while cookies are
secure: isProd. compose.yaml binds 3000:3000 over plain HTTP. A production-mode container therefore silently fails to set the session cookie, and login just appears to do nothing. The env example doesn't mention that a TLS terminator is required.
- First boot lands on a site that redirects every page.
SESSION_SECRET= ships blank and the only seeded league is required-gate. The env comment says it "500s without it"; checkLeagueAccess actually returns "redirect". Seeding a --gate optional starter league would make docker compose up work out of the box.
- Two writers, one SQLite file, no coordination.
web and the ingest sidecar share the DB over a volume, with || true swallowing failures, and apps/web/src/lib/prisma.ts configures no WAL mode or busy_timeout. A SQLITE_BUSY would be silent and simply retried 30 minutes later.
Long-running request handler
POST /api/admin/leagues/[slug]/ingest-now runs the whole scrape synchronously in a request handler: N × 1.5 s polite delay + download + pdftotext. With no proxy in front that's fine; behind anything else it hits a gateway timeout while the run continues in the background.
The mutex does release correctly in finally, so there is no lock leak — but this wants to be a background job with a status endpoint.
Turso migration recovery procedure (docs)
Dropping the wrapping transaction in migrate-turso.ts was the right call (see #93 — PRAGMA foreign_keys is a no-op inside a transaction), and PR #99 added structural enforcement of that invariant. The trade-off is that there is no local rollback: a failure part-way through a multi-statement migration leaves the DB in an intermediate state.
docs/ should carry the recovery procedure — how to establish a PITR restore point before migrating, and how to determine which statement actually landed — rather than leaving it to be worked out mid-incident.
Deferred from the PR #99 review. The Docker/compose artifact shipped in that release works, but is not production-shaped, and the Turso migration path lacks a written recovery procedure.
Container
Dockerfilehas noUSER node, and ships the full workspace including dev dependencies and the build toolchain.compose
secure: isProd.compose.yamlbinds3000:3000over plain HTTP. A production-mode container therefore silently fails to set the session cookie, and login just appears to do nothing. The env example doesn't mention that a TLS terminator is required.SESSION_SECRET=ships blank and the only seeded league isrequired-gate. The env comment says it "500s without it";checkLeagueAccessactually returns"redirect". Seeding a--gate optionalstarter league would makedocker compose upwork out of the box.weband theingestsidecar share the DB over a volume, with|| trueswallowing failures, andapps/web/src/lib/prisma.tsconfigures no WAL mode orbusy_timeout. ASQLITE_BUSYwould be silent and simply retried 30 minutes later.Long-running request handler
POST /api/admin/leagues/[slug]/ingest-nowruns the whole scrape synchronously in a request handler: N × 1.5 s polite delay + download +pdftotext. With no proxy in front that's fine; behind anything else it hits a gateway timeout while the run continues in the background.The mutex does release correctly in
finally, so there is no lock leak — but this wants to be a background job with a status endpoint.Turso migration recovery procedure (docs)
Dropping the wrapping transaction in
migrate-turso.tswas the right call (see #93 —PRAGMA foreign_keysis a no-op inside a transaction), and PR #99 added structural enforcement of that invariant. The trade-off is that there is no local rollback: a failure part-way through a multi-statement migration leaves the DB in an intermediate state.docs/should carry the recovery procedure — how to establish a PITR restore point before migrating, and how to determine which statement actually landed — rather than leaving it to be worked out mid-incident.