The build pipeline
Every build migrates, ensures the administrator, seeds an empty dataset and checks the sign-up posture — before the frontend is compiled, and idempotently.
The build command is two halves, written out rather than hidden in a lifecycle hook, because a reader has to be able to see both:
node scripts/bootstrap-deployment.mjs && next buildWhat the first half does
- Migrations, under a Postgres advisory lock so two builds cannot interleave.
- The administrator, created and confirmed exactly once, its password never rewritten.
- The demo content, only into a dataset with no content documents, always with
createIfNotExistson fixed ids. - A sign-up posture assertion, reported loudly in the log.
With no database environment at all — a plain CI run — it prints a multi-line SKIPPED notice and exits successfully. It never degrades a deployed build to a skip, and there is no environment variable that turns it off. A deployment that silently shipped an unmigrated database would be the worst possible failure mode.
Idempotency, in two independent layers
- The gate. The content seed runs only against a dataset with no content documents.
- The mutation. Fixed ids plus
createIfNotExists, so an existing document is returned untouched even when the gate opens.
The second is not redundancy. The gate is a read followed by writes in separate transactions, so two concurrent builds can both pass it; fixed ids make that race converge on one copy rather than two.
Keeping content in step with a repository
The default is to never overwrite an author's edit, which is right for a product. For a site whose content is source-controlled — this one — there is an explicit, operator-only mode that writes with createOrReplace and bypasses the gate:
npm run seed:demo:replaceIt is never part of a build. Running it means deciding that the repository is the source of truth for that dataset, which is a decision a build should not make on anyone's behalf.
Crons
A daily keep-alive query stops a free-tier database from pausing after a week of inactivity, and an orphan sweep reclaims asset bytes whose rows were deleted.
