Skip to content

0010. Give the schema one owner and every runtime an explicit lifecycle

  • Status: accepted; the parts on Graphile Worker’s migrations are superseded by 0019
  • Date: 2026-10-26

Several packages need tables: the domain (db), Better Auth (auth), the scheduler (jobs), Graphile Worker itself. Deployments need one migration step before web and worker start, under a privileged role, with ordinary runtimes that never hold migration credentials (analysis, chapters 8 and 12). The same composition runs as a Vercel function, a Next.js build, a resident worker, a migrator and an OpenAPI exporter, each with different prerequisites (structure review, findings R04 and R06).

Schema ownership

  • db is the single physical owner of GuestCraft’s SQL: domain tables, RLS policies, the table inventory, the Better Auth table definitions and the scheduler tables, in separate subpath exports (db/auth, db/scheduler, db/migrations, db/bootstrap). There is one migration journal.
  • Only auth and approved migration or test tooling import db/auth; only jobs and the composition import db/scheduler.
  • Graphile Worker keeps its own schema and migrations; they run through its pinned migration API, invoked by the same migration command.
  • Role bootstrap (creating gc_<instance>_app and gc_<instance>_worker) is a separate step from migrations.

One migration command. server/migrate runs, under an advisory lock and with the migration role, GuestCraft’s migrations and then Graphile’s. Vercel production builds and Docker’s one-shot migrate service call the same command. Runtimes check at startup that the schema revision is the expected one and stop with a clear error otherwise: they never migrate, and never fall back to an owner connection.

Runtime lifecycle

  • server has one entry point per runtime: server/http, server/worker, server/migrate and server/config, built from shared composition factories. There is no barrel that imports them all.
  • Importing a module has no side effects: no configuration parsing, connections, timers, signal handlers or migrations. Each runtime initializes explicitly; Next.js does it lazily at the first request, so that next build needs no runtime secret.
  • Next.js-specific code (next/server, after(), server-only) stays in apps/web; background execution is injected, so the worker never imports Next.js.
  • Each runtime owns its disposal: the worker finishes the current step and closes its pool on SIGTERM.
  • A test imports every shared entry point with an empty environment and asserts that no connection or timer was created.
  • Adding a table is always a change in db, reviewed by the owners of migrations and RLS.
  • The migrator is a separate artifact with its own credentials; the web and worker artifacts cannot run migrations even by mistake.
  • Each adapter owns its tables. Several migration journals, ordering problems at deploy time, and a likely db → auth → db cycle.
  • Migrations at application startup. Rejected in the analysis: concurrent instances, owner credentials at runtime, and no single point that can stop a bad deploy.