obsidian/wiki/payloadcms/migration-troubleshooting.md
2026-05-15 15:13:56 +01:00

10 KiB

tags topic sources created
payloadcms
tech-patterns
payloadcms
https://payloadcms.com/docs/troubleshooting/troubleshooting
https://payloadcms.com/docs/migration-guide/overview
https://payloadcms.com/docs/migration-guide/v3
https://payloadcms.com/docs/migration-guide/v4
https://payloadcms.com/docs/hierarchy/overview
https://payloadcms.com/docs/integrations/vercel-content-link
https://payloadcms.com/docs/examples/overview
2026-05-15

PayloadCMS — Migration, Troubleshooting, Hierarchy & Integrations

Overview

Payload follows semver. Major bumps (v2→v3, v3→v4) include breaking changes with published migration guides. v4 ships a @payloadcms/codemod CLI for most auto-migratable changes.

Migration Guide: v2 → v3

Core Shift

Admin Panel moved from React Router SPA to Next.js App Router (RSC). Express is removed; req/res are now Web API Request/Response.

Installation Steps

# Install new packages (all must share exact versions)
pnpm i next react react-dom payload @payloadcms/ui @payloadcms/next

# Remove deprecated packages
pnpm remove express nodemon @payloadcms/bundler-webpack @payloadcms/bundler-vite

Run DB migrations if on Postgres or MongoDB — see release tags v3.0.0-beta.39 (pg) and v3.0.0-beta.131 (mongo).

Key Breaking Changes (v2 → v3)

Area Change
Bundler Remove admin.bundler — Next.js handles bundling now
Secret Move secret from payload.init()payload.config.ts top-level
Env vars PAYLOAD_PUBLIC_*NEXT_PUBLIC_* for client-side env vars
Request req.headers['content-type']req.headers.get('content-type')
CSS Remove admin.css / admin.scss; use (payload)/custom.scss or a Custom Provider
Favicon admin.faviconadmin.icons: [{ path, sizes }]
OG Image admin.meta.ogImageadmin.meta.openGraph.images
Routes admin.logoutRoute / admin.inactivityRouteadmin.routes.logout / .inactivity
custom Top-level custom is now server-only; use admin.custom for client+server
afterError hook Now an array of functions; args expanded with req, res
Static dir upload.staticDir must be an absolute path
upload.staticURL Removed — use generateFileURL instead

Custom Components — Critical:

// Import paths moved from payload → @payloadcms/ui
- import { TextField, useField } from 'payload'
+ import { TextField, useField } from '@payloadcms/ui'

// Component definitions: direct imports → file paths
- Button: MyComponent
+ Button: '/src/components/Logout#MyComponent'

// All custom components are server-rendered by default
// Add 'use client' when you need state/hooks

Endpoints — New Response API:

- handler: (req, res) => { res.json({ ... }) }
+ handler: (req) => { return Response.json({ ... }) }
// params → req.routeParams; body → req.data (must call addDataAndFileToRequest)

React Hooks:

- useTitle() from 'payload'
+ const { title } = useDocumentInfo()  // from '@payloadcms/ui'

- useConfig() → SanitizedConfig
+ const { config } = useConfig()       // now ClientConfig

- useTranslation('general')
+ useTranslation()  // use full keys: t('general:cancel')

Plugins — all now named exports with Plugin suffix:

- import seo from '@payloadcms/plugin-seo'
+ import { seoPlugin } from '@payloadcms/plugin-seo'

Cloud Storage — standalone packages replace sub-path adapters:

- import { s3Adapter } from '@payloadcms/plugin-cloud-storage/s3'
+ import { s3Storage } from '@payloadcms/storage-s3'

Reserved field names — avoid: file, _id (mongo), salt, hash, password, email, filename, url, sizes, _order, _path, _locale.


Migration Guide: v3 → v4

Codemod (run first)

npx @payloadcms/codemod

Idempotent — safe to run on partially-migrated projects.

Key Breaking Changes (v3 → v4)

List View Select API — now always-on; remove admin.enableListViewSelectAPI: true.

Globals admin.components shape:

admin: {
  components: {
-   elements: {
-     SaveButton: '/path/to/CustomSaveButton',
-     Description: '/path/to/CustomDescription',
-   },
+   Description: '/path/to/CustomDescription',
+   edit: {
+     SaveButton: '/path/to/CustomSaveButton',
+   },
  },
}

forceSelectselect function:

- forceSelect: { title: true, slug: true }
+ select: ({ select }) => (select ? { ...select, title: true, slug: true } : undefined)

admin.hideAPIURL removed — use view tab condition:

admin.components.views.edit.api.tab.condition = () => false

Import sources consolidated (@payloadcms/ui re-exports removed):

- import { Column, ListPreferences } from '@payloadcms/ui'
+ import type { Column, CollectionPreferences } from 'payload'
- import { headersWithCors } from '@payloadcms/next/utilities'
+ import { headersWithCors } from 'payload'

Document title split out:

- const { title } = useDocumentInfo()
+ const { title } = useDocumentTitle()  // from '@payloadcms/ui'

Troubleshooting Common Errors

TypeError: Cannot destructure property 'config' of...

Cause: Duplicate or mismatched versions of payload / @payloadcms/* / react / react-dom.

Diagnose:

pnpm why @payloadcms/ui
# or manually:
find node_modules -name package.json -exec grep -H '"name": "@payloadcms/ui"' {} \;

Fix sequence:

  1. Pin exact versions (remove ^ and ~) for payload, @payloadcms/*, react, react-dom
  2. Delete node_modules/
  3. pnpm install
  4. If persists: pnpm store prune + delete lockfile + pnpm install
  5. pnpm dedupe

Deep imports rule: Inside the Payload Admin Panel, only import from:

  • @payloadcms/ui
  • @payloadcms/ui/rsc
  • @payloadcms/ui/shared

Never @payloadcms/ui/elements/Button (use that only in your own frontend).

useUploadHandlers must be used within UploadHandlersProvider

Cause: Monorepo with mismatched next or react versions across packages.

Fix: Install payload, @payloadcms/*, next, react, react-dom at monorepo root to prevent hoisting duplicates.

Unauthorized, you must be logged in after login

Auth cookie set but rejected. Check:

  • CORS: avoid '*'; explicitly whitelist your domain
  • CSRF: whitelist your domain if configured
  • Cookie settings: no misconfigured domain

Inspect: DevTools → Network → login request → check for Set-Cookie header and any browser ⚠️ icon.

Database connection error: Cannot read properties of undefined (reading 'searchParams')

Special characters in DB password must be URI-encoded:

const encodedPassword = encodeURIComponent(process.env.DB_PASSWORD)
// Use encodedPassword in your connection string

HMR WebSocket fails with --experimental-https

# .env
USE_HTTPS=true
# or for fully custom URL:
PAYLOAD_HMR_URL_OVERRIDE=wss://localhost:3000/_next/webpack-hmr

Hierarchy Feature

Adds automatic parent-child tree management to any collection.

export const Pages: CollectionConfig = {
  slug: 'pages',
  fields: [{ name: 'title', type: 'text', required: true }],
  hierarchy: {
    parentFieldName: 'parent',  // auto-created if absent
  },
}

Auto-generates virtual fields (not stored in DB):

  • _h_slugPath — URL-safe breadcrumb path
  • _h_titlePath — human-readable breadcrumb

Opt-in path computation:

// Local API
payload.findByID({ collection: 'pages', id, context: { computeHierarchyPaths: true } })

// REST API
GET /api/pages/abc123?computeHierarchyPaths=true

// Or via select
payload.findByID({ collection: 'pages', id, select: { _h_slugPath: true } })

Performance: Each document requires one extra query for ancestors; ancestors are request-scoped cached. Only enable when displaying breadcrumbs/URLs.

Polymorphic hierarchy (posts under pages):

hierarchy: {
  parentFieldName: 'parent',
  relationTo: ['pages', 'posts'],
}

Limitations:

  • locale: 'all' skips hierarchy processing — update locales individually
  • Parent field is not localized — tree structure is shared across all locales
  • Circular references throw an error automatically

Enterprise-only. Embeds Content Source Maps into API responses via @payloadcms/plugin-csm.

npm i @payloadcms/plugin-csm
import contentSourceMaps from '@payloadcms/plugin-csm'

buildConfig({
  plugins: [contentSourceMaps({ collections: ['pages'] })],
})

Enable encoding only in draft/preview (performance):

// REST
fetch(`/api/pages?encodeSourceMaps=true&where[slug][equals]=${slug}`)

// Local API
payload.find({ collection: 'pages', context: { encodeSourceMaps: true } })

Troubleshooting:

  • date fields not encoded by default — use vercelStegaSplit(text) to separate encoded data
  • blocks / array fields get _encodedSourceMap property; use data-vercel-edit-target attribute on wrapper

Examples — Quickstart Templates

npx create-payload-app --example <name>

Available examples: auth, custom-components, draft-preview, email, form-builder, live-preview, multi-tenant, tailwind-shadcn-ui, whitelabel

Full list: github.com/payloadcms/payload/tree/main/examples


Gotchas

  • Yarn 1.x is not supported — use pnpm (recommended), npm, or Yarn >=2.
  • v3 removes express entirely — any req.params access must become req.routeParams.
  • admin.components.views keys are now camelCase: Editedit, Defaultdefault.
  • Fields type renamed to FormState in v3.
  • BlockFieldBlocksField in v3.
  • In monorepos, install shared dependencies at root to avoid multiple instances.