The Situation
A development team had inherited a Payload CMS project backed by PostgreSQL. The codebase looked like it was running migrations — there was a migration folder, committed files, and a migration chain in index.ts. But the project had a mixed history: it had been started in Payload's dev mode, which pushes schema changes automatically, and later switched to committed migrations without reconciling the gap.
The result was a setup nobody could fully trust. One already-applied migration was empty in the latest commit. push: false wasn't actually set in the Payload config, meaning future automatic schema push was still possible. The historical database dump didn't align with the current schema. Running the migration chain against a fresh environment could silently produce the wrong schema, or fail outright.
A second concern was the data importer. A single edition import produced hundreds of writes — media, articles, PDFs, preview images, pages, relation records — all routed through the Payload local API one record at a time. Image resizes were being triggered through the media collection during import, adding Sharp processing overhead. Under heavier loads, the runtime was returning 504s.
Promoting to staging or production was blocked. The engagement was scoped to a structured technical review of both problems, a local repair, and a written operating model for going forward.
What the Audit Found
The root cause of the migration instability was straightforward: the database had been created through Payload's automatic schema push behavior and the project later switched to committed migrations without reconciling the gap. The existing migration chain couldn't be applied cleanly because the database's actual schema reflected the older push-era state.
Specific findings:
push: falsewas missing frompayload.config.ts— automatic schema push was still active20260318_124327.ts, an already-applied migration, was empty in the latest commit — a classic source of migration history instability- Schema drift of 20 normalized statements between the restored historical dump and the current code-expected schema, across enum naming, column names, and table structure
- The import write path paid full Payload local API overhead — validation, hooks, access control, lifecycle processing — on every individual record, making high-volume ingestion slow by design
- Image resize processing during import was compounding the runtime load, contributing to instability under heavier imports
The migration problem and the import problem were independent in cause but compounding in effect: a team that can't trust its schema baseline and can't run imports reliably has no safe path forward.
The Outcome
Local repair was completed and verified within 3 working days.
Migration recovery:
push: falseenforced inpayload.config.ts- Committed migration chain restored and validated
- Bootstrap SQL written to align the legacy database lineage with the correct historical baseline
- Forward delta migration written and applied — covering enum renames, column renames, and structural corrections across locales, interstitials, editions, and showcases
- All three migrations confirmed applied in correct batch order via
npx payload migrate:status - Application startup verified against the migrated database
- Corrected baseline dump created as the preferred starting point for all future environments
Delivered alongside the repair: a written strategy report covering the recommended production-safe migration workflow, a two-path baseline policy for disposable and persistent environments, a deployment model that moves migrations out of application startup into an explicit CI step, and a concrete import architecture direction — replacing sequential local API writes with transactional batch writes via payload.db / Drizzle ORM, with Payload's job queue for visibility, retry behavior, and worker isolation.
The team had a working migration chain, a clean baseline artifact, and a clear operating model. The path to staging and production was no longer blocked.