Author SHA1 Message Date
Samraaj BathandClaude Fable 5 1ad0f28f48 Run db:migrate automatically before every Railway deploy
The 2026-07-06 prod outage was a missed manual migration: the 0002
job_events migration never ran against prod, so the worker crash-looped
on every job. Make the deploy pipeline own it:

- railway.json sets `npm run db:migrate` as the pre-deploy command.
  Railway runs it with the service's env before starting the new
  deployment; a failed migration fails the deploy and the old version
  keeps serving.
- runMigrations() now serializes on a Postgres advisory lock so api and
  worker pre-deploys firing off the same push can't race Drizzle's
  journal writes.
- DEPLOY.md: document the automatic path; keep the manual command for
  first-time setup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 23:20:59 -07:00
3 changed files with 33 additions and 7 deletions
+11 -5
View File
@@ -12,14 +12,20 @@ Postgres and S3-compatible storage. The recommended OSS-friendly stack:
## 1. Database (Neon)
1. Create a Neon project; copy the pooled connection string.
2. Apply migrations (from CI/CD or locally):
2. Migrations run **automatically before every Railway deploy**: the repo's
`railway.json` sets `npm run db:migrate` as the pre-deploy command, which
Railway executes (with the service's env, so `DATABASE_URL` is present)
before starting the new deployment. Re-running is a no-op for anything
already applied, and concurrent runs (api + worker deploying off the same
push) serialize on a Postgres advisory lock. A failed migration fails the
deploy — the old version keeps serving.
To apply migrations manually (first-time setup, or outside Railway):
```bash
DATABASE_URL='postgres://...neon.../ditto_site?sslmode=require' npm run db:migrate
```
Run this on every deploy that changes `packages/db/migrations`. Drizzle tracks
applied migrations in the database, so re-running is a no-op for anything
already applied. (Alternatively, apply a single migration by piping its SQL
through `psql`/the Neon CLI — but prefer `db:migrate`, which also records the
(Alternatively, apply a single migration by piping its SQL through
`psql`/the Neon CLI — but prefer `db:migrate`, which also records the
journal entry so future runs stay consistent.)
Current migrations: `0000` core tables, `0001` signup tokens,
+15 -1
View File
@@ -6,12 +6,26 @@ import { createDb } from "./client.js";
const HERE = dirname(fileURLToPath(import.meta.url));
export const MIGRATIONS_DIR = join(HERE, "..", "migrations");
/** Apply all pending Drizzle migrations to the target database. */
/** App-wide advisory lock id for migrations. Concurrent `db:migrate` runs (e.g.
* api + worker pre-deploy firing off the same push) serialize on it instead of
* racing Drizzle's journal writes. */
const MIGRATE_LOCK_ID = 0xd1770;
/** Apply all pending Drizzle migrations to the target database. Safe to run
* concurrently: callers serialize on a Postgres advisory lock, and re-running
* applied migrations is a journal no-op. */
export async function runMigrations(connectionString: string, migrationsFolder = MIGRATIONS_DIR): Promise<void> {
const { db, pool } = createDb(connectionString);
const client = await pool.connect();
try {
await client.query("SELECT pg_advisory_lock($1)", [MIGRATE_LOCK_ID]);
try {
await migrate(db, { migrationsFolder });
} finally {
await client.query("SELECT pg_advisory_unlock($1)", [MIGRATE_LOCK_ID]);
}
} finally {
client.release();
await pool.end();
}
}
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://railway.com/railway.schema.json",
"deploy": {
"preDeployCommand": "npm run db:migrate"
}
}