Skip to content

feat(db): apply pending migrations as a deploy step - #352

Merged
pratyush618 merged 3 commits into
mainfrom
feat/migrate-on-deploy
Sep 19, 2026
Merged

pratyush618 merged 3 commits into
mainfrom
feat/migrate-on-deploy

Conversation

@pratyush618

Copy link
Copy Markdown
Contributor

0007_academy_samples shipped in #350 and was never applied to the database the academy site connects to. Nothing noticed until somebody filed a sample request, at which point POST /api/orders answered 503 and the log said the database could not be reached — the database was fine, the table was missing.

CI's db:migrate only seeds the test database. Applying a migration to production was a manual step, and a manual step is one that eventually gets skipped. This makes the deploy do it.

How it works

packages/db/scripts/migrate.ts connects, checks what the journal has that the database does not, takes a session-level advisory lock, applies the rest, and exits non-zero if it could not. With nothing pending it is one connection and two queries, and it never takes the lock. Every outcome is a logged line naming the database and the migrations, because the failure this is meant to prevent was an invisible one.

It runs from the build script of the three apps that read Postgres, so a release and its schema arrive together. Nothing in it is Vercel-specific — any platform that runs a build command can run it, and DATABASE_MIGRATE=force makes it usable from a release step on one that does not build at all.

The decisions live in packages/db/src/deploy.ts as pure functions of the environment, which is the part that is worth testing:

  • Preview builds do not migrate. A preview runs the code of an unmerged branch, and plenty of projects give preview and production the same DATABASE_URL. Recognising the platform is a table with one row in it today; adding another platform is adding a row. A build on something unrecognised — CI, a container, a laptop with DATABASE_URL exported — is trusted to have chosen its own database.
  • A transaction pooler is moved to its session port. The advisory lock needs a connection that stays the same between statements, and Supabase's 6543 does not give one. This reuses the vendor rule already in client.ts rather than writing a second one. DATABASE_MIGRATE_URL covers anything else that pools by transaction.
  • No .env file is loaded. The environment is the whole contract. A stray local env file naming a different project is exactly how the migration went to the wrong database in the first place.

packages/db/migrations/** joins globalDependencies in turbo.json: a cached build is a build that did not run, so without it a release carrying a new migration can be served from a cache taken before that migration existed.

Verification

Against a throwaway Postgres 17:

  • empty database → applied all 8; rerun → up to date, exit 0
  • three concurrent runs → all exit 0, exactly one applied
  • unreachable database → exit 1, so the build fails rather than shipping
  • preview → skipped; production → runs; no DATABASE_URL → skipped, with the reason logged
  • a full pnpm build in apps/academy → migrated, built, academy.sample_requests present

51 tests pass in @byteveda/db (20 new), typecheck and Biome are clean. The Lighthouse workflow builds these apps without a DATABASE_URL and takes the skip path, so it is unaffected.

After merge

Production is still missing 0007 until the next production deploy of academy, admin or flexiq applies it. Preview environments need no configuration — they are skipped by default.

0007 shipped without ever being applied to the database the academy site connects to, so the table was missing until the first visitor asked for a sample. A migration that has to be remembered is one that will be forgotten.
The migrations folder joins globalDependencies because a cached build is a build that did not run, and would serve a release from before the migration existed.
@vercel

vercel Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
byteveda-site Ready Ready Preview Sep 19, 2026 4:07pm UTC
byteveda-site-academy-lycj Ready Ready Preview Sep 19, 2026 4:07pm UTC
byteveda-site-admin Ready Ready Preview Sep 19, 2026 4:07pm UTC
byteveda-site-flexiq Ready Ready Preview Sep 19, 2026 4:07pm UTC

@pratyush618
pratyush618 merged commit 5aa355c into main Sep 19, 2026
16 checks passed
@pratyush618
pratyush618 deleted the feat/migrate-on-deploy branch September 19, 2026 16:10

This branch was successfully deployed

4 active deployments
Preview – byteveda-site-flexiq — 7eb9e2ac Deployed Sep 19, 2026 by vercel[bot]
Preview – byteveda-site-admin — 7eb9e2ac Deployed Sep 19, 2026 by vercel[bot]
Preview – byteveda-site-academy-lycj — 7eb9e2ac Deployed Sep 19, 2026 by vercel[bot]
Preview – byteveda-site — 7eb9e2ac Deployed Sep 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants