The database
The kit runs on MongoDB or PostgreSQL. DATABASE_URL chooses which, by its scheme:
DATABASE_URL starts with | Database |
|---|---|
mongodb:// or mongodb+srv:// | MongoDB, version 7 or later |
postgres:// or postgresql:// | PostgreSQL (CI tests version 17) |
DATABASE_URL=mongodb://127.0.0.1:27017/my-site
DATABASE_URL=postgres://user:[email protected]:5432/my-site
DATABASE_URL=postgresql://user:[email protected]:5432/my-site?sslmode=require
Anything else stops the server with an error that names the schemes it accepts.
- The same site on either. The code makes no database-specific calls: everything goes through Payload. Ids are strings on both (UUIDs on PostgreSQL), so
src/payload-types.tsis the same. CI runs every test on both. - Blocks need no migration. A layout's blocks are stored with their document, as JSON on PostgreSQL, so adding or changing a block changes no tables.
- Pick one before you go live. Content does not move from one database to the other by itself.
MongoDB #
Nothing to prepare: no schema and no migrations. Payload creates the collections and indexes it needs as it goes.
For development, docker compose up -d starts MongoDB 8 on 127.0.0.1:27017, which is where .env.example points.
PostgreSQL #
For development, docker compose up -d postgres starts PostgreSQL 17 on 127.0.0.1:5432, for:
DATABASE_URL=postgres://tolk:[email protected]:5432/tolk
Payload creates the database named in DATABASE_URL if it does not exist. The database user needs the right to create databases for that; otherwise create it yourself.
docker compose up -d --build app-postgres runs the production image on PostgreSQL, on http://localhost:3000.
Migrations #
PostgreSQL keeps the content model as tables, and the tables must match the config. Payload keeps them in step in two ways:
- In development (
pnpm dev,pnpm test:int), Payload changes the tables to match the config as it starts, so there are no migrations while you work. A change that would delete data asks first, in the terminal. - In production, the tables come from the migrations in
src/migrations. The kit ships the first one (…_initial).pnpm startand the Docker image run every migration not yet run when the server starts.pnpm payload migrateruns them by hand, andpnpm payload migrate:statuslists them.
After a change that changes the tables (a collection, a field, a select's options, a language, a module switch), write the next migration and commit it with the change:
pnpm payload migrate:create add-event-dates
It needs a postgres:// DATABASE_URL, but it does not connect. It writes src/migrations/<timestamp>_add-event-dates.ts (the SQL, with a down to undo it), a .json snapshot the next migration is compared with, and a line in src/migrations/index.ts.
- Read the migration before you commit it. A rename shows up as a drop and an add, which loses the column's data.
- CI checks it. CI fails when the config changes the tables and no migration does.
- Never run
pnpm devagainst the production database. Development marks a database whose tables it changed (a row with batch-1inpayload_migrations). Every production start then stops to ask whether to go on, which a server cannot answer. If it happened, check that the tables match the migrations, then delete that row. - Several containers starting at once run the same pending migration. One wins, and the others stop and restart. To avoid that, run
pnpm payload migrateonce before the deploy.
Switching a module off #
On PostgreSQL a module's collections are tables, so switching a module off is a migration, and the migration deletes the module's tables: its documents, their versions, and references to them from other documents' fields. Export what you want to keep first; on MongoDB the data would stay.
- Set the module to
falseinsrc/modules.config.ts. - Create the migration:
pnpm payload migrate:create news-off. - Commit it and deploy. It runs when the server starts.
Its blocks, and links and cards to its documents inside layouts, stay in the pages, as on MongoDB: left out when read, and removed when a page is saved. Switching the module back on is another migration, which creates its tables again, empty. See Modules.
Fixes to dependencies #
patches/ holds two small fixes that pnpm applies when it installs (pnpm.patchedDependencies in package.json):
drizzle-ormreads a table of more than 100 columns. PostgreSQL functions take at most 100 arguments, and the Microcopy global has a column for each of its texts.drizzle-kit: a migration that drops a table, such as switching a module off, no longer trips over the foreign keys the table took with it.
When a Payload upgrade brings other versions of these packages, pnpm install stops at the patch that no longer applies. Check whether the new version still needs the fix. Then make the patch again with pnpm patch <package>@<version>, or delete it from patches/ and package.json. See Upgrading Payload.
Backups #
Back up the database with its own tools (mongodump, pg_dump, or your provider's backups), and the uploads folder with it: the database names the files, the folder holds them.