Problem
The server starts accepting traffic before pending SQLite migrations finish, and migration failure is merely logged. Requests can therefore reach route handlers against an older schema; if a migration fails, the process remains healthy-looking and continues serving an incompatible database.
Evidence (quoted code + context)
Startup calls "httpServer.listen(PORT, HOST, () => {" at line 120, immediately logs that it is running (line 121), then begins "runMigrations().then(...)" at line 124. The catch only performs "logger.error('server', Migration error: ${err.message});" (lines 128-130), leaving the listener alive. runMigrations itself creates/uses the migration table then applies each migration asynchronously in a loop (lines 39-62), so there is an observable period in which migration.up() has not completed or has failed while routes are already mounted.
Impact
After deployment with a new migration, early requests can fail with missing tables/columns or operate on stale data. A permanent migration error does not fail readiness, so an orchestrator can route production traffic to an instance that cannot satisfy its database contract.
Implementation plan
Refactor bootstrap into an awaited async startup path: validate startup configuration, run SQLite migrations first, and only then bind/listen and report readiness. Treat a migration error as a failed startup with a non-zero exit (after closing any resources) so process supervision can surface it. Preserve a separate liveness/readiness distinction if the deployment environment needs it, and add transaction boundaries where individual migration semantics permit them.
Acceptance criteria
No HTTP listener is bound before runMigrations resolves; a rejection prevents readiness/listening and exits or rejects startup predictably; successful migration preserves the current route behavior; and startup logs clearly identify migration success/failure.
Verification
Add server Vitest tests with mocked runMigrations and httpServer.listen proving listen is deferred and rejected migration does not expose the server. Run npm test --prefix server -- --run for bootstrap/migration tests, npm run build --prefix server, and an integration smoke that starts with a pending migration then requests /api/health only after readiness.
Dependencies and related work
This concerns the active SQLite runtime only and must not duplicate or alter the planned staged PostgreSQL migration in #120 and #149-#155.
Scope (Complexity, files, non-goals)
Medium; server bootstrap, focused tests, and possibly migration transaction helpers. Non-goals: replacing SQLite or changing the PostgreSQL rollout plan.
Problem
The server starts accepting traffic before pending SQLite migrations finish, and migration failure is merely logged. Requests can therefore reach route handlers against an older schema; if a migration fails, the process remains healthy-looking and continues serving an incompatible database.
Evidence (quoted code + context)
Startup calls
"httpServer.listen(PORT, HOST, () => {"at line 120, immediately logs that it is running (line 121), then begins"runMigrations().then(...)"at line 124. The catch only performs"logger.error('server',Migration error: ${err.message});"(lines 128-130), leaving the listener alive.runMigrationsitself creates/uses the migration table then applies each migration asynchronously in a loop (lines 39-62), so there is an observable period in whichmigration.up()has not completed or has failed while routes are already mounted.Impact
After deployment with a new migration, early requests can fail with missing tables/columns or operate on stale data. A permanent migration error does not fail readiness, so an orchestrator can route production traffic to an instance that cannot satisfy its database contract.
Implementation plan
Refactor bootstrap into an awaited async startup path: validate startup configuration, run SQLite migrations first, and only then bind/listen and report readiness. Treat a migration error as a failed startup with a non-zero exit (after closing any resources) so process supervision can surface it. Preserve a separate liveness/readiness distinction if the deployment environment needs it, and add transaction boundaries where individual migration semantics permit them.
Acceptance criteria
No HTTP listener is bound before
runMigrationsresolves; a rejection prevents readiness/listening and exits or rejects startup predictably; successful migration preserves the current route behavior; and startup logs clearly identify migration success/failure.Verification
Add server Vitest tests with mocked
runMigrationsandhttpServer.listenproving listen is deferred and rejected migration does not expose the server. Runnpm test --prefix server -- --runfor bootstrap/migration tests,npm run build --prefix server, and an integration smoke that starts with a pending migration then requests/api/healthonly after readiness.Dependencies and related work
This concerns the active SQLite runtime only and must not duplicate or alter the planned staged PostgreSQL migration in #120 and #149-#155.
Scope (Complexity, files, non-goals)
Medium; server bootstrap, focused tests, and possibly migration transaction helpers. Non-goals: replacing SQLite or changing the PostgreSQL rollout plan.