Skip to content

Latest commit

 

History

History
1111 lines (897 loc) · 33.8 KB

overview.mdx

File metadata and controls

1111 lines (897 loc) · 33.8 KB
title label order desc keywords
2.0 to 3.0 Migration Guide
2.0 to 3.0 Migration Guide
10
Upgrade guide for Payload 2.x projects migrating to 3.0.
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 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

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.

  npx create-payload-app

For more details, see the Documentation.

  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.

      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.
  2. Uninstall deprecated packages:

    pnpm remove express nodemon @payloadcms/bundler-webpack @payloadcms/bundler-vite
  3. 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.

  4. For Payload Cloud users, the plugin has changed.

    Uninstall the old package:

    pnpm remove @payloadcms/plugin-cloud

    Install the new package:

    pnpm i @payloadcms/payload-cloud
    // payload.config.ts
    - import { payloadCloud } from '@payloadcms/plugin-cloud'
    + import { payloadCloudPlugin } from '@payloadcms/payload-cloud'
    
    buildConfig({
      // ...
      plugins: [
    -   payloadCloud()
    +   payloadCloudPlugin()
      ]
    })
  5. 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:

    pnpm i sharp
    // 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.

    // 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.

  2. 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:

    // payload.config.ts
    
    buildConfig({
      // ...
    + secret: process.env.PAYLOAD_SECRET
    })
  3. 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.

    '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.

  4. The req object used to extend the Express Request, but now extends the Web Request. You may need to update your code accordingly to reflect this change. For example:

    - req.headers['content-type']
    + req.headers.get('content-type')
  5. The admin.css and admin.scss properties in the Payload Config have been removed.

    // 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.

    2. For plugins author, you can use a Custom Provider at admin.components.providers to import your stylesheet:

      // 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 (
          <React.fragment>
            {children}
          </React.fragment>
        )
      }
  6. The admin.indexHTML property has been removed. Delete this from your Payload Config.

    // payload.config.ts
    
    buildConfig({
      // ...
      admin: {
        // ...
    -   indexHTML: ''
      }
    })
  7. The collection.admin.hooks property has been removed. Instead, use the new beforeDuplicate field-level hook which take the usual field hook arguments.

    // collections/Posts.ts
    import type { CollectionConfig } from 'payload'
    
    export const PostsCollection: CollectionConfig = {
      slug: 'posts',
      admin: {
        hooks: {
    -     beforeDuplicate: ({ data }) => {
    -       return {
    -         ...data,
    -         title: `${data.title}-duplicate`
    -       }
    -     }
        }
      },
      fields: [
        {
          name: 'title',
          type: 'text',
          hooks: {
    +      beforeDuplicate: [
    +        ({ data }) => `${data.title}-duplicate`
    +      ],
          },
        },
      ],
    }
  8. Fields with unique: true now automatically be appended with “- Copy” through the new admin.beforeDuplicate field hooks (see previous bullet).

  9. The upload.staticDir property must now be an absolute path. Before it would attempt to use the location of the Payload Config and merge the relative path set for staticDir.

    // collections/Media.ts
    import type { CollectionConfig } from 'payload'
    import path from 'path'
    + import { fileURLToPath } from 'url'
    
    + const filename = fileURLToPath(import.meta.url)
    + const dirname = path.dirname(filename)
    
    export const MediaCollection: CollectionConfig = {
      slug: 'media',
      upload: {
    -   staticDir: path.resolve(__dirname, './uploads'),
    +   staticDir: path.resolve(dirname, '../uploads'),
      },
    }
  10. The upload.staticURL property has been removed. If you were using this format URLs when using an external provider, you can leverage the generateFileURL functions in order to do the same.

    // collections/Media.ts
    import type { CollectionConfig } from 'payload'
    
    export const MediaCollection: CollectionConfig = {
      slug: 'media',
      upload: {
    -   staticURL: '',
      },
    }
  11. The admin.favicon property is now admin.icons and the types have changed:

    // payload.config.ts
    import { buildConfig } from 'payload'
    
    const config = buildConfig({
      // ...
      admin: {
    -   favicon: 'path-to-favicon.svg',
    +   icons: [{
    +     path: 'path-to-favicon.svg',
    +     sizes: '32x32'
    +   }]
      }
    })

    For more details, see the Documentation.

  12. The admin.meta.ogImage property has been replaced by admin.meta.openGraph.images:

    // payload.config.ts
    import { buildConfig } from 'payload'
    
    const config = buildConfig({
      // ...
      admin: {
        meta: {
    -     ogImage: '',
    +     openGraph: {
    +       images: []
    +     }
        }
      }
    })

    For more details, see the Documentation.

  13. The args of the admin.livePreview.url function have changed. It no longer receives documentInfo as an arg, and instead, now has collectionConfig and globalConfig.

    // payload.config.ts
    import { buildConfig } from 'payload'
    
    export default buildConfig({
      // ...
      admin: {
        // ...
        livePreview: ({
    -     documentInfo,
    +     collectionConfig,
    +     globalConfig
        }) => ''
      }
    })
  14. The admin.logoutRoute and admin.inactivityRoute properties have been consolidated into a single admin.routes property. To migrate, simply move those two keys as follows:

    // payload.config.ts
    import { buildConfig } from 'payload'
    
    const config = buildConfig({
      // ...
      admin: {
    -   logoutRoute: '/custom-logout',
    +   inactivityRoute: '/custom-inactivity'
    +   routes: {
    +     logout: '/custom-logout',
    +     inactivity: '/custom-inactivity'
    +   }
      }
    })
  15. The custom property in the Payload Config, i.e. Collections, Globals, and Fields is now server only and will not appear in the client-side bundle. To add custom properties to the client bundle, use the new admin.custom property, which will be available on both the server and the client.

    // payload.config.ts
    import { buildConfig } from 'payload'
    
    export default buildConfig({
      custom: {
        someProperty: 'My Server Prop' // Now server only!
      },
      admin: {
    +   custom: {
    +     name: 'My Client Prop' // Available in server AND client
    +   }
      },
    })
  16. hooks.afterError is now an array of functions instead of a single function. The args have also been expanded. Read More.

    // payload.config.ts
    import { buildConfig } from 'payload'
    
    export default buildConfig({
      hooks: {
    -   afterError: async ({ error }) => {
    +   afterError: [
    +     async ({ error, req, res }) => {
    +       // ...
    +     }
    +   ]
      }
    })
  17. The ./src/public directory is now located directly at root level ./public see Next.js docs for details

Custom Components

  1. All Payload React components have been moved from the payload package to @payloadcms/ui. If you were previously importing components into your app from the payload package, for example to create Custom Components, you will need to change your import paths:

    - import { TextField, useField, etc. } from 'payload'
    + import { TextField, useField, etc. } from '@payloadcms/ui'

    Note: for brevity, not all modules are listed here

  2. All Custom Components are now defined as file paths instead of direct imports. If you are using Custom Components in your Payload Config, remove the imported module and point to the file's path instead:

    import { buildConfig } from 'payload'
    - import { MyComponent } from './src/components/Logout'
    
    const config = buildConfig({
      // ...
      admin: {
        components: {
          logout: {
    -       Button: MyComponent,
    +       Button: '/src/components/Logout#MyComponent'
          }
        }
      },
    })

    For more details, see the Documentation.

  3. All Custom Components are now server-rendered by default, and therefore, cannot use state or hooks directly. If you’re using Custom Components in your app that requires state or hooks, add the 'use client' directive at the top of the file.

    // components/MyClientComponent.tsx
    + 'use client'
    import React, { useState } from 'react'
    
    export const MyClientComponent = () => {
      const [state, setState] = useState()
    
      return (
        <div>
          {state}
        </div>
      )
    }

    For more details, see the Documentation.

  4. The admin.description property within Collection, Globals, and Fields no longer accepts a React Component. Instead, you must define it as a Custom Component.

    1. For Collections, use the admin.components.edit.Description key:
    // collections/Posts.ts
    import type { CollectionConfig } from 'payload'
    - import { MyCustomDescription } from '../components/MyCustomDescription'
    
    export const PostsCollection: CollectionConfig = {
      slug: 'posts',
      admin: {
    -    description: MyCustomDescription,
    +    components: {
    +      edit: {
    +        Description: 'path/to/MyCustomDescription'
    +      }
    +    }
      }
    }
    1. For Globals, use the admin.components.elements.Description key:
    // globals/Site.ts
    import type { GlobalConfig } from 'payload'
    - import { MyCustomDescription } from '../components/MyCustomDescription'
    
    export const SiteGlobal: GlobalConfig = {
      slug: 'site',
      admin: {
    -    description: MyCustomDescription,
    +    components: {
    +      elements: {
    +        Description: 'path/to/MyCustomDescription'
    +      }
    +    }
      }
    }
    1. For Fields, use the admin.components.Description key:
    // fields/MyField.ts
    import type { FieldConfig } from 'payload'
    - import { MyCustomDescription } from '../components/MyCustomDescription'
    
    export const MyField: FieldConfig = {
      type: 'text',
      admin: {
    -    description: MyCustomDescription,
    +    components: {
    +      Description: 'path/to/MyCustomDescription'
    +    }
      }
    }
  5. Array Field row labels and Collapsible Field label now only accepts a React Component, and no longer accepts a plain string or record:

    // file: Collection.tsx
    import type { CollectionConfig } from 'payload'
    - import { MyCustomRowLabel } from './components/MyCustomRowLabel.tsx'
    
    export const MyCollection: CollectionConfig = {
      slug: 'my-collection',
      fields: [
        {
          name: 'my-array',
          type: 'array',
          admin: {
            components: {
    -         RowLabel: 'My Array Row Label,
    +         RowLabel: './components/RowLabel.ts'
            }
          },
          fields: [...]
        },
        {
          name: 'my-collapsible',
          type: 'collapsible',
          admin: {
            components: {
    -         Label: 'My Collapsible Label',
    +         Label: './components/RowLabel.ts'
            }
          },
          fields: [...]
        }
      ]
    }
  6. All default view keys are now camelcase:

    For example, for Root Views:

    // file: payload.config.ts
    
    import { buildConfig } from 'payload'
    
    export default buildConfig({
    admin: {
      views: {
    -    Account: ...
    +    account: ...
      }
    })

    Or Document Views:

    // file: Collection.tsx
    
    import type { CollectionConfig } from 'payload'
    
    export const MyCollection: CollectionConfig = {
      slug: 'my-collection',
      admin: {
        views: {
    -     Edit: {
    -       Default: ...
    -     }
    +     edit: {
    +       default: ...
    +     }
        }
      }
    }
  7. Custom Views within the config no longer accept React Components directly, instead, you must use their Component property:

    // file: Collection.tsx
    import type { CollectionConfig } from 'payload'
    - import { MyCustomView } from './components/MyCustomView.tsx'
    
    export const MyCollection: CollectionConfig = {
      slug: 'my-collection',
      admin: {
        views: {
    -     Edit: MyCustomView
    +     edit: {
    +       Component: './components/MyCustomView.tsx'
    +     }
        }
      }
    }

    This also means that Custom Root Views are no longer defined on the edit key. Instead, use the new views.root key:

    // file: Collection.tsx
    import type { CollectionConfig } from 'payload'
    - import { MyCustomRootView } from './components/MyCustomRootView.tsx'
    
    export const MyCollection: CollectionConfig = {
      slug: 'my-collection',
      admin: {
        views: {
    -     Edit: MyCustomRootView
          edit: {
    +       root: {
    +         Component: './components/MyCustomRootView.tsx'
    +       }
          }
        }
      }
    }
  8. The href and isActive functions on View Tabs no longer includes the match or location arguments. This is is a property specific to React Router, not Next.js. If you need to do URL matching similar to this, use a custom tab that fires of some hooks, i.e. usePathname() and run it through your own utility functions:

    // collections/Posts.ts
    import type { CollectionConfig } from 'payload'
    
    export const PostsCollection: CollectionConfig = {
      slug: 'posts',
      admin: {
        components: {
          views: {
    -        Edit: {
    -          Tab: {
    -            isActive: ({ href, location, match }) => true,
    -            href: ({ href, location, match }) => ''
    -          },
    -       },
    +       edit: {
    +         tab: {
    +           isActive: ({ href }) => true,
    +           href: ({ href }) => '' // or use a Custom Component (see below)
    +           // Component: './path/to/CustomComponent.tsx'
    +         }
    +       },
          },
        },
      },
    }
  9. The admin.components.views[key].Tab.pillLabel has been replaced with admin.components.views[key].tab.Pill:

    // collections/Posts.ts
    import type { CollectionConfig } from 'payload'
    
    export const PostsCollection: CollectionConfig = {
      slug: 'posts',
      admin: {
        components: {
    -     views: {
    -       Edit: {
    -         Tab: {
    -           pillLabel: 'Hello, world!',
    -         },
    -       },
    +       edit: {
    +         tab: {
    +           Pill: './path/to/CustomPill.tsx',
    +         }
    +       },
          },
        },
      },
    }
  10. react-i18n was removed, the Trans component from react-i18n has been replaced with a Payload-provided solution:

    'use client'
    - import { Trans } from "react-i18n"
    + import { Translation } from "@payloadcms/ui"
    
    // Example string to translate:
    // "loggedInChangePassword": "To change your password, go to your <0>account</0> and edit your password there."
    
    export const MyComponent = () => {
      return (
    -     <Trans i18nKey="loggedInChangePassword" t={t}>
    -       <Link to={`${admin}/account`}>account</Link>
    -     </Trans>
    
    +     <Translation
    +       t={t}
    +       i18nKey="authentication:loggedInChangePassword"
    +       elements={{
    +         '0': ({ children }) => <Link href={`${admin}/account`} children={children} />,
    +       }}
    +     />
      )
    }

Endpoints

  1. All endpoint handlers have changed. The args no longer include res, and next, and the return type now expects a valid HTTP Response instead of res.json, res.send, etc.:

    // collections/Posts.ts
    import type { CollectionConfig } from 'payload'
    
    export const PostsCollection: CollectionConfig = {
      slug: 'posts',
      endpoints: [
    -   {
    -     path: '/whoami/:parameter',
    -     method: 'post',
    -     handler: (req, res) => {
    -       res.json({
    -         parameter: req.params.parameter,
    -         name: req.body.name,
    -         age: req.body.age,
    -       })
    -     }
    -   },
    +   {
    +     path: '/whoami/:parameter',
    +     method: 'post',
    +     handler: (req) => {
    +       return Response.json({
    +         parameter: req.routeParams.parameter,
    +         // ^^ `params` is now `routeParams`
    +         name: req.data.name,
    +         age: req.data.age,
    +       })
    +     }
    +   }
      ]
    }
  2. Endpoint handlers no longer resolves data, locale, or fallbackLocale for you on the request. Instead, you must resolve them yourself or use the Payload-provided utilities:

    // collections/Posts.ts
    import type { CollectionConfig } from 'payload'
    + import { addDataAndFileToRequest } from '@payloadcms/next/utilities'
    + import { addLocalesToRequest } from '@payloadcms/next/utilities'
    
    export const PostsCollection: CollectionConfig = {
      slug: 'posts',
      endpoints: [
    -   {
    -     path: '/whoami/:parameter',
    -     method: 'post',
    -     handler: async (req) => {
    -       return Response.json({
    -         name: req.data.name, // data will be undefined
    -       })
    -     }
    -   },
    +   {
    +     path: '/whoami/:parameter',
    +     method: 'post',
    +     handler: async (req) => {
    +       // mutates req, must be awaited
    +       await addDataAndFileToRequest(req)
    +       await addLocalesToRequest(req)
    +
    +       return Response.json({
    +         name: req.data.name, // data is now available
    +    	    fallbackLocale: req.fallbackLocale,
    +         locale: req.locale,
    +       })
    +     }
    +   }
      ]
    }

React Hooks

  1. The useTitle hook has been consolidated into the useDocumentInfo hook. Instead, you can get title directly from document info context:

    'use client'
    - import { useTitle } from 'payload'
    + import { useDocumentInfo } from '@payloadcms/ui'
    
    export const MyComponent = () => {
    - const title = useTitle()
    + const { title } = useDocumentInfo()
    
      // ...
    }
  2. The useDocumentInfo hook no longer returns collection or global. Instead, various properties of the config are passed, like collectionSlug and globalSlug. You can use these to access a client-side config, if needed, through the useConfig hook (see next bullet).

    'use client'
    import { useDocumentInfo } from '@payloadcms/ui'
    
    export const MyComponent = () => {
      const {
    -   collection,
    -   global,
    +   collectionSlug,
    +   globalSlug
      } = useDocumentInfo()
    
      // ...
    }
  3. The useConfig hook now returns a ClientConfig and not a SanitizedConfig. This is because the config itself is not serializable and so it is not able to be thread through to the client. This means that all non-serializable props have been omitted from the Client Config, such as db, bundler, etc.

    'use client'
    - import { useConfig } from 'payload'
    + import { useConfig } from '@payloadcms/ui'
    
    export const MyComponent = () => {
    - const config = useConfig() // used to be a 'SanitizedConfig'
    + const { config } = useConfig() // now is a 'ClientConfig'
    
      // ...
    }

    For more details, see the Documentation.

  4. The useCollapsible hook has had slight changes to its property names. collapsed is now isCollapsed and withinCollapsible is now isWithinCollapsible.

    'use client'
    import { useCollapsible } from '@payloadcms/ui'
    
    export const MyComponent = () => {
    - const { collapsed, withinCollapsible } = useCollapsible()
    + const { isCollapsed, isWithinCollapsible } = useCollapsible()
    }
  5. The useTranslation hook no longer takes any options, any translations using shorthand accessors will need to use the entire group:key

    'use client'
    - import { useTranslation } from 'payload'
    + import { useTranslation } from '@payloadcms/ui'
    
    export const MyComponent = () => {
    - const { i18n, t } = useTranslation('general')
    + const { i18n, t } = useTranslation()
    
    - return <p>{t('cancel')}</p>
    + return <p>{t('general:cancel')}</p>
    }

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:

    - import type { Fields } from 'payload'
    + import type { FormState } from 'payload'
  2. The BlockField and related types have been renamed to BlocksField for semantic accuracy.

    - 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):

// ❌ 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.

    - 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
// ❌ 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

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

    // 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.

Upgrade from previous beta

Reference this community-made site. 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 for how things should end up.