---
title: 2.0 to 3.0 Migration Guide
label: 2.0 to 3.0 Migration Guide
order: 10
desc: Upgrade guide for Payload 2.x projects migrating to 3.0.
keywords: local api, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react
---
# Payload 2.0 to 3.0 Migration Guide
Payload 3.0 completely replatforms the Admin Panel from a React Router single-page application onto the Next.js App Router with full support for React Server Components. This change completely separates Payload "core" from its rendering and HTTP layers, making it truly Node-safe and portable.
If you are upgrading from a _previous beta_, please see the [Upgrade From Previous Beta](#upgrade-from-previous-beta) section.
## What has changed?
The core logic and principles of Payload remain the same from 2.0 to 3.0, with the majority of changes affecting specifically the HTTP layer and the Admin Panel, which is now built upon Next.js. With this change, your entire application can be served within a single repo, with Payload endpoints are now opened within your own Next.js application, directly alongside your frontend. Payload is still headless, you will still be able to leverage it completely headlessly just as you do now with Sveltekit, etc. All Payload APIs remain exactly the same (with a few new features), and the Payload Config is generally the same, with the breaking changes detailed below.
### Table of Contents
All breaking changes are listed below. If you encounter changes that are not explicitly listed here, please consider contributing to this documentation by submitting a PR.
- [Installation](#installation)
- [Breaking Changes](#breaking-changes)
- [Custom Components](#custom-components)
- [Endpoints](#endpoints)
- [React Hooks](#react-hooks)
- [Types](#types)
- [Email Adapters](#email-adapters)
- [Plugins](#plugins)
- [Upgrade From Previous Beta](#upgrade-from-previous-beta)
## Installation
Payload 3.0 requires a set of auto-generated files that you will need to bring into your existing project. The easiest way of acquiring these is by initializing a new project via `create-payload-app`, then replace the provided Payload Config with your own.
```bash
npx create-payload-app
```
For more details, see the [Documentation](https://payloadcms.com/docs/getting-started/installation).
1. **Install new dependencies of Payload, Next.js and React**:
Refer to the package.json file made in the create-payload-app, including peerDependencies, devDependencies, and dependencies. The core package and plugins require all versions to be synced. Previously, on 2.x it was possible to be running the latest version of payload 2.x with an older version of db-mongodb for example. This is no longer the case.
```bash
pnpm i next react react-dom payload @payloadcms/ui @payloadcms/next
```
Also install the other @payloadcms packages specific to the plugins and adapters you are using. Depending on your project, these may include:
- @payloadcms/db-mongodb
- @payloadcms/db-postgres
- @payloadcms/richtext-slate
- @payloadcms/richtext-lexical
- @payloadcms/plugin-form-builder
- @payloadcms/plugin-nested-docs
- @payloadcms/plugin-redirects
- @payloadcms/plugin-relationship
- @payloadcms/plugin-search
- @payloadcms/plugin-sentry
- @payloadcms/plugin-seo
- @payloadcms/plugin-stripe
- @payloadcms/plugin-cloud-storage - Read [More](#@payloadcms/plugin-cloud-storage).
1. Uninstall deprecated packages:
```bash
pnpm remove express nodemon @payloadcms/bundler-webpack @payloadcms/bundler-vite
```
1. Database Adapter Migrations
_If you have existing data_ and are using the MongoDB or Postgres adapters, you will need to run the database migrations to ensure your database schema is up-to-date.
- [postgres](https://github.com/payloadcms/payload/releases/tag/v3.0.0-beta.39)
- [mongodb](https://github.com/payloadcms/payload/releases/tag/v3.0.0-beta.131)
1. For Payload Cloud users, the plugin has changed.
Uninstall the old package:
```bash
pnpm remove @payloadcms/plugin-cloud
```
Install the new package:
```bash
pnpm i @payloadcms/payload-cloud
```
```diff
// payload.config.ts
- import { payloadCloud } from '@payloadcms/plugin-cloud'
+ import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
buildConfig({
// ...
plugins: [
- payloadCloud()
+ payloadCloudPlugin()
]
})
```
1. **Optional** sharp dependency
If you have upload enabled collections that use `formatOptions`, `imageSizes`, or `resizeOptions`—payload expects to have `sharp` installed. In 2.0 this was a dependency was installed for you. Now it is only installed if needed. If you have any of these options set, you will need to install `sharp` and add it to your payload.config.ts:
```bash
pnpm i sharp
```
```diff
// payload.config.ts
import sharp from 'sharp'
buildConfig({
// ...
+ sharp,
})
```
## Breaking Changes
1. Delete the `admin.bundler` property from your Payload Config. Payload no longer bundles the Admin Panel. Instead, we rely directly on Next.js for bundling.
```diff
// payload.config.ts
- import { webpackBundler } from '@payloadcms/bundler-webpack'
buildConfig({
// ...
admin: {
// ...
- bundler: webpackBundler(),
}
})
```
This also means that the `@payloadcms/bundler-webpack` and `@payloadcms/bundler-vite` packages have been deprecated. You can completely uninstall those from your project by removing them from your `package.json` file and re-running your package manager’s installation process, i.e. `pnpm i`.
1. Add the `secret` property to your Payload Config. This used to be set in the `payload.init()` function of your `server.ts` file. Instead, move it to `payload.config.ts`:
```diff
// payload.config.ts
buildConfig({
// ...
+ secret: process.env.PAYLOAD_SECRET
})
```
1. Environment variables prefixed with `PAYLOAD_PUBLIC` will no longer be available on the client. In order to access them on the client, those will now have to be prefixed with `NEXT_PUBLIC` instead.
```diff
'use client'
- const var = process.env.PAYLOAD_PUBLIC_MY_ENV_VAR
+ const var = process.env.NEXT_PUBLIC_MY_ENV_VAR
```
For more details, see the [Documentation](https://payloadcms.com/docs/configuration/environment-vars).
1. The `req` object used to extend the [Express Request](https://expressjs.com/), but now extends the [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request). You may need to update your code accordingly to reflect this change. For example:
```diff
- req.headers['content-type']
+ req.headers.get('content-type')
```
1. The `admin.css` and `admin.scss` properties in the Payload Config have been removed.
```diff
// payload.config.ts
buildConfig({
// ...
admin: {
// ...
- css: '',
- scss: ''
}
})
```
To migrate, choose one of the following options:
1. For most use cases, you can simply customize the file located at `(payload)/custom.scss`. You can import or add your own styles here, such as for Tailwind.
1. For plugins author, you can use a Custom Provider at `admin.components.providers` to import your stylesheet:
```tsx
// payload.config.js
//...
admin: {
components: {
providers: [
MyProvider: './providers/MyProvider.tsx'
]
}
},
//...
// providers/MyProvider.tsx
'use client'
import React from 'react'
import './globals.css'
export const MyProvider: React.FC<{children?: any}= ({ children }) ={
return (
{t('cancel')}
+ return{t('general:cancel')}
} ``` ## Types 1. The `Fields` type was renamed to `FormState` for improved semantics. If you were previously importing this type in your own application, simply change the import name: ```diff - import type { Fields } from 'payload' + import type { FormState } from 'payload' ``` 1. The `BlockField` and related types have been renamed to `BlocksField` for semantic accuracy. ```diff - import type { BlockField, BlockFieldProps } from 'payload' + import type { BlocksField, BlocksFieldProps } from 'payload' ``` ## Email Adapters Email functionality has been abstracted out into email adapters. - All existing nodemailer functionality was abstracted into the `@payloadcms/email-nodemailer` package - No longer configured with ethereal.email by default. - Ability to pass email into the `init` function has been removed. - Warning will be given on startup if email not configured. Any `sendEmail` call will simply log the To address and subject. - A Resend adapter is now also available via the `@payloadcms/email-resend` package. ### If you used the default email configuration in 2.0 (nodemailer): ```tsx // ❌ Before // via payload.init payload.init({ email: { transport: someNodemailerTransport fromName: 'hello', fromAddress: 'hello@example.com', }, }) // or via email in payload.config.ts export default buildConfig({ email: { transport: someNodemailerTransport fromName: 'hello', fromAddress: 'hello@example.com', }, }) // ✅ After // Using new nodemailer adapter package import { nodemailerAdapter } from '@payloadcms/email-nodemailer' export default buildConfig({ email: nodemailerAdapter() // This will be the old ethereal.email functionality }) // or pass in transport export default buildConfig({ email: nodemailerAdapter({ defaultFromAddress: 'info@payloadcms.com', defaultFromName: 'Payload', transport: await nodemailer.createTransport({ host: process.env.SMTP_HOST, port: 587, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, }) }) }) ``` ### Removal of rate-limiting - Now only available if using custom server and using express or similar ## Plugins 1. _All_ plugins have been standardized to use _named exports_ (as opposed to default exports). Most also have a suffix of `Plugin` to make it clear what is being imported. ```diff - import seo from '@payloadcms/plugin-seo' + import { seoPlugin } from '@payloadcms/plugin-seo' - import stripePlugin from '@payloadcms/plugin-stripe' + import { stripePlugin } from '@payloadcms/plugin-stripe' // and so on for every plugin ``` ## `@payloadcms/plugin-cloud-storage` - The adapters that are exported from `@payloadcms/plugin-cloud-storage` (ie. `@payloadcms/plugin-cloud-storage/s3`) package have been removed. - New _standalone_ packages have been created for each of the existing adapters. Please see the documentation for the one that you use. - `@payloadcms/plugin-cloud-storage` is still fully supported but should only to be used if you are providing a custom adapter that does not have a dedicated package. - If you have created a custom adapter, the type must now provide a `name` property. | Service | Package | | -------------------- | ---------------------------------------------------------------------------- | | Vercel Blob | https://github.com/payloadcms/payload/tree/main/packages/storage-vercel-blob | | AWS S3 | https://github.com/payloadcms/payload/tree/main/packages/storage-s3 | | Azure | https://github.com/payloadcms/payload/tree/main/packages/storage-azure | | Google Cloud Storage | https://github.com/payloadcms/payload/tree/main/packages/storage-gcs | ```tsx // ❌ Before (required peer dependencies depending on adapter) import { cloudStorage } from '@payloadcms/plugin-cloud-storage' import { s3Adapter } from '@payloadcms/plugin-cloud-storage/s3' plugins: [ cloudStorage({ collections: { media: { adapter: s3Adapter({ bucket: process.env.S3_BUCKET, config: { credentials: { accessKeyId: process.env.S3_ACCESS_KEY_ID, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, }, region: process.env.S3_REGION, }, }), }, }, }), ], // ✅ After import { s3Storage } from '@payloadcms/storage-s3' plugins: [ s3Storage({ collections: { media: true, }, bucket: process.env.S3_BUCKET, config: { credentials: { accessKeyId: process.env.S3_ACCESS_KEY_ID, secretAccessKey: process.env.S3_SECRET_ACCESS_KEY, }, region: process.env.S3_REGION, }, }), ], ``` ## `@payloadcms/plugin-form-builder` 1. Field overrides for form and form submission collections now accept a function with a `defaultFields` inside the args instead of an array of config ```diff // payload.config.ts import { buildConfig } from 'payload' import { formBuilderPlugin } from '@payloadcms/plugin-form-builder' const config = buildConfig({ // ... plugins: formBuilderPlugin({ - fields: [ - { - name: 'custom', - type: 'text', - } - ], + fields: ({ defaultFields }) => { + return [ + ...defaultFields, + { + name: 'custom', + type: 'text', + }, + ] + } }) }) ``` ## `@payloadcms/plugin-redirects` 1. Field overrides for the redirects collection now accepts a function with a `defaultFields` inside the args instead of an array of config ```diff // payload.config.ts import { buildConfig } from 'payload' import { redirectsPlugin } from '@payloadcms/plugin-redirects' const config = buildConfig({ // ... plugins: redirectsPlugin({ - fields: [ - { - name: 'custom', - type: 'text', - } - ], + fields: ({ defaultFields }) => { + return [ + ...defaultFields, + { + name: 'custom', + type: 'text', + }, + ] + } }) }) ``` ## `@payloadcms/richtext-lexical` If you have custom features for `@payloadcms/richtext-lexical` you will need to migrate your code to the new API. Read more about the new API in the [documentation](https://payloadcms.com/docs/rich-text/building-custom-features). ## Reserved Field names Payload reserves certain field names for internal use. Using any of the following names in your collections or globals will result in those fields being sanitized from the config, which can cause deployment errors. Ensure that any conflicting fields are renamed before migrating. ### General Reserved Names - `file` - `_id` (MongoDB only) - `__v` (MongoDB only) **Important Note**: It is recommended to avoid using field names with an underscore (`_`) prefix unless explicitly required by a plugin. Payload uses this prefix for internal columns, which can lead to conflicts in certain SQL conditions. The following are examples of reserved internal columns (this list is not exhaustive and other internal fields may also apply): - `_order` - `_path` - `_uuid` - `_parent_id` - `_locale` ### Auth-Related Reserved Names These are restricted if your collection uses `auth: true` and does not have `disableAuthStrategy: true`: - `salt` - `hash` - `apiKey` (when `auth.useAPIKey: true` is enabled) - `useAPIKey` (when `auth.useAPIKey: true` is enabled) - `resetPasswordToken` - `resetPasswordExpiration` - `password` - `email` - `username` ### Upload-Related Reserved Names These apply if your collection has `upload: true` configured: - `filename` - `mimetype` - `filesize` - `width` - `height` - `focalX` - `focalY` - `url` - `thumbnailURL` If `imageSizes` is configured, the following are also reserved: - `sizes` If any of these names are found in your collection / global fields, update them before migrating to avoid unexpected issues. ## Upgrade from previous beta Reference this [community-made site](https://payload-releases-filter.vercel.app/?version=3&from=152429656&to=188243150&sort=asc&breaking=on). Set your version, sort by oldest first, enable breaking changes only. Then go through each one of the breaking changes and make the adjustments. You can optionally reference the [blank template](https://github.com/payloadcms/payload/tree/main/templates/blank) for how things should end up.