- PayloadCMS in Production? Turn Off Push and Go Migration‑Only (Zero‑Downtime Guide)
PayloadCMS in Production? Turn Off Push and Go Migration‑Only (Zero‑Downtime Guide)
From dev-mode push to production-grade migrations in PayloadCMS + Postgres—safely, step by step.

There’s a moment every PayloadCMS project hits in production: things are running smoothly, then the next feature lands and you realize you’re still on dev-mode “push.” I’ve been there. That’s the point where shipping changes starts to feel risky, reviews are harder, and rollbacks are unclear.
You’ll find a friendly, professional walkthrough with guardrails: back up first, switch push off safely, record a no-op baseline so history matches reality, and adopt a simple workflow your whole team can follow. It’s written so a junior can execute step by step, while seniors can skim the
TL;DR and jump straight to CI. The goal is confidence: you’ll know what’s changing, when it changes, and how to recover if anything goes sideways.
Who this is for
This guide is for teams running PayloadCMS 3.x on Postgres in production who still have database push (dev mode) enabled and now need to switch to a migration-only workflow. You’ll make this change to gain control and auditability over schema changes, ship safer deployments, and eliminate the risk of uncontrolled schema drift in production.
Assumptions & scope
To keep this practical and accurate, the steps assume Payload 3.x (validated with payload@3.49.x
and @payloadcms/db-postgres@3.49.x
), Postgres via @payloadcms/db-postgres
, and Drizzle migrations managed by Payload. Commands use pnpm
and tsx
, but you can adapt them to npm/node. This guide intentionally excludes Mongo/SQLite and other adapters, and it stays provider‑agnostic so you can apply it with any Postgres host.
TL;DR — Transition checklist
If you just need the steps, follow this sequence end‑to‑end. The sections below explain each step in more detail and include context and verification guidance.
- Commit a clean working tree
- Create a Postgres backup (do not skip)
- Ensure
push: false
andmigrationDir
inpostgresAdapter
- Restart your app/dev server to apply config
- Generate a baseline migration
- Convert the baseline "up" to a safe no-op (recommended: script)
- Run migrations (records baseline without schema changes)
- Verify success and that push is truly off
- Adopt the schema → data → constraints workflow
- Configure multi-env and run order (local/staging → production)
- Add CI step: migrate → build → start; fail build on migration errors
- Keep a rollback plan with tested backups
Safety first: backups, commits, staging
Start from a fully recoverable state. Commit your work, ensure the pipeline is green, take a fresh Postgres backup, and—if possible—rehearse the transition on staging before touching production.
Before you change anything:
- Commit all changes and ensure CI is green.
- Take a database backup and store it securely (not in git).
- If you have staging, perform all steps there first.
Command (use environment variables and placeholders; do not paste secrets):
# Ensure the backups directory exists
mkdir -p backups
# Use your Postgres URL from environment variables
# Example: export DATABASE_URL="postgres://user:pass@host/db?sslmode=require"
pg_dump "$DATABASE_URL" > "backups/backup_$(date +%Y%m%d_%H%M%S).sql"
Core concepts:
pg_dump
creates a logical snapshot of your DB. It is fast and safe to run online.- Keep backups encrypted and accessible to the recovery team. Periodically test restore.
Turn off push mode
Next, disable automatic schema push and explicitly set your migrations directory in the Postgres adapter. This prevents uncontrolled schema changes and ensures only migrations can modify the database.
Edit payload.config.ts
to include:
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URL,
},
push: false,
migrationDir: './src/migrations',
}),
Core concepts:
- Push mode auto-applies schema changes from code at startup and is great for local prototyping, not production.
- With
push: false
, schema changes only happen when you run migrations. - You must restart the server/dev process after changing adapter settings; the old config is cached in the running process.
Verification tip:
- After restart, introduce a harmless schema change locally and confirm it does not apply automatically. Revert the change.
Generate a baseline migration
Now create a migration that captures your current production schema state. This gives you a clean starting point for all future, controlled changes without altering any existing data today.
Generate the migration:
pnpm payload migrate:create
Core concepts:
- The baseline will try to create objects that already exist (because push mode previously created them). Running it as-is will fail with "already exists" errors.
- We will convert this baseline to a safe no-op so it only records a checkpoint in migration history.
Make the baseline safe (no‑op)
Convert the baseline’s "up" step to a no‑op. This sidesteps "already exists" conflicts (types, tables, indexes) while recording a correct checkpoint in migration history.
Recommended automation (provided in this repo):
# Preview (dry-run) the conversion
pnpm fix-baseline src/migrations/<your_baseline_migration>.ts --dry-run
# Apply the conversion (creates a .backup of the original file)
pnpm fix-baseline src/migrations/<your_baseline_migration>.ts
Core concepts:
- The no-op baseline writes a record to the migration tracking table without attempting to recreate existing objects (enums, tables, indexes).
- A backup of the original file lets you review or revert the change if needed.
- Avoid trying to wrap every CREATE statement with IF NOT EXISTS—especially enums—this is brittle and unnecessary for the baseline case.
Manual alternative (concept-only):
- Keep the migration file structure intact but replace the SQL in
up
with a comment/no-op and keepdown
inert.
Apply the baseline
Run migrations to record the baseline checkpoint. This aligns the database with the migration history without changing the schema.
Run:
pnpm payload migrate
Core concepts:
- You should see the baseline migration marked as migrated. No schema changes should occur, given it's a no-op.
- The migration tracking table (managed by Drizzle/Payload) will include an entry for this baseline.
Verification:
- Check the CLI output for a successful migration entry.
- Sanity-check critical app flows; nothing should break or change.
New workflow going forward
From here on, adopt a safe, repeatable pattern that minimizes production risk and makes reviews and rollbacks straightforward.
Pattern:
- Schema migration (additive, nullable)
- Data migration (backfill/transform)
- Constraint migration (make non-null, add FKs, enforce uniqueness)
Core concepts:
- Keep migrations small and focused; one concern per migration.
- Never edit a committed migration; create a new one to change direction.
- Review migration files in PRs like application code.
Multi‑environment setup
Use separate environment files and a consistent order of operations. This protects production and allows you to roll out safely through staging first.
Recommendations:
- Use
.env.local
for development, staged env files for staging/production (managed securely in CI). - Run order: local → staging → production.
- Prefer
push: false
in all environments for consistency. If you keep push enabled in dev temporarily, expect drift and conflicts.
Core concepts:
- The
DATABASE_URL
controls which database is targeted by migration commands. - Ensure CI uses the correct environment variables without hardcoding secrets.
CI/CD integration
Add an explicit migration step to your pipeline so deploys fail fast when a migration cannot apply, rather than shipping incompatible code.
Sequence (provider-agnostic):
- Set environment variables (including
DATABASE_URL
) - Run migrations:
pnpm payload migrate
- Build the application:
pnpm build
- Start the application
Core concepts:
- Treat a migration failure as a deployment failure; do not continue.
- Keep secrets in your CI secret store; never commit them.
Verification and observability
Confirm the transition worked and set up simple checks to spot drift early. This builds confidence and shortens triage when something goes wrong.
Checklist:
- Migration command logs show the baseline was applied once and future migrations apply in order.
- Schema changes never apply automatically—if they do, push mode is not fully disabled or the process wasn't restarted.
- Optional: Query your DB to see the migration tracking table entry for the baseline (read-only).
- Smoke test critical flows after every migration.
Troubleshooting matrix
Here are common errors mapped to quick fixes to reduce time‑to‑recovery.
- "type/index/table already exists": Baseline isn't a no-op. Re-run the baseline conversion, then re-apply.
- "push still applying changes":
push: false
not set or server not restarted. Fix config and restart. - "migration not found" or not applied: Wrong
migrationDir
or file naming. Verify config and paths. - CI migration step fails: Wrong env vars or missing DB privileges. Fix secrets/permissions and re-run.
Rollback playbook
Have a minimal, practical plan to undo the last change. Favor forward fixes, but know when to restore from a tested backup for safety and speed.
Options:
- Prefer forward fixes: create a new migration that restores the desired schema/data.
- If data corruption/loss risk exists, restore from the most recent tested backup.
Core concepts:
- Test restore procedures periodically so you're confident under pressure.
- Avoid rewriting migration history; add new migrations instead. Only restore from backup if necessary.
Appendix: Manual baseline conversion
If you can’t use the script, you can still create a safe baseline without tooling.
Concept-only steps:
- Make a backup copy of the baseline file.
- In the baseline's
up
, replace the SQL body with a harmless comment/string soawait db.execute(...)
does nothing. - Keep the
down
inert as well. - Save, then run
pnpm payload migrate
to record the baseline.
Core concepts:
- The aim is to align migration history without changing existing objects created during push mode.
- Avoid complex PL/pgSQL blocks or DO blocks for existence checks in this baseline operation; they add risk without benefit here.
Conclusion
You’ve moved your production PayloadCMS project from dev‑mode push to a clean, migration‑only workflow. Along the way, you took a safe backup, turned off push to stop uncontrolled schema changes, generated a baseline and converted it to a no‑op so history matches reality, and applied it without touching existing data. You also set up a practical development rhythm—schema, then data, then constraints—added environment discipline, wired migrations into CI so deploys fail fast, and established simple verification, troubleshooting, and rollback habits. From here, every change is deliberate, reviewable, and recoverable.
Thanks, Matija