---
tags: [payloadcms, tech-patterns]
topic: payloadcms
sources:
  - 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
created: 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

```bash
# 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.favicon` → `admin.icons: [{ path, sizes }]` |
| OG Image | `admin.meta.ogImage` → `admin.meta.openGraph.images` |
| Routes | `admin.logoutRoute` / `admin.inactivityRoute` → `admin.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:**

```diff
// 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:**

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

**React Hooks:**

```diff
- 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:

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

**Cloud Storage** — standalone packages replace sub-path adapters:

```diff
- 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)

```bash
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:**

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

**`forceSelect` → `select` function:**

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

**`admin.hideAPIURL`** removed — use view tab condition:

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

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

```diff
- 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:**

```diff
- 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:**

```bash
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:

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

### HMR WebSocket fails with `--experimental-https`

```bash
# .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.

```ts
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:**

```ts
// 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):

```ts
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

---

## Vercel Content Link Integration

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

```bash
npm i @payloadcms/plugin-csm
```

```ts
import contentSourceMaps from '@payloadcms/plugin-csm'

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

Enable encoding only in draft/preview (performance):

```ts
// 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

```bash
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](https://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: `Edit` → `edit`, `Default` → `default`.
- `Fields` type renamed to `FormState` in v3.
- `BlockField` → `BlocksField` in v3.
- In monorepos, install shared dependencies at root to avoid multiple instances.

## Related

- [[wiki/payloadcms/getting-started|Getting Started]]
- [[wiki/payloadcms/configuration|Configuration]]
- [[wiki/payloadcms/admin-panel-overview|Admin Panel]]
- [[wiki/payloadcms/local-api|Local API]]
