Upgrading Payload
payload and every @payloadcms/* package are pinned to the same exact version in package.json. They must always move together: a mix of versions fails in ways that are hard to read.
The weekly upgrade #
Every Monday, .github/workflows/payload-upgrade.yml checks npm for a newer Payload in the current major. When there is one, it:
- pins every Payload package to it and installs;
- regenerates the types and the import map;
- writes a PostgreSQL migration if the new version changes the tables;
- opens a pull request (
fix(deps): upgrade Payload to …) with a link to the release notes, and starts CI on it.
CI then runs everything on both databases. Read the release notes and the table below, and merge when it is green. Nothing merges by itself.
- A new major (Payload 4, say) is not attempted: the workflow opens an issue instead, because a major needs its migration guide.
- When the upgrade can't be prepared, usually because a patch in
patches/no longer applies, it opens an issue with the install log. - Run it now from the Actions tab ("Payload upgrade", "Run workflow").
- It needs "Allow GitHub Actions to create and approve pull requests" in the repository's Actions settings, the same setting the release workflow needs.
New versions of the kit itself arrive the same way, as a weekly pull request of their own: see Updating Tolk Starter.
Dependabot leaves Payload to this workflow and keeps the other dependencies current; Next.js and React move together in a group of their own.
To upgrade by hand instead, follow the steps below.
Steps #
Update every Payload package to the same version. In
package.json, set the new version forpayloadand for every@payloadcms/*dependency, exactly (no^), then runpnpm install.Regenerate the generated files and commit what changes:
pnpm generate:types pnpm generate:importmapRun everything CI runs:
pnpm lint,pnpm typecheck,pnpm test:unit,pnpm test:int,pnpm build, then the end-to-end tests against the build (CI=true pnpm test:e2e, see Testing).On PostgreSQL, if the new version changes the schema, create a migration (
pnpm payload migrate:create) and commit it with the upgrade. Ifpnpm installstops at a patch inpatches/, see Fixes to dependencies.Read the release notes for breaking changes, especially to plugins the kit uses: form builder, nested docs, redirects and SEO.
Where the kit leans on Payload #
Most of the kit uses Payload's public API, which the tests cover. A few places go further; check them by hand when an upgrade touches them:
| Where | What it relies on |
|---|---|
src/app/(payload)/layout.tsx | Marked as generated, but edited on purpose: it registers the schedule-publish server function (src/access/schedulePublish.ts), so only people who may publish can schedule. If an upgrade regenerates the file, put the edit back. |
src/access/schedulePublish.ts | Imports Payload's own schedule handler from @payloadcms/ui/utilities/schedulePublishHandler. |
src/jobs/start.ts | Starts the jobs timer with getPayload({ config, cron: true }), reads payload.crons and calls payload._initializeCrons() after a failed start. |
src/jobs/index.ts | jobs.autoRun, jobs.shouldAutoRun and the payload-jobs-stats global. |
src/endpoints/translate/mcp/keys.ts | The name Payload gives the API-key login strategy (<users slug>-api-key) and its header format. |
src/access/roles.ts | Payload adding its own collections (preferences, locks, migrations, jobs) after the plugins run. |
scripts/seed-demo.ts and the other payload run scripts | How payload run passes arguments to a script (hence the -- in the seed:demo script). |
Behaviours the code works around are explained in CLAUDE.md and in comments where they are used: a Local API call with a hook's req and a locale switches req.locale for the rest of the save; sibling fields validate in parallel with beforeChange hooks; the edit view throws on a block type the config lacks; claiming a job is not atomic. The tests cover each of them, so a change in behaviour shows up as a failing test.