Updated

3 min read

Payload CMS + PostgreSQL Migrations: Explained in 30 Seconds

While the Payload documentation is comprehensive regarding PostgreSQL migrations, there's a simpler mental model for handling migrations on both development and production.

Local (dev)

  • Make schema/config changes in your codebase (e.g. add a new field to a collection in Payload)
  • Drizzle pushes schema changes automatically (generates + runs SQL, which can be disabled optionally)
  • Run payload migrate:create to generate migration files only
  • Do NOT apply migrations locally (i.e. don't run payload migrate)

Production

  • Deploy code + migration files
  • Run payload migrate to apply migrations
  • Build and deploy your app
  • Done

That's it.

Why disable Drizzle push?

Drizzle push is cool in theory, but brittle in practice. You're likely to encounter "column does not exist" and other schema mismatch errors frequently.

Important

I recommend disabling Drizzle automatic schema push in your Payload config file, as you'll otherwise likely encounter schema mismatch errors frequently.

To disable Drizzle automatic schema push, you can set the push option to false in your Payload config file.

payload.config.ts
export default config({ // ... db: postgresAdapter( pool: { connectionString: process.env.DATABASE_URL, }, push: false // Disable Drizzle push ) });

The Payload documentation is a bit misleading on this point. Their recommendation to keep Drizzle push only holds if all of the following are true:

  • Schema changes are purely additive
  • No renames, no enum edits, no constraint rewrites
  • No partial failures
  • No long-lived local DB state
  • No need for reproducibility

Under those conditions, push can feel magical.

But importantly (and what the docs don't mention): Drizzle push is not a schema migration system.

It is:

  • a best-effort diff
  • non-transactional across complex changes
  • lossy in intent (rename vs drop+add)
  • brittle around enums, FKs, NOT NULL, defaults

Payload's docs frame the choice as:

"Push vs migrations"

But the real choice is:

"Sandbox convenience vs deterministic correctness"

I'd take the latter any day.

Wrapping up

Disabling Drizzle push changes the local development workflow slightly:

  • Make schema/config changes
  • Run payload migrate:create to generate migration files
  • NEW: Run payload migrate to apply migrations (Drizzle push is disabled)

And the production workflow remains the same:

  • Deploy code + migration files
  • Run payload migrate to apply migrations
  • Build and deploy your app
  • Done

This will make your local dev much easier, otherwise you'll likely find yourself dropping the database and starting over frequently.

Hope this helps!

Join my newsletter for lessons, experiments, and failures in bootstrapping online businesses.

Sign up if you're curious. I’ll only email you if it's actually good.

Ryan Chiang

Meet the Author

Ryan Chiang

Hello, I'm Ryan. I build things and write about them. This is my blog of my learnings, tutorials, and whatever else I feel like writing about.
What I'm currently building →.

2026

2025

2024

2023

© 2023-2026 Ryan Chiangryansc.io

Join my newsletter for lessons, experiments, and failures in bootstrapping online businesses.

Sign up if you're curious. I’ll only email you if it's actually good.