Upgrading
How to move a KDIGI CMS install to a newer version, how database migrations work, and how to write one if you extend the schema.
How schema changes work
Section titled “How schema changes work”- The database schema is never synchronised automatically (
push: falseinsrc/payload.config.ts). Every schema change ships as a timestamped migration insrc/migrations/, registered insrc/migrations/index.ts. - Version 1.0.0 starts from a single baseline migration (
20261103_042515_initial) that creates the whole schema on an empty database. Later releases add new files after it; you never edit applied migrations. - Applied migrations are recorded in the
payload_migrationstable, so running the command again is a no-op when nothing is pending. - Versions follow SemVer. The CHANGELOG lists schema changes, new environment variables and breaking API changes for each release.
Upgrade procedure
Section titled “Upgrade procedure”- Read the CHANGELOG for every version between yours and the target. Look for "Breaking", new required variables and anything that touches REST API responses your frontends rely on.
- Back up the database and the media volume (deploy → backups). Do not skip this: rolling a schema back is done by restoring the backup.
- Get the new code:
git pull(orgit fetch --tags && git checkout vX.Y.Z) and rebuild.- Docker Compose:
docker compose up -d --build— the entrypoint runspayload migratebefore starting the server. - Coolify: redeploy.
- Without Docker:
pnpm install --frozen-lockfile && pnpm build && pnpm migrate && pnpm start.
- Docker Compose:
- Verify: log in to
/admin, open a few documents in each collection, upload an image, and request a public URL from your frontend.GET /api/healthonly proves the process is up, not that the schema is current. - Update
.env/container variables if the changelog added any (configuration).
Running migrations yourself
Section titled “Running migrations yourself”| Where | Command |
|---|---|
| Local checkout | pnpm migrate (equivalent to pnpm payload migrate) |
| Status | pnpm payload migrate:status |
| Running container | docker compose exec app node_modules/.bin/payload migrate |
| As a separate release step | set SKIP_MIGRATIONS=true on the app container and run the command above before routing traffic to the new version |
Migrations run with the database connection of DATABASE_URI; make sure the CLI can reach it from where you run it (inside the Compose network use host db).
Zero-downtime notes
Section titled “Zero-downtime notes”Migrations that only add tables, columns or enum values are safe to apply while the previous version still serves traffic; the changelog flags the ones that are not. Because the repository does not orchestrate rolling deploys, plan a short maintenance window for upgrades that the changelog marks as breaking, and upgrade the CMS before the frontends that consume new fields.
Rolling back
Section titled “Rolling back”Redeploy the previous image only if it can read the migrated schema (additive migrations usually allow it). Otherwise restore the database backup taken in step 2 — you lose content written after the backup. Avoid pnpm migrate:down on a production database: it reverses the latest batch, which may contain more than the one migration you have in mind. Use it on scratch databases to test down() functions.
Moving between environments
Section titled “Moving between environments”- Upgrading Node.js/pnpm: the image uses Node 22;
package.jsonrequires Node ≥ 20.9 and pinspackageManager. Usecorepack enableto get the right pnpm. - Changing
PAYLOAD_SECRETlogs everyone out and invalidates API keys; it does not affect stored content. - Changing
PAYLOAD_PUBLIC_SERVER_URLchanges the admin origin and the prefix of local media URLs; update frontends that cache absolute media URLs. - Local disk → R2: see media and storage (manual procedure; no built-in command).
Writing a migration (forks and contributors)
Section titled “Writing a migration (forks and contributors)”- Change collections/fields in
src/. pnpm payload migrate:create <short-name>generates a migration plus a schema snapshot (.json). It may ask interactive questions about renames; read the generated SQL and delete anything unrelated to your change.- Make it safe:
CREATE TYPEis not idempotent — guard it (DO $$ … EXCEPTION WHEN duplicate_object …) or check the catalog; useIF NOT EXISTS/IF EXISTSfor tables, columns and indexes; adding enum labels usesALTER TYPE … ADD VALUE IF NOT EXISTS. - Provide a working
down(). Test both directions on a scratch database:pnpm migrate,pnpm migrate:down,pnpm migrate. - New collection? Make sure the migration also adds its foreign-key column to
payload_locked_documents_rels, or every admin page fails with a 500. Generated migrations include it; hand-written ones often forget. - Adding a new
Sites.themevalue, or any otherselectoption, needs a migration that adds the enum label, otherwise saving a document with the new value fails. - A new background-job task slug needs its label added to
enum_payload_jobs_task_slugandenum_payload_jobs_log_task_slug. - Run
pnpm generate:types(updatessrc/payload-types.ts) and, for new admin components,pnpm generate:importmap. - Run
pnpm payload migrateagainst an empty database and against a copy of real data before releasing. CI also applies the migration chain to an empty PostgreSQL and runs the integration tests.
More on conventions: CONTRIBUTING, architecture.