Local dev and the test suite previously defaulted to the live production Neon database, and running the tests mutated prod data. Add guardrails plus a proper local Postgres + migrations workflow so production can't be hit by mistake: - docker-compose: local Postgres 17 (matches prod) + auto-created teqo_test - dev guard refuses a non-local DATABASE_URL; tests refuse non-_test databases - .env.test isolates the suite; vitest/playwright load it and fail closed - Payload migrations set up (push disabled); build runs `payload migrate`, so deploys apply pending migrations to prod automatically - baseline `initial` migration + idempotent consent.text varchar->jsonb fix - `pnpm db:pull` copies prod content locally, excluding supporter PII - docs (README, AGENTS.md) + agent skills for the local-db/migration workflow
3.4 KiB
| name | description |
|---|---|
| payload-migrations | Create, apply, and deploy Payload schema migrations for the Teqo project (Postgres, push disabled). Use when changing any collection/global/field config, adding indexes or enums, reconciling schema drift, writing a data migration, or when the user mentions migrations, migrate:create, or applying schema changes to production. Explains how migrations reach the production Neon database on deploy. |
Teqo Payload Migrations
Schema is managed exclusively by committed migrations in src/migrations/. push: false is set in payload.config.ts for every environment — never enable it against a remote database.
How it deploys
pnpm build runs payload migrate before next build. On Vercel, the build runs with the production DATABASE_URL, so every deploy applies pending migrations to production automatically. Do not run pnpm migrate against production by hand.
Production is already baselined at migration 20260715_163458_initial (marked applied in prod's payload_migrations). Never regenerate, rename, or replace the initial migration — doing so would make prod try to recreate existing tables.
Making a schema change
- Make sure the local DB is running and current:
pnpm db:start,DATABASE_URLlocal,pnpm migrate. - Edit the collection / global / field config.
- Generate the migration:
pnpm migrate:create <name> - Review the generated SQL in
src/migrations/<timestamp>_<name>.ts. Confirm it does only what you intend (watch for unintended drops/renames). - Commit all generated files: the
.ts, the.jsonsnapshot, and the updatedsrc/migrations/index.ts. - Apply locally and verify:
pnpm migrate(inspect withpnpm migrate:status). - If types or the import map are affected:
pnpm generate:types/pnpm generate:importmap. - Deploying (merge/deploy) applies it to prod via the build step.
Hand-written data / reconciliation migrations
For changes Payload's diff can't generate (data backfills, type reconciliations), add a file manually and register it in index.ts. Make it idempotent so it's safe on databases already in the target state.
Follow the existing example src/migrations/20260715_163500_consent_text_to_jsonb.ts:
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
DO $$
BEGIN
IF EXISTS ( /* guard: only run when not already in target state */ ) THEN
/* ALTER / UPDATE ... */
END IF;
END $$;
`)
}
export async function down({ db }: MigrateDownArgs): Promise<void> {
/* reverse, also guarded */
}
Then add both up/down and a name entry to src/migrations/index.ts (order matters — after the previous migration).
Verifying against production (read-only, safe)
To confirm what a deploy will apply, run status pointed at prod's unpooled URL — this only reads:
DATABASE_URL="<unpooled Neon URL>" pnpm migrate:status
Never do
- Set
push: true, especially against a remote DB. - Run
pnpm migratemanually against production (deploys handle it). - Edit production schema by hand — make it a migration instead.
- Modify or delete the baseline
20260715_163458_initialmigration.
Related
- Local database setup,
db:pull, and test DB: use thelocal-databaseskill.