# Payload 4.x Documentation > Complete official documentation for Payload 4.x. Do not mix APIs or examples from another major version. # What is Payload? Source: https://payloadcms.com/docs/beta/getting-started/what-is-payload [Video: Introduction to Payload — The open-source Next.js backend](https://www.youtube.com/watch?v=ftohATkHBi0) ## Payload is a fullstack framework. Write a Payload Config and instantly get a full Admin Panel, a database with migrations, REST and GraphQL APIs, authentication, access control, file storage, live preview, and more — all in one open-source TypeScript codebase you own and deploy anywhere. Payload works with the Next.js App Router or TanStack Start. - [Use Cases](/docs/v4/getting-started/use-cases.md): See how teams use Payload for headless CMS, internal tools, commerce, and DAM workloads — and whether it fits yours. - [Core Concepts](/docs/v4/getting-started/concepts.md): Get familiar with the high-level concepts Payload is built around before you write any code. - [Installation](/docs/v4/getting-started/installation.md): Add Payload to a new or existing Next.js or TanStack Start app and get the Admin Panel running in minutes. - [The Admin Panel](/docs/v4/admin/overview.md): Explore the auto-generated React UI that matches the shape of your data and extends with your own components. - [Database & Schema](/docs/v4/database/overview.md): Direct DB access and ownership with migrations, transactions, and proper indexing across MongoDB and Postgres. - [REST & GraphQL APIs](/docs/v4/rest-api/overview.md): Instant REST, GraphQL, and straight-to-DB Node.js APIs generated from your config. - [Authentication](/docs/v4/authentication/overview.md): A full auth system you can use in your Payload app and reuse in your own frontends. - [Access Control](/docs/v4/access-control/overview.md): A deeply customizable access pattern that runs at the document, field, and operation level. - [File Uploads](/docs/v4/upload/overview.md): Built-in file storage, image resizing, focal-point cropping, and media access control. # Use Cases Source: https://payloadcms.com/docs/beta/getting-started/use-cases Payload started as a headless Content Management System (CMS), but since, we've seen our community leverage Payload in ways far outside of simply managing pages and blog posts. It's grown into a full-stack TypeScript app framework. Large enterprises use Payload to power significant internal tools, retailers power their entire storefronts without the need for headless Shopify, and massive amounts of digital assets are stored + managed within Payload. Of course, websites large and small still use Payload for content management as well. ### Headless CMS The biggest barrier in large web projects cited by marketers is engineering. On the flip side, engineers say the opposite. This is a big problem that has yet to be solved even though we have countless CMS options. Payload has restored a little love back into the dev / marketer equation with features like Live Preview, redirects, form builders, visual editing, static A/B testing, and more. But even with all this focus on marketing efficiency, we aren't compromising on the developer experience. That way engineers and marketers alike can be proud of the products they build. If you're building a website and your frontend is on Next.js or TanStack Start, then Payload is a no-brainer. > **Success** > > Instead of going out and signing up for a SaaS vendor that makes it so you > have to manage two completely separate concerns, with little to no native > connection back and forth, just install Payload in your existing Next.js or > TanStack Start repo and instantly get a full CMS. Get started with Payload as a CMS using our official Website template: ``` npx create-payload-app@latest -t website ``` ### Enterprise Tool When a large organization starts up a new software initiative, there's a lot of plumbing to take care of. - Scaffold the data layer with an ORM or an app framework like Ruby on Rails or Laravel - Implement their SSO provider for authentication - Design an access control pattern for authorization - Open up any REST endpoints required or implement GraphQL queries / mutations - Implement a migrations workflow for the database as it changes over time - Integrate with other third party solutions by crafting a system of webhooks or similar And then there's the [Admin Panel](/docs/v4/admin/overview.md). Most enterprise tools require an admin UI, and building one from scratch can be the most time-consuming aspect of any new enterprise tool. There are off-the-shelf packages for app frameworks like Rails, but often the customization is so involved that using Material UI or similar from scratch might be better. Then there are no-code admin builders that could be used. However, wiring up access control and the connection to the data layer, with proper version control, makes this a challenging task as well. That's where Payload comes in. Payload instantly provides all of this out of the box, making complex internal tools extremely simple to both spin up and maintain over time. The only custom code that will need to be written is any custom business logic. That means Payload can expedite timelines, keep budgets low, and allow engineers to focus on their specific requirements rather than complex backend / admin UI plumbing. Generally, the best place to start for a new enterprise tool is with a blank canvas, where you can define your own functionality: ``` npx create-payload-app@latest -t blank ``` ### Headless Commerce Companies who prioritize UX generally run into frontend constraints with traditional commerce vendors. These companies will then opt for frontend frameworks like Next.js which allow them to fine-tune their user experience as much as possible—promoting conversions, personalizing experiences, and optimizing for SEO. But the challenge with using something like Next.js for headless commerce is that in order for non-technical users to manage the storefront, you instantly need to pair a headless commerce product with a headless CMS. Then, your editors need to bounce back and forth between different admin UIs for different functionality. The code required to seamlessly glue them together on the frontend becomes overly complex. Payload can integrate with any payment processor like Stripe and its content authoring capabilities allow it to manage every aspect of a storefront—all in one place. If you can build your storefront with a single backend, and only offload things like payment processing, the code will be simpler and the editing experience will be significantly streamlined. Manage products, catalogs, page content, media, and more—all in one spot. ### Digital Asset Management Payload's API-first tagging, sorting, and querying engine lends itself perfectly to all types of content that a CMS might ordinarily store, but these strong fundamentals also make it a formidable Digital Asset Management (DAM) tool as well. Similarly to the Ecommerce use case above, if an organization uses a CMS for its content but a separate DAM for its digital assets, administrators of both tools will need to juggle completely different services for tasks that are closely related. Two subscriptions will need to be managed, two sets of infrastructure will need to be provisioned, and two admin UIs need to be used / learned. Payload flattens CMS and DAM into a single tool that makes no compromises on either side. Powerful features like folder-based organization, file versioning, bulk upload, and media access control allow Payload to simultaneously function as a full Digital Asset Management platform as well as a Content Management System at the same time. [Click here](https://payloadcms.com/use-cases/digital-asset-management) for more information on how to get started with Payload as a DAM. ## Is Payload Right For You? Payload is a great choice for applications of all sizes and types, but it might not be the right choice for every project. Here are some guidelines to help you decide if Payload is the right choice for your project. ### When Payload might be for you - If data ownership and privacy are important to you, and you don't want to allow another proprietary SaaS vendor to host and own your data - If you're building a Next.js or TanStack Start site that needs a CMS - If you need to re-use your data outside of a SaaS API - If what you're building has custom business logic requirements outside of a typical headless CMS - You want to deploy serverless on platforms like Vercel ### When Payload might not be for you - If you can manage your project fully with code, and don't need an admin UI - If you are building a website that fits within the limits of a tool like Webflow or Framer - If you already have a full database and just need to visualize the data somehow - If you are confident that you won't need code / data ownership at any point in the future # Payload Concepts Source: https://payloadcms.com/docs/beta/getting-started/concepts Payload is based around a small and intuitive set of high-level concepts. Before starting to work with Payload, it's a good idea to familiarize yourself with these concepts in order to establish a common language and understanding when discussing Payload. - [Config](/docs/v4/configuration/overview.md): A deeply typed JavaScript object that drives everything Payload does — the central piece of every project. - [Database](/docs/v4/database/overview.md): Plug in Postgres, MongoDB, or SQLite behind Payload's APIs via a Database Adapter — Payload is database agnostic. - [Collections](/docs/v4/configuration/collections.md): A group of records (Documents) that share a schema. The most common way you'll model data in Payload. - [Globals](/docs/v4/configuration/globals.md): Like Collections, but for content that has only one of — settings, navigation, footer, and so on. - [Fields](/docs/v4/fields/overview.md): The building blocks of Payload — they define your schema and auto-generate the matching Admin UI. - [Hooks](/docs/v4/hooks/overview.md): Run your own side effects at specific points in the Document lifecycle — before read, after create, and more. - [Authentication](/docs/v4/authentication/overview.md): A secure, portable user-account system for both the Admin Panel and your own external applications. - [Access Control](/docs/v4/access-control/overview.md): Decide who can read, update, or delete every Document — and what they see in the Admin Panel. - [Admin Panel](/docs/v4/admin/overview.md): The auto-generated, fully type-safe React interface editors use to manage your data. Built on Next.js or TanStack Start. ## Retrieving Data Everything Payload does (create, read, update, delete, login, logout, etc.) is exposed to you via three APIs: - [Local API](/docs/v4/local-api/overview.md): Direct-to-database access with no HTTP overhead — call payload.find from any server context, including React Server Components. - [REST API](/docs/v4/rest-api/overview.md): Standard HTTP endpoints mounted at /api — fetch documents from any client with full pagination, depth, and sorting. - [GraphQL](/docs/v4/graphql/overview.md): A full GraphQL API with a Playground at /api/graphql-playground. Works with graphql-request, Apollo, or any other client. > **Note:** All of these APIs share the exact same query language. [More > details](/docs/v4/queries/overview.md). ## Package Structure Payload is abstracted into a set of dedicated packages to keep the core `payload` package as lightweight as possible. This allows you to only install the parts of Payload based on your unique project requirements. > **Important:** Version numbers of all official Payload packages are always > published in sync. You should make sure that you always use matching versions > for all official Payload packages. `payload` The `payload` package is where core business logic for Payload lives. You can think of Payload as an ORM with superpowers—it contains the logic for all Payload "operations" like `find`, `create`, `update`, and `delete` and exposes a [Local API](/docs/v4/local-api/overview.md). It executes [Access Control](/docs/v4/access-control/overview.md), [Hooks](/docs/v4/hooks/overview.md), [Validation](/docs/v4/fields/overview.md#validation), and more. Payload itself is extremely compact, and can be used in any Node environment. As long as you have `payload` installed and you have access to your Payload Config, you can query and mutate your database directly without going through an unnecessary HTTP layer. Payload also contains all TypeScript definitions, which can be imported from `payload` directly. Here's how to import some common Payload types: ```ts import { Config, CollectionConfig, GlobalConfig, Field } from 'payload' ``` `@payloadcms/next` Whereas Payload itself is responsible for direct database access, and control over Payload business logic, the `@payloadcms/next` package is responsible for the Admin Panel and the entire HTTP layer that Payload exposes, including the [REST API](/docs/v4/rest-api/overview.md) and [GraphQL API](/docs/v4/graphql/overview.md). The `@payloadcms/tanstack-start` package provides that same Admin Panel and HTTP layer for apps built on TanStack Start. `@payloadcms/graphql` All of Payload's GraphQL functionality is abstracted into a separate package. Payload, its Admin UI, and REST API have absolutely no overlap with GraphQL, and you will incur no performance overhead from GraphQL if you are not using it. However, it's installed within the `@payloadcms/next` package so you don't have to install it manually. You do, however, need to have GraphQL installed separately in your `package.json` if you are using GraphQL. `@payloadcms/ui` This is the UI library that Payload's Admin Panel uses. All components are exported from this package and can be re-used as you build extensions to the Payload admin UI, or want to use Payload components in your own React apps. Some exports are server components and some are client components. `@payloadcms/db-postgres`, `@payloadcms/db-vercel-postgres`, `@payloadcms/db-mongodb`, `@payloadcms/db-sqlite` You can choose which Database Adapter you'd like to use for your project, and no matter which you choose, the entire data layer for Payload is contained within these packages. You can only use one at a time for any given project. `@payloadcms/richtext-lexical` Payload's Rich Text functionality is abstracted into a separate package. If you want to enable Rich Text in your project, you'll need to install this package. > **Note:** Rich Text is entirely optional and you may not need it for your > project. # Installation Source: https://payloadcms.com/docs/beta/getting-started/installation ## Software Requirements Payload requires the following software: - Any JavaScript package manager (pnpm, npm, or yarn 2+ - pnpm is preferred, yarn 1.x is not supported) - Node.js version `24.22.1`+ - Next.js `16.4.0`+ - TypeScript version `6.0.3`+. Types are not guaranteed to work on older versions. - Any [compatible database](/docs/v4/database/overview.md) (MongoDB, Postgres or SQLite) > **Important:** Before proceeding any further, please ensure that you have the > above requirements met. Not all Next.js 16 releases are compatible — make sure > you're using one of the supported version ranges listed above. ## Quickstart with create-payload-app To quickly scaffold a new Payload app in the fastest way possible, you can use [create-payload-app](https://npmjs.com/package/create-payload-app). To do so, run the following command: ```bash npx create-payload-app@beta ``` Then just follow the prompts! You'll get set up with a new folder and a functioning Payload app inside. You can then start [configuring your application](/docs/v4/configuration/overview.md). ## Adding to an existing app Adding Payload to an existing Next.js app is super straightforward. You can either run the `npx create-payload-app` command inside your Next.js project's folder, or manually install Payload by following the steps below. If you don't have a Next.js app already, but you still want to start a project from a blank Next.js app, you can create a new Next.js app using `npx create-next-app` - and then just follow the steps below to install Payload. > **Note** > > The steps below target the **Next.js App Router**. Payload's Admin Panel also > runs on [TanStack Start](https://tanstack.com/start/latest) via the > `@payloadcms/tanstack-start` package — the differences are called out inline > where they apply. See [Project Structure](/docs/v4/admin/overview.md#project-structure) > for the TanStack Start file layout. #### 1. Install the relevant packages First, you'll want to add the required Payload packages to your project: ```bash pnpm i payload @payloadcms/next ``` If you are running the Admin Panel on TanStack Start, install `@payloadcms/tanstack-start` in place of `@payloadcms/next`: ```bash pnpm i payload @payloadcms/tanstack-start ``` You'll also likely want to install the following optional packages: - `@payloadcms/richtext-lexical` - Rich text editor (not needed if you don't use Rich Text) - `sharp` - Image resizing, cropping, and focal point support (only needed if you use [Upload](/docs/v4/upload/overview.md) collections with image manipulation) - `graphql` - Only needed if you want to use the [GraphQL API](/docs/v4/graphql/overview.md) ```bash pnpm i @payloadcms/richtext-lexical sharp graphql ``` > **Note:** Swap out `pnpm` for your package manager. If you are using npm, you > might need to install using legacy peer deps: `npm i --legacy-peer-deps`. Next, install a [Database Adapter](/docs/v4/database/overview.md). Payload requires a Database Adapter to establish a database connection. Payload works with all types of databases, but the most common are MongoDB and Postgres. To install a Database Adapter, you can run **one** of the following commands: - To install the [MongoDB Adapter](/docs/v4/database/mongodb.md), run: ```bash pnpm i @payloadcms/db-mongodb ``` - To install the [Postgres Adapter](/docs/v4/database/postgres.md), run: ```bash pnpm i @payloadcms/db-postgres ``` - To install the [SQLite Adapter](/docs/v4/database/sqlite.md), run: ```bash pnpm i @payloadcms/db-sqlite ``` > **Note:** New [Database Adapters](/docs/v4/database/overview.md) are becoming > available every day. Check the docs for the most up-to-date list of what's > available. #### 2. Copy Payload files into your app For Next.js, Payload installs directly in your `/app` folder, and you'll need to place some files into that folder for Payload to run. You can copy these files from the [Blank Template](https://github.com/payloadcms/payload/tree/main/templates/blank/src/app/%28payload%29) on GitHub. Once you have the required Payload files in place in your `/app` folder, you should have something like this: ```plaintext app/ ├─ (payload)/ ├── // Payload files ├─ (my-app)/ ├── // Your app files ``` _For an exact reference of the `(payload)` directory, see [Project Structure](/docs/v4/admin/overview.md#project-structure)._ > **Warning** > > You may need to copy all of your existing frontend files, including your > existing root layout, into its own newly created [Route > Group](https://nextjs.org/docs/app/building-your-application/routing/route-groups), > i.e. `(my-app)`. For TanStack Start, copy the generated routes and supporting files from the [Blank TanStack Start Template](https://github.com/payloadcms/payload/tree/main/templates/blank-tanstack/src/app) into your `src/app` directory. The files are `__root.tsx`, `_payload.tsx`, the `_payload/` route directory, and `router.tsx` at `src/router.tsx`: ```plaintext src/ ├─ router.tsx └─ app/ ├─ __root.tsx ├─ _payload.tsx └─ _payload/ ``` The Next.js files that Payload needs in your `/app` folder do not regenerate and will never change. Once you slot them in, you never have to revisit them. They are not meant to be edited and simply import Payload dependencies from `@payloadcms/next` for the REST / GraphQL API and Admin Panel. The corresponding TanStack Start files import from `@payloadcms/tanstack-start` and `@payloadcms/tanstack-start/client`. You can name the `(my-app)` folder anything you want. The name does not matter and will just be used to clarify your directory structure for yourself. Common names might be `(frontend)`, `(app)`, or similar. [More details](/docs/v4/admin/overview.md). #### 3. Add the Payload Plugin to your Next.js config Payload has a Next.js plugin that it uses to ensure compatibility with some of the packages Payload relies on, like `mongodb` or `drizzle-kit`. To add the Payload Plugin, use `withPayload` in your `next.config.js`: ```js import { withPayload } from '@payloadcms/next/withPayload' /** @type {import('next').NextConfig} */ const nextConfig = { // Your Next.js config here } // Make sure you wrap your `nextConfig` // with the `withPayload` plugin export default withPayload(nextConfig) // highlight-line ``` > **Important:** Payload is a fully ESM project, and that means the > `withPayload` function is an ECMAScript module. To import the Payload Plugin, you need to make sure your `next.config` file is set up to use ESM. You can do this in one of two ways: 1. Set your own project to use ESM, by adding `"type": "module"` to your `package.json` file 2. Give your Next.js config the `.mjs` file extension In either case, all `require`s and `export`s in your `next.config` file will need to be converted to `import` / `export` if they are not set up that way already. > **TanStack Start:** there is no `next.config` step. TanStack Start builds with > [Vite](https://vite.dev), so use `withPayload` from > `@payloadcms/tanstack-start` in your `vite.config`. Pass it a > `payloadConfigPath` and provide the `rsc`, `tanstackStart`, and `viteReact` > plugins through its `pluginOptions`. See the [Blank TanStack Start > Template](https://github.com/payloadcms/payload/blob/main/templates/blank-tanstack/vite.config.ts) > for a complete example. #### 4. Create a Payload Config and add it to your TypeScript config Finally, you need to create a [Payload Config](/docs/v4/configuration/overview.md). Generally the Payload Config is located at the root of your repository, or next to your `/app` folder, and is named `payload.config.ts`. Here's what Payload needs at a bare minimum: ```ts import sharp from 'sharp' import { lexicalEditor } from '@payloadcms/richtext-lexical' import { mongooseAdapter } from '@payloadcms/db-mongodb' import { buildConfig } from 'payload' export default buildConfig({ // If you'd like to use Rich Text, pass your editor here editor: lexicalEditor(), // Define and configure your collections in this array collections: [], // Your Payload secret - should be a complex and secure string, unguessable secret: process.env.PAYLOAD_SECRET || '', // Whichever Database Adapter you're using should go here // Mongoose is shown as an example, but you can also use Postgres db: mongooseAdapter({ url: process.env.DATABASE_URL || '', }), // If you want to resize images, crop, set focal point, etc. // make sure to install it and pass it to the config. // This is optional - if you don't need to do these things, // you don't need it! sharp, }) ``` Although this is just the bare minimum config, there are _many_ more options that you can control here. To reference the full config and all of its options, [click here](/docs/v4/configuration/overview.md). Once you have a Payload Config, update your `tsconfig` to include a `path` that points to it: ```json { "compilerOptions": { "paths": { "@payload-config": ["./payload.config.ts"] } } } ``` When using TanStack Start, the `payloadConfigPath` passed to `withPayload` in `vite.config.ts` must point to the same config file as this `@payload-config` TypeScript alias. #### 5. Fire it up! After you've reached this point, it's time to boot up Payload. Start your project in your application's folder to get going. By default, the Next.js dev script is `pnpm dev` (or `npm run dev` if using npm). After it starts, you can go to `http://localhost:3000/admin` to create your first Payload user! # AI Tooling Source: https://payloadcms.com/docs/beta/getting-started/ai-tooling Payload publishes an agent skill inside the `payload` package. Because it ships with the package, the guidance your AI assistant reads always matches the version of Payload you have installed. The skill lives at `node_modules/payload/skills/payload/` once `payload` is installed. ## What's included The skill's `SKILL.md` opens with a Quick Reference table mapping common tasks to the relevant reference file, so your assistant only opens the reference docs a given task needs rather than loading everything up front. Those reference docs cover: - **Collections**: auth, uploads, drafts, and live preview configurations - **Fields**: all field types including relationships, arrays, blocks, joins, and virtual fields - **Hooks**: `beforeChange`, `afterChange`, `beforeValidate`, and field hooks - **Access Control**: collection, field, and global patterns including RBAC and multi-tenant, plus advanced row-level security - **Queries**: Local API, REST, and GraphQL with complex operators - **Database Adapters**: MongoDB, Postgres, and SQLite configuration and transactions - **Endpoints**: custom collection and global API routes - **Plugin Development**: plugin architecture and package structure - **Advanced Features**: jobs queue, localization, and other advanced configuration ## Setup New projects created with `create-payload-app` are configured automatically with an `AGENTS.md` at the project root. Pass `--no-agent` to skip it. For an existing project, add `AGENTS.md` at the project root: ```md # AI Agent For any Payload-related work, reference the skill at `node_modules/payload/skills/payload/SKILL.md`. ``` > **Note** > > `SKILL.md` costs roughly 8,000 tokens once your assistant opens it. The files > under `reference/` are only read on demand, so the full skill is not loaded > into context up front. ## Claude Code plugin Payload also publishes the skill as a Claude Code plugin. The plugin tracks the `main` branch rather than your installed version, so prefer it only when contributing to Payload itself or when you specifically want unreleased guidance. ```bash /plugin marketplace add payloadcms/payload /plugin install payload@payload-marketplace ``` > **Warning** > > Use one channel per project. The plugin and the `node_modules` pointer both > provide a skill named `payload`. Enabling both puts two copies at different > versions in scope, and they can give conflicting guidance. # Migration Guides Source: https://payloadcms.com/docs/beta/migration-guide/overview Payload follows [semantic versioning](https://semver.org/). Major version bumps may include breaking changes; the guides below walk through each upgrade path. - [3.0 to 4.0 Migration Guide](/docs/v4/migration-guide/v4.md) — upgrade Payload 3.x projects to 4.0. - [2.0 to 3.0 Migration Guide](/docs/v4/migration-guide/v3.md) — upgrade Payload 2.x projects to 3.0. # 2.0 to 3.0 Migration Guide Source: https://payloadcms.com/docs/beta/migration-guide/v3 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](/docs/v4/getting-started/installation.md). 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-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](/docs/v4/configuration/environment-vars.md). 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.css`. 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 ( {children} ) } ``` 1. The `admin.indexHTML` property has been removed. Delete this from your Payload Config. ```diff // payload.config.ts buildConfig({ // ... admin: { // ... - indexHTML: '' } }) ``` 1. The `collection.admin.hooks` property has been removed. Instead, use the new `beforeDuplicate` field-level hook which take the usual field hook arguments. ```diff // 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` + ], }, }, ], } ``` 1. Fields with `unique: true` now automatically be appended with “- Copy” through the new `admin.beforeDuplicate` field hooks (see previous bullet). 1. 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. ```diff // 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'), }, } ``` 1. 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. ```diff // collections/Media.ts import type { CollectionConfig } from 'payload' export const MediaCollection: CollectionConfig = { slug: 'media', upload: { - staticURL: '', }, } ``` 1. The `admin.favicon` property is now `admin.icons` and the types have changed: ```diff // 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](/docs/v4/admin/metadata.md#icons). 1. The `admin.meta.ogImage` property has been replaced by `admin.meta.openGraph.images`: ```diff // payload.config.ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { meta: { - ogImage: '', + openGraph: { + images: [] + } } } }) ``` For more details, see the [Documentation](/docs/v4/admin/metadata.md#open-graph). 1. 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`. ```diff // payload.config.ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // ... livePreview: ({ - documentInfo, + collectionConfig, + globalConfig }) => '' } }) ``` 1. 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: ```diff // payload.config.ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { - logoutRoute: '/custom-logout', + inactivityRoute: '/custom-inactivity' + routes: { + logout: '/custom-logout', + inactivity: '/custom-inactivity' + } } }) ``` 1. 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. ```diff // 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 + } }, }) ``` 1. `hooks.afterError` is now an array of functions instead of a single function. The args have also been expanded. Read [More](/docs/v4/hooks/overview.md#root-hooks). ```diff // payload.config.ts import { buildConfig } from 'payload' export default buildConfig({ hooks: { - afterError: async ({ error }) => { + afterError: [ + async ({ error, req, res }) => { + // ... + } + ] } }) ``` 1. The `./src/public` directory is now located directly at root level `./public` [see Next.js docs for details](https://nextjs.org/docs/pages/building-your-application/optimizing/static-assets) 1. Payload now automatically removes `localized: true` property from sub-fields if a parent is localized, as it's redundant and unnecessary. If you have some existing data in this structure and you want to disable that behavior, you need to enable `allowLocalizedWithinLocalized` flag in your payload.config [read more in documentation](/docs/v4/configuration/overview.md#compatibility-flags), or create a migration script that aligns your data. Mongodb example for a link in a page layout. ```diff - layout.columns.en.link.en.type.en + layout.columns.en.link.type ``` ## 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: ```diff - import { TextField, useField, etc. } from 'payload' + import { TextField, useField, etc. } from '@payloadcms/ui' ``` _Note: for brevity, not *all* modules are listed here_ 1. 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: ```diff 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](/docs/v4/custom-components/overview.md#component-paths). 1. 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. ```diff // components/MyClientComponent.tsx + 'use client' import React, { useState } from 'react' export const MyClientComponent = () => { const [state, setState] = useState() return (
{state}
) } ``` For more details, see the [Documentation](/docs/v4/custom-components/overview.md#client-components). 1. 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: ```diff // 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' + } + } } } ``` 2. For Globals, use the `admin.components.elements.Description` key: ```diff // 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' + } + } } } ``` 3. For Fields, use the `admin.components.Description` key: ```diff // 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' + } } } ``` 1. Array Field row labels and Collapsible Field label now _only_ accepts a React Component, and no longer accepts a plain string or record: ```diff // 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: [...] } ] } ``` 1. All default view keys are now camelcase: For example, for Root Views: ```diff // file: payload.config.ts import { buildConfig } from 'payload' export default buildConfig({ admin: { views: { - Account: ... + account: ... } }) ``` Or Document Views: ```diff // file: Collection.tsx import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { slug: 'my-collection', admin: { views: { - Edit: { - Default: ... - } + edit: { + default: ... + } } } } ``` 1. Custom Views within the config no longer accept React Components directly, instead, you must use their `Component` property: ```diff // 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: ```diff // 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' + } } } } } ``` 1. The `href` and `isActive` functions on View Tabs no longer includes the `match` or `location` arguments. This 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 off some hooks, i.e. `usePathname()` and run it through your own utility functions: ```diff // 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' + } + }, }, }, }, } ``` 1. The `admin.components.views[key].Tab.pillLabel` has been replaced with `admin.components.views[key].tab.Pill`: ```diff // 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', + } + }, }, }, }, } ``` 1. `react-i18n` was removed, the `Trans` component from `react-i18n` has been replaced with a Payload-provided solution: ```diff '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 and edit your password there." export const MyComponent = () => { return ( - - account - + , + }} + /> ) } ``` ## 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`](https://developer.mozilla.org/en-US/docs/Web/API/Response) instead of `res.json`, `res.send`, etc.: ```diff // 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, + }) + } + } ] } ``` 1. 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: ```diff // 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: ```diff 'use client' - import { useTitle } from 'payload' + import { useDocumentInfo } from '@payloadcms/ui' export const MyComponent = () => { - const title = useTitle() + const { title } = useDocumentInfo() // ... } ``` 1. 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). ```diff 'use client' import { useDocumentInfo } from '@payloadcms/ui' export const MyComponent = () => { const { - collection, - global, + collectionSlug, + globalSlug } = useDocumentInfo() // ... } ``` 1. 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. ```diff '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](/docs/v4/custom-components/overview.md#accessing-the-payload-config). 1. The `useCollapsible` hook has had slight changes to its property names. `collapsed` is now `isCollapsed` and `withinCollapsible` is now `isWithinCollapsible`. ```diff 'use client' import { useCollapsible } from '@payloadcms/ui' export const MyComponent = () => { - const { collapsed, withinCollapsible } = useCollapsible() + const { isCollapsed, isWithinCollapsible } = useCollapsible() } ``` 1. The `useTranslation` hook no longer takes any options, any translations using shorthand accessors will need to use the entire `group:key` ```diff 'use client' - import { useTranslation } from 'payload' + import { useTranslation } from '@payloadcms/ui' export const MyComponent = () => { - const { i18n, t } = useTranslation('general') + const { i18n, t } = useTranslation() - 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](/docs/v4/rich-text/custom-features.md). ## 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. # 3.0 to 4.0 Migration Guide Source: https://payloadcms.com/docs/beta/migration-guide/v4 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. ## Codemod Most of the breaking changes below are auto-migratable using the [`@payloadcms/codemod`](https://github.com/payloadcms/payload/tree/main/packages/codemod) CLI: ```bash npx @payloadcms/codemod ``` The codemod is idempotent and safe to run on a partially-migrated project. ## Breaking Changes ### Payload request creation is consolidated Payload request creation now uses `createPayloadRequest` for local contexts and `createPayloadRequestFromWebRequest` to convert a Web `Request`. Admin context setup is exposed as `initAdminContext`. | Old name | New name | | --------------------------------------- | ------------------------------------------- | | `createLocalReq` | `createPayloadRequest` | | `CreateLocalReqOptions` (internal type) | `Omit` | | `createPayloadRequest` | `createPayloadRequestFromWebRequest` | | `initReq` | `initAdminContext` | | `InitReqArgs` | `InitAdminContextArgs` | | `InitReqResult` | `AdminContext` | | `InitReqCache` | `AdminContextCache` | | `InitReqPartialResult` | `PartialAdminContext` | `initAdminContext` and its cache types are primarily contracts for framework adapters. Custom Root layout adapters must rename their injected `initReq` callback to `initAdminContext`. Local request creation now takes a single object argument containing `payload`. For a request without options: ```diff -import { createLocalReq } from 'payload' +import { createPayloadRequest } from 'payload' -const req = await createLocalReq({}, payload) +const req = await createPayloadRequest({ payload }) ``` `CreatePayloadRequestArgs` is exported from `payload` and requires `payload`. Options-only values should use `Omit`. The old `CreateLocalReqOptions` type was declared in the internal request utility, not exported from the package root; relative/private imports of that type require manual migration. The codemod handles an existing named import of that type only when its source is `payload`. Web request conversion uses its new role-specific name: ```diff -import { createPayloadRequest } from 'payload' +import { createPayloadRequestFromWebRequest } from 'payload' -const req = await createPayloadRequest({ config, request }) +const req = await createPayloadRequestFromWebRequest({ config, request }) ``` #### Codemod To migrate automatically, there's a codemod for this change available by running: ```bash npx @payloadcms/codemod --transform migrate-payload-request-creation ``` Automatic call migration requires a plain identifier payload argument and either `{}` or an initialized local `const` options identifier. The options type must expose all seven old option properties, or be `any`. For that identifier form, the generated argument captures payload first and uses forwarding getters for `context`, `depth`, `fallbackLocale`, `locale`, `req`, `urlSuffix`, and `user`. This preserves inherited getters, their read order, and rejected promises from throwing getters without reading unrelated properties. Complex expressions, mutable/unresolved options bindings, narrow option types, collisions, and unsupported type usages are left unchanged with notes for manual migration. ### JWT authentication is versioned without moving token data Payload 4 keeps the decoded JWT payload flat, so integrations can continue reading `id`, `collection`, `sid`, `email`, and fields selected by `saveToJWT` from the same paths as Payload 3. Payload now reserves `id`, `collection`, `email`, `sid`, `iat`, and `exp`. A `saveToJWT` alias using one of those names is ignored, and Payload writes the trusted value after collecting fields configured with `saveToJWT`. This prevents an alias such as `saveToJWT: 'id'` or `saveToJWT: 'collection'` from changing which user or collection the signed token authenticates as. The token's signed protected header now contains `authVersion: 1`. This marker identifies tokens issued by the fixed authentication format. It lives in the header, rather than the payload, because earlier configurations could alias a field to any payload key. Payload rejects tokens that do not contain the supported header version. ```ts // Decoded payload: unchanged paths { id: 'user-id', collection: 'users', sid: 'session-id', email: 'user@example.com', role: 'super-admin', // from saveToJWT iat: 1750000000, exp: 1750007200, } // Signed protected header { alg: 'HS256', typ: 'JWT', authVersion: 1, } ``` Tokens issued by a patched Payload 3 release already contain the same `authVersion: 1` protected-header marker and remain valid after upgrading to Payload 4, provided the signing secrets and authentication collection configuration remain compatible. Tokens issued by an unpatched Payload release do not contain the marker and are rejected. There is no safe compatibility fallback because a legacy token might have been signed after its trusted identity fields were overwritten. When upgrading directly from an unpatched release: - Users must sign in again after deployment. - Integrations must reissue stored bearer tokens. - Expect a temporary spike in `401` responses until clients discard their stored tokens. Raw-token consumers do not need to change token data paths, and `/me` returns the same data for a valid new token. External validators and integrations that mint Payload-compatible JWTs must set and check the protected-header version. Import Payload's shared version instead of hardcoding it: ```ts import { decodeProtectedHeader } from 'jose' import { JWT_AUTH_VERSION } from 'payload' if (decodeProtectedHeader(token).authVersion !== JWT_AUTH_VERSION) { throw new Error('Unsupported Payload token') } ``` A token is also rejected if its `collection` is not auth-enabled or has `disableLocalStrategy` enabled. See [Token Data](/docs/v4/authentication/token-data.md) for the full token data shape. ### Sessions track their refreshes Auth collections with `useSessions` (the default) now store two more fields on each session, `refreshCount` and `refreshedAt`. [Refresh tokens](/docs/v4/authentication/overview.md#refresh-tokens) use them to tell the latest refresh token apart from older ones. On SQL adapters this adds two columns to each `_sessions` table, so existing projects need a database migration. In development the schema is pushed automatically; in production, generate a migration with `payload migrate:create` and run `payload migrate`. Existing sessions keep working, since an empty `refreshCount` counts as `0`. MongoDB needs no migration. ### Local API access is now enforced by default #overrideaccess-now-defaults-to-false-on-local-api-operations `overrideAccess` now defaults to `false` on Local API operations, so omitting it respects Access Control instead. Operations that do not expose `overrideAccess`, and operations such as `resetPassword` where it was already required or did not default to `true`, are unaffected by this change. ```diff const posts = await payload.find({ collection: 'posts', + overrideAccess: true, // preserves the Payload 3 default }) ``` - `overrideAccess: false` (the new default) — respect Access Control. Use this whenever the operation acts on behalf of a user, and pass `user` alongside it. - `overrideAccess: true` — bypass Access Control. Use this for trusted server-side work such as cron jobs, seeding, and migrations. The property stays optional on the affected operations, and there is no compile error for either TypeScript or JavaScript projects — a call that omitted `overrideAccess` before still compiles after upgrading, it just behaves differently: it now enforces Access Control instead of skipping it. Run the codemod to find every affected call site and restore the previous behavior explicitly: ```bash npx @payloadcms/codemod ``` Review the results afterwards. Leave the property off, or set it to `overrideAccess: false`, wherever the operation acts on behalf of a user — that is the more secure outcome and is now the default. #### Cases the codemod leaves behind Run a typecheck after the codemod. Three patterns it cannot resolve: - **Spread arguments** — `payload.find({ ...args })`. The codemod cannot tell whether `args` already supplies the property. - **Aliased or cast receivers** — `const db = payload; db.find({ ... })` and `(payload as any).find({ ... })`. It matches `payload`, `.payload`, and their `.jobs` properties only. - **Non-literal arguments** — `payload.find(options)`. The codemod cannot safely modify the value at the call site. #### Jobs operations use the same default `payload.jobs.queue`, `run`, `runByID`, `cancel`, and `cancelByID` now also default `overrideAccess` to `false`. The option remains optional, but omitting it runs the corresponding `jobs.access.queue`, `jobs.access.run`, or `jobs.access.cancel` function. The codemod adds `overrideAccess: true` to these calls—including zero-argument `payload.jobs.run()` calls—to preserve Payload 3 behavior for review. ### On-demand validation adds a `validate` operation The `Operation` type now includes `validate`. The `operation` argument for collection, global, and field `beforeValidate` and `beforeChange` hooks also includes `validate`. On-demand validation runs these hooks without saving the candidate. Update exhaustive operation checks and skip external side effects that should only run during `create` or `update`: ```ts import type { CollectionBeforeChangeHook } from 'payload' const syncCustomer: CollectionBeforeChangeHook = async ({ data, operation, }) => { if (operation === 'validate') { return data } await syncCustomerWithStripe(data) return data } ``` Sanitized collection and global access types now require a `validate` key. Payload supplies this key during configuration sanitization and uses `update` access by default. Code that constructs a sanitized access object directly must add the key. ### Access Control callbacks now receive document slugs Collection and Global Access Control callback arguments now include the document's `slug`. This additive property is unlikely to affect most projects, but code that manually invokes access callbacks or `executeAccess` must now pass `slug`. Code that validates, enumerates, or compares the exact callback argument shape may also need updating. ### Custom bin scripts replaced by native CLI commands The `config.bin` API has been removed. Custom commands are now registered through `config.cli.commands` and use the same schema-backed CLI API as Payload's built-in commands. In Payload 3.x, config entries connected a command name to a script path: ```ts // payload.config.ts export default buildConfig({ bin: [ { key: 'seed', scriptPath: path.resolve(dirname, 'seed.ts'), }, ], }) ``` The imported module had to expose a specially named `script` function: ```ts // seed.ts import type { SanitizedConfig } from 'payload' import payload from 'payload' export const script = async (config: SanitizedConfig) => { await payload.init({ config }) // Seed the database... process.exit(0) } ``` In Payload 4.0, define a real CLI command instead: ```ts // seed.ts import { strictObject, z } from 'payload' import { defineCLICommand } from 'payload/cli' export const seedCommand = defineCLICommand({ description: 'Seed the database.', input: strictObject({ clear: z ._default(z.boolean(), false) .check(z.describe('Delete existing documents first.')), }), handler: async ({ args, getPayload }) => { const payload = await getPayload() if (args.clear) { // Delete existing documents... } // Seed the database... payload.logger.info('Successfully seeded!') }, }) ``` Register the command export in your Payload config: ```ts // payload.config.ts export default buildConfig({ cli: { commands: { seed: './seed.js#seedCommand', }, }, }) ``` When migrating custom scripts: - Replace `bin` entries with entries in the `cli.commands` map. The map key becomes the command name. - Replace the exported `script(config)` function with a schema-backed command created by `defineCLICommand`. - Register commands by import path to keep them out of the Payload config's module graph. Paths use the existing `PayloadComponent` syntax, resolve relative to `payload.config.ts`, and support default or named exports. Direct `CLICommand` values are also accepted. - Define the handler's semantic input with a Standard Schema implementation such as Zod. Supported top-level properties become options automatically; use `cli` only to mark positional arguments, hide fields, or override unusual shell syntax. - Use the provided `getPayload()` function so Payload can shut down database connections after the command finishes. - Return an exit code from the handler when needed. Do not call `process.exit()` from the command. Payload adds built-in command import references to `cli.commands` while sanitizing the config, then applies project entries over those defaults. All built-ins are named exports from `payload/cli/builtin`, and Payload imports a referenced module only once even when it provides several commands. The sanitized map therefore contains the complete local CLI, and built-in and custom commands use the same loader. Custom commands appear in `payload --help` and receive Standard Schema validation and generated shell help. A command with the same map key as a built-in command replaces it, while `false` disables that command. Set `cli: false` to disable the entire Payload CLI. Command names are case-sensitive. Because custom commands must be registered before the CLI parses input, the Payload config is now loaded before displaying root CLI help. See the [Payload CLI documentation](/docs/v4/configuration/cli.md) for the complete API. ### Legacy migration CLI export removed The undocumented `migrateCLI` export has been removed. It accepted an already-sanitized config, a minimist-shaped `parsedArgs` object, and a special `migrationDir` override. This duplicated migration command registration and kept the old argument format alive after the main CLI moved to Commander. If application code needs migration behavior rather than command-line parsing, call the database adapter directly through an initialized Payload instance: ```ts await payload.db.migrate() ``` The adapter also exposes the corresponding `migrateDown`, `migrateFresh`, `migrateRefresh`, `migrateReset`, `migrateStatus`, and `createMigration` methods. If you need to execute the real CLI from JavaScript, use the existing generic `payload.bin()` method. It starts the same CLI process a user would run in a terminal and returns its exit code: ```ts import payload from 'payload' const { code } = await payload.bin({ args: ['migrate:create', '--file', migrationPath], cwd: projectDirectory, }) ``` The spawned CLI discovers the Payload config from `cwd` or `PAYLOAD_CONFIG_PATH`. Configure a custom migration directory through your database adapter instead of passing it as a programmatic CLI override. ### Generator helpers now return result objects `generateTypes` now returns `{ types, outputFile, written }` instead of returning the generated source directly when using `returnString: true`. `generateImportMap` now returns `{ outputFile, written }`, and `payload.db.generateSchema()` returns `{ outputFile }`. Code that ignores these return values does not need to change. ### Default query depth has been reduced Queries that omit `depth` now populate one relationship level instead of two. This reduces database work and response size by default. If your application relies on two populated levels, prefer specifying `depth: 2` only on the queries that need it: ```ts const posts = await payload.find({ collection: 'posts', overrideAccess: true, depth: 2, }) ``` To preserve the Payload 3.x behavior application-wide, set `defaultDepth: 2`: ```diff export default buildConfig({ + defaultDepth: 2, }) ``` One place inherits `defaultDepth` without naming it: when localization is enabled, `@payloadcms/plugin-search` refetches the document without an explicit depth before handing `originalDoc` to your `beforeSync` hook. Read only first-level relationships there, or refetch yourself with the depth you need. ### Versions are now enabled by default for all collections and globals Collections and globals now have `versions: true` by default (`maxPerDoc`/`max: 100`, drafts disabled). In previous versions you had to opt in; you now opt out. **What changes:** - Any collection or global without an explicit `versions` property will automatically get a `__versions` table or collection in the database. You will need to run a database migration to add these new tables for existing projects. - Auth collections such as Users do **not** get versions by default — the built-in user collection already ships with `versions: false`, and you should do the same for any custom auth collection. - The bare `versions: true` you may have added previously is now redundant and can be removed. **To opt out for an existing collection or global:** ```diff export const Posts: CollectionConfig = { slug: 'posts', fields: [], + versions: false, } ``` **Two codemods are available** to automate this migration: ```bash # 1. Preserve existing behaviour: add versions: false everywhere versions is not set npx @payloadcms/codemod --transform migrate-versions-default # 2. Remove now-redundant versions: true (bare boolean) from your configs npx @payloadcms/codemod --transform remove-versions-true ``` Run `migrate-versions-default` if you want zero schema changes — it adds `versions: false` to every `CollectionConfig` and `GlobalConfig` that does not already set `versions`. If you want to selectively keep versions on some configs, run the codemod and then delete `versions: false` from the ones you want to enable. Each config you opt into requires a database migration. Run `remove-versions-true` to clean up any bare `versions: true` that is now the default. ### Version read access inherits collection and global read access When `readVersions` is not configured, version access now inherits the Collection or Global `read` Access Control instead of requiring an authenticated user by default. This means a public `read` function can make historical and unpublished versions publicly readable, while a query constraint returned by `read` is also applied to version documents. If you want to preserve the previous authenticated-only behavior, configure `readVersions` explicitly: ```ts export const Posts: CollectionConfig = { slug: 'posts', access: { read: () => true, readVersions: ({ req: { user } }) => Boolean(user), }, fields: [], } ``` ### Authorship is now enabled by default Collections and globals now track authorship by default: Payload injects `createdBy` and `updatedBy` polymorphic relationship fields (related to every auth-enabled collection) and stamps them from `req.user` on each write. Previously there was no authorship tracking; you now opt out with `authorship: false`. **What changes:** - Every non-internal collection and global gains `createdBy` and `updatedBy` when your config has at least one auth collection. On SQL adapters this adds relationship rows (stored in each entity's `_rels` table), so existing projects need a database migration. In development the schema is pushed automatically; in production, generate a migration with `payload migrate:create` and run `payload migrate`. Existing documents are backfilled with `null`. - `createdBy` is only derived on a document's first write, so editing a document whose `createdBy` is `null` (for example a migrated or anonymously created document) does not backfill the editor as the creator. - Payload's internal system collections and globals are excluded automatically: `payload-jobs`, `payload-jobs-stats`, `payload-preferences`, `payload-locked-documents`, `payload-migrations`, `payload-query-presets`, and the KV collection. **Plugins:** plugin-injected collections also receive authorship unless the plugin opts out. In this release: - **Opted out** (no authorship): `@payloadcms/plugin-search`, `@payloadcms/payload-cloud`, and all `@payloadcms/plugin-ecommerce` collections. - **Kept on** (they gain `createdBy` / `updatedBy`): `@payloadcms/plugin-redirects`, `@payloadcms/plugin-form-builder` (forms and form submissions), and `@payloadcms/plugin-import-export` (imports and exports). Existing projects using these plugins on a SQL adapter need a migration for those collections too. To opt one out, set `authorship: false` through that plugin's collection-override option. **To opt out for an existing collection or global:** ```diff export const Posts: CollectionConfig = { slug: 'posts', fields: [], + authorship: false, } ``` **A codemod is available** to preserve the previous behaviour: ```bash npx @payloadcms/codemod --transform migrate-authorship-default ``` `migrate-authorship-default` adds `authorship: false` to every `CollectionConfig` and `GlobalConfig` in your source that does not already set `authorship`, so your own collections have zero schema changes. It only rewrites your config files — it cannot reach plugin-injected collections, so any plugin that keeps authorship on (above) still gains the fields and requires a migration (or an explicit `authorship: false` override). ### List View Select API is now the default The `admin.enableListViewSelectAPI` Collection Config property has been removed. The List View now always uses the [Select API](/docs/v4/queries/select.md) to query only the active columns, which was previously opt-in. If you previously set `admin.enableListViewSelectAPI: true`, remove the property — the behavior is now default. The `migrate-list-view-select-api` transform in `@payloadcms/codemod` handles this automatically. ### Global component slots now match Collections Global configs now use the same `admin.components` shape as Collections. The Edit View slot container is renamed from `elements` to `edit`, the `Description` slot is hoisted to top-level `admin.components.Description`, and `editMenuItems` is now supported. ```diff export const Header: GlobalConfig = { slug: 'header', admin: { components: { - elements: { - SaveButton: '/path/to/CustomSaveButton', - Description: '/path/to/CustomDescription', - }, + Description: '/path/to/CustomDescription', + edit: { + SaveButton: '/path/to/CustomSaveButton', + }, }, }, } ``` The available slots under `admin.components.edit` for Globals now match Collections (`beforeDocumentControls`, `editMenuItems`, `PreviewButton`, `PublishButton`, `SaveButton`, `SaveDraftButton`, `Status`, `UnpublishButton`). `Upload` remains Collection-only. Globals also now support custom views with arbitrary keys under `admin.components.views`, mirroring Collections. This change is auto-migrated by the `globals-components-edit` codemod. ### Dynamic selection replaces static forced selection The static `forceSelect` config has been removed. Each Collection and Global now accepts a top-level `select` function that receives the current `operation`, `req`, and the caller's `select`, and returns the final `select` to use. Returning `undefined` leaves the caller's `select` unchanged. The new function form **replaces** the caller's select rather than deep-merging into it. To preserve the previous behavior of always forcing certain fields, spread the caller's `select` into the returned object yourself. ```diff // collections/Posts.ts import type { CollectionConfig } from 'payload' export const PostsCollection: CollectionConfig = { slug: 'posts', - forceSelect: { - title: true, - slug: true, - }, + select: ({ select }) => (select ? { ...select, title: true, slug: true } : undefined), fields: [], } ``` The same change applies to Globals. Run `npx @payloadcms/codemod --transform migrate-force-select` to migrate automatically. The codemod handles object-literal `forceSelect` values, including nested ones (which previously deep-merged) — those are rewritten to call `deepMergeSimple` from `payload/shared`, with the import added automatically. Non-literal values, configs that already define a sibling `select`, and unsupported member kinds are surfaced as notes for manual review. #### API tab visibility now uses a condition `admin.hideAPIURL` has been removed from Collections and Globals. Use the existing `admin.components.views.edit.api.tab.condition` to hide the API tab instead. ```diff // collections/Posts.ts import type { CollectionConfig } from 'payload' export const PostsCollection: CollectionConfig = { slug: 'posts', admin: { - hideAPIURL: true, + components: { + views: { + edit: { + api: { + tab: { + condition: () => false, + }, + }, + }, + }, + }, }, fields: [], } ``` Run `npx @payloadcms/codemod --transform remove-hide-api-url` to migrate automatically. ### Aliased UI and Next.js re-exports have been removed Several types and utilities that were re-exported from `@payloadcms/ui` and `@payloadcms/next/utilities` for backwards compatibility have been removed. The canonical exports live in `payload` and `payload/shared`; import directly from there. **Pass-through re-exports** — same name, new source: | Symbol | Old source | New source | | ----------------------------- | ---------------------------- | ------------------------------------------------- | | `Column` | `@payloadcms/ui` | `payload` | | `ListViewSlots` | `@payloadcms/ui` | `payload` | | `ListViewClientProps` | `@payloadcms/ui` | `payload` | | `EntityType` | `@payloadcms/ui/shared` | `payload/shared` | | `formatAdminURL` | `@payloadcms/ui/shared` | `payload/shared` | | `mergeListSearchAndWhere` | `@payloadcms/ui/shared` | `payload/shared` | | `mergeHeaders` | `@payloadcms/next/utilities` | `payload` | | `headersWithCors` | `@payloadcms/next/utilities` | `payload` | | `createPayloadRequest` | `@payloadcms/next/utilities` | `createPayloadRequestFromWebRequest` in `payload` | | `addDataAndFileToRequest` | `@payloadcms/next/utilities` | `payload` | | `sanitizeLocales` | `@payloadcms/next/utilities` | `payload` | | `addLocalesToRequestFromData` | `@payloadcms/next/utilities` | `payload` | **Renamed types** — use the new canonical name from `payload`: | Old name | New name | Source | | -------------------------- | ----------------------- | --------- | | `ListPreferences` | `CollectionPreferences` | `payload` | | `ListComponentClientProps` | `ListViewClientProps` | `payload` | | `ListComponentServerProps` | `ListViewServerProps` | `payload` | ```diff - import type { Column, ListViewSlots, ListPreferences } from '@payloadcms/ui' - import { EntityType, formatAdminURL, mergeListSearchAndWhere } from '@payloadcms/ui/shared' - import { headersWithCors, mergeHeaders } from '@payloadcms/next/utilities' + import type { Column, ListViewSlots, CollectionPreferences } from 'payload' + import { EntityType, formatAdminURL, mergeListSearchAndWhere } from 'payload/shared' + import { headersWithCors, mergeHeaders } from 'payload' ``` Running `npx @payloadcms/codemod --transform migrate-aliased-exports` preserves old local bindings by importing renamed symbols with an `as` alias (e.g. `import type { CollectionPreferences as ListPreferences } from 'payload'` or `import { createPayloadRequestFromWebRequest as createPayloadRequest } from 'payload'`). Run `migrate-payload-request-creation` to complete the request helper's identifier and call-site migration, or drop the other aliases and rename their usages manually if you want to fully commit to the new names. ### Field component types replaced by props types Field component aliases that wrapped props in `React.ComponentType` have been removed. Import the props type directly so each component can use `React.FC` or a function declaration. ```diff - import type { TextFieldClientComponent } from 'payload' + import type { TextFieldClientProps } from 'payload' + import type React from 'react' - export const CustomTextField: TextFieldClientComponent = (props) => { + export const CustomTextField: React.FC = (props) => { // ... } ``` Generic type arguments move to the props type: ```diff - import type { FieldErrorClientComponent, TextFieldClient } from 'payload' + import type { FieldErrorClientProps, TextFieldClient } from 'payload' + import type React from 'react' - const Error: FieldErrorClientComponent = (props) => null + const Error: React.FC> = (props) => null ``` Function declarations can annotate their parameter without a component wrapper: ```tsx import type { TextFieldServerProps } from 'payload' export default async function CustomTextField(props: TextFieldServerProps) { // ... } ``` This covers main field components, labels, descriptions, errors, diffs, and block row labels. Admin-view types such as `AdminViewComponent` and `DocumentTabComponent` are separate. #### Codemod To migrate automatically, there's a codemod for this change available by running: ```bash npx @payloadcms/codemod --transform migrate-field-component-types ``` ### Diff component props `comparisonValue` and `versionValue` renamed to `valueFrom` and `valueTo` Custom Diff Components (`admin.components.Diff`) receive the two field values being compared as `valueFrom` and `valueTo`. The `comparisonValue` and `versionValue` props have been removed from `FieldDiffClientProps` and `FieldDiffServerProps`, including every field-specific variant such as `TextFieldDiffClientProps`. | Old prop | New prop | Description | | ----------------- | ----------- | ------------------------------------------------ | | `comparisonValue` | `valueFrom` | Field value from the version being compared from | | `versionValue` | `valueTo` | Field value from the version being compared to | ```diff import type { TextFieldDiffClientProps } from 'payload' import type React from 'react' export const MyTextDiff: React.FC = ({ - comparisonValue, - versionValue, + valueFrom, + valueTo, }) => { // ... } ``` #### Codemod To migrate automatically, there's a codemod for this change available by running: ```bash npx @payloadcms/codemod --transform rename-diff-value-props ``` The codemod updates functions whose props are typed with a Diff props or component type imported from `payload` (e.g. `TextFieldDiffClientProps`), or with a type in the same file built from one. It renames destructured props and reads such as `props.comparisonValue`. Untyped components and props passed on to other components (e.g. ``) aren't changed automatically; the codemod prints a note for each leftover in files that import a Diff type. ### Default value functions can now return promises ([#17522](https://github.com/payloadcms/payload/pull/17522)) The exported `DefaultValue` type's function variant previously declared a synchronous-only return type (`SerializableValue`), even though Payload has always awaited the result of a `defaultValue` function at runtime. The type now correctly reflects this and allows `Promise | SerializableValue`. **What changes:** - Field-level `defaultValue` functions can now be typed as `async` without a TypeScript error. This was always safe at runtime — only the type was too strict. - If you import `DefaultValue` to type your own function or variable and then call it synchronously assuming the result is never a `Promise`, you'll need to update that code to handle the `Promise` branch. ```diff import type { DefaultValue } from 'payload' - const resolveDefault = (fn: DefaultValue, args: Args) => { - const value = fn(args) - return value.toString() - } + const resolveDefault = async (fn: DefaultValue, args: Args) => { + const value = await fn(args) + return value.toString() + } ``` If you only use `DefaultValue` to type a `defaultValue` property passed into a Collection, Global, or Field config, no changes are needed. ### Sanitized collection and auth config types now match runtime defaults `SanitizedCollectionConfig` and its auth type no longer use `DeepRequired`. This reduces TypeScript checker work and makes optionality match runtime defaults. Properties Payload does not default, such as `auth.cookies.domain`, `auth.useAPIKey`, and `auth.depth`, now require undefined handling: ```ts const useAPIKey = collection.auth.useAPIKey ?? false ``` Properties sanitization does fill, including standard access functions, hook arrays, cookie defaults, and normalized auth options, are now required. Use `CollectionConfig` or `IncomingAuthType` for input and `SanitizedCollectionConfig` after sanitization. ### Sanitized root config types now match runtime defaults `SanitizedConfig` no longer uses `DeepRequired`. This reduces TypeScript checker work and preserves optionality for nested values Payload does not populate, such as `admin.autoLogin`, `graphQL.mutations`, `queryPresets`, and `typescript.postProcess`. Code that consumes these properties from `payload.config` may now need to handle `undefined`: ```ts const autoLogin = payload.config.admin.autoLogin const presets = payload.config.queryPresets?.constraints ``` Properties filled during sanitization, including `auth.jwtOrder`, root routes, GraphQL defaults, TypeScript output settings, and admin dashboard defaults, remain required. `Config.auth.jwtOrder` is now optional for incoming configs because Payload supplies its documented default. The unused `SanitizedConfig.paths` property was also removed because it is not present at runtime. ### Sanitized global config types now match runtime defaults `SanitizedGlobalConfig` no longer uses `DeepRequired`. This reduces TypeScript checker work and preserves optionality for values global sanitization does not populate, such as `admin.components`, `graphQL`, `lockDocuments`, and `typescript`. Code that consumes these properties may now need to handle `undefined`: ```ts const views = global.admin.components?.views const interfaceName = global.typescript?.interface ``` Properties filled during sanitization, including `_sanitized`, `admin`, `custom`, `label`, standard access functions, hook arrays, endpoints, and flattened fields, remain required. Use `GlobalConfig` for input and `SanitizedGlobalConfig` after sanitization. ### User types have been consolidated Payload's user types have been consolidated so each one has a clear job. This completes a cleanup already planned in 3.x: `UntypedUser` was deprecated for removal in 4.0, and `TypedUser` was marked to be renamed to `User` in 4.0. Previously, the public `User` type was an alias of the loose `UntypedUser`, while `TypedUser` was the generated type for auth-enabled collections. `ClientUser` was also loose, and the runtime auth fields `_strategy` and `_sid` were not typed on `req.user`. There are now two types: - **`User`** — the generated type for a user of an auth-enabled collection (what `TypedUser` used to be). Without generated types, it falls back to a documented shape containing Payload's built-in auth fields. - **`AuthenticatedUser`** — `User` plus the optional runtime auth fields `_strategy` and `_sid`. This is what `req.user`, `payload.auth()`, auth strategies, `me` responses, and `useAuth().user` return. `AuthenticatedUser` is intentionally shared by server and client auth APIs. It describes the signed-in identity, not an exact serialization or sanitization contract: runtime, hidden, access-controlled, or unselected fields can be absent from individual responses. Use `User` for other stored or read user documents, including user documents rendered in client code. `_sid` is flow-dependent: request-authenticated users can carry it, while standard login, `me`, and refresh responses omit it. The `collection` property remains available on authenticated users and in client auth responses. Removing `ClientUser` does not change runtime serialization. **What changes:** - `TypedUser` is removed — replace it with `User`. - `UntypedUser` is removed — use `User` for a user document or `AuthenticatedUser` for the signed-in user. - `ClientUser` is removed — replace it with `AuthenticatedUser` for `useAuth().user`, `me` responses, and other client auth APIs. - `User` and `AuthenticatedUser` no longer have a `{ [key: string]: any }` index signature. Custom auth-collection fields require generated types or an explicit augmented type or cast. - `req.user` and `payload.auth().user` are now typed as `AuthenticatedUser`, so `_strategy` and `_sid` type-check. - The deprecated top-level `strategy` field is removed from `me` and `refresh` responses (REST, GraphQL, and the SDK). Read `user._strategy` instead. - `@payloadcms/plugin-ecommerce`'s `ClientUserWithCart` is removed — replace it with `UserWithCart`. - `MeOperationResult.user` and `refreshCookieAsync()` can be `null`, matching their existing runtime behavior. `refreshCookieAsync()` also preserves the custom user type passed to `useAuth()`. - Collection `admin.hidden` callbacks now receive `PayloadRequest['user']`, including `null` when there is no authenticated user. - The Local API `user` option is now `User | null` instead of the loose `Document` type for collection `count`, `create`, `delete`, `duplicate`, `find`, `findByID`, `findDistinct`, and `update`; collection and global version operations; and global `findOne` and `update`. - `UserSession.createdAt` is now optional and nullable (`createdAt?: Date | null | string`) to match the generated session shape. Callers must handle a missing value. `AuthenticatedUser` is assignable to `User`, so passing `req.user` to Local API operations continues to work. Passing an arbitrary object now type-errors. ```diff - import type { ClientUser, TypedUser, UntypedUser } from 'payload' + import type { AuthenticatedUser, User } from 'payload' - const user: TypedUser = req.user + const user: AuthenticatedUser = req.user - type CurrentUser = ClientUser + type CurrentUser = AuthenticatedUser ``` Use `User` for stored or read user documents. Use `AuthenticatedUser` when code specifically receives the signed-in user from `req.user`, `payload.auth()`, an auth strategy, a `me` response, or `useAuth()`. **For plugin authors:** because `User` no longer has an index signature, reading a field your plugin adds to the user collection off `req.user` (or `User`) no longer type-checks. Declare an augmented type and cast `req.user` to it — this is what the first-party plugins now do (e.g. `@payloadcms/plugin-ecommerce` for its `cart` join field): ```ts import type { AuthenticatedUser } from 'payload' // Your plugin adds a `cart` join field (or any custom field) to the user collection. type UserWithCart = AuthenticatedUser & { cart?: { docs?: (Cart | string)[] } | null } const user = req.user as null | UserWithCart user?.cart?.docs // now typed ``` If the field name is dynamic (configurable at runtime), index through `Record` instead: ```ts const value = (user as Record)[fieldNameFromConfig] ``` ### Plugins now infer generated types automatically The `` type parameter on `multiTenantPlugin` and `searchPlugin` — and on `MultiTenantPluginConfig` / `SearchPluginConfig` — has been removed. It only existed to override the `user` / `locale` types, which Payload now infers from your generated types automatically: `userHasAccessToAllTenants` receives the generated `User`, and `search`'s `skipSync` callback receives `TypedLocale | undefined`. Drop the type argument — your callbacks are typed correctly without it. ```diff - multiTenantPlugin<{ user: MyUser }>({ ... }) + multiTenantPlugin({ ... }) ``` ### Plugin definitions now require a slug and named options The experimental `definePlugin` helper now requires a `slug` and passes plugin options through a named `options` argument: - **`slug` is now required.** It registers the plugin in the `plugins` map for cross-plugin discovery. Add one to every `definePlugin` call. - **Options are a named `options` argument.** They were previously spread into the `plugin` callback's args alongside `config` and `plugins`; they're now under a single `options` property. `TOptions` is also no longer constrained to `Record`, so `interface` and generic option types work. ```diff export const myPlugin = definePlugin({ - // slug was optional + slug: 'my-plugin', - plugin: ({ config, plugins, collections }) => ({ ...config }), + plugin: ({ config, options }) => ({ ...config }), }) ``` ### Admin routing is now framework-agnostic `@payloadcms/ui` no longer depends on `next` directly. Custom admin components (field components, custom views, plugins) that previously imported router hooks or the `Link` component from `next/*` must now import the framework-agnostic equivalents from `@payloadcms/ui`. Behavior is identical when running on Next.js — the `NextRouterAdapter` shipped by `@payloadcms/next` wires `next/navigation` hooks and `next/link` into the same context — but UI code can now also run on non-Next adapters such as TanStack Start without modification. **Hooks** — import from `@payloadcms/ui` instead of `next/navigation`: | Old import | New import | | --------------------------------------------------- | -------------------------------------------------- | | `import { useRouter } from 'next/navigation'` | `import { useRouter } from '@payloadcms/ui'` | | `import { usePathname } from 'next/navigation'` | `import { usePathname } from '@payloadcms/ui'` | | `import { useSearchParams } from 'next/navigation'` | `import { useSearchParams } from '@payloadcms/ui'` | | `import { useParams } from 'next/navigation'` | `import { useParams } from '@payloadcms/ui'` | **`Link` component** — use `PayloadLink` from `@payloadcms/ui`: ```diff - import Link from 'next/link' + import { PayloadLink as Link } from '@payloadcms/ui' ``` **Types** — `LinkProps` from `next/link` is replaced by `LinkAdapterProps` from `payload`: ```diff - import type { LinkProps } from 'next/link' + import type { LinkAdapterProps } from 'payload' ``` `LinkAdapterProps` is a framework-neutral shape: `href: string`, optional `prefetch` / `replace` / `scroll` / `ref`, plus standard `AnchorHTMLAttributes` (minus `href`). The Next adapter forwards these to `next/link`. **`RouterAdapterRouter` shape** — the object returned by `useRouter()` now has a stable, framework-agnostic surface: ```ts type RouterAdapterRouter = { back: () => void push: (path: string, options?: { scroll?: boolean }) => void refresh: () => void replace: (path: string, options?: { scroll?: boolean }) => void } ``` If you were relying on Next-specific extras like `prefetch()` on the router instance, switch to the `Link` component's `prefetch` prop instead. **Custom apps embedding `RootProvider`** — `RootProvider` now requires a `RouterAdapter` prop. Apps using the default Next.js admin layout get this wired automatically. If you assemble `RootProvider` yourself, pass the framework's adapter component: ```diff + import { NextRouterAdapter } from '@payloadcms/next/elements/RouterAdapter' ``` ### Next.js subpath exports have moved to the UI package The `./client`, `./rsc`, and `./templates` subpath exports on `@payloadcms/next` were thin re-exports of components that now live in `@payloadcms/ui`. All admin elements (DocumentHeader, FormHeader, HierarchyTypeField, Logo, Nav) and templates (DefaultTemplate, MinimalTemplate) have been relocated to `@payloadcms/ui` and no longer depend on `next/*` modules. Import them directly from the canonical source. | Symbol | Old source | New source | | -------------------------- | ---------------------------- | -------------------- | | `DefaultTemplate` | `@payloadcms/next/templates` | `@payloadcms/ui/rsc` | | `MinimalTemplate` | `@payloadcms/next/templates` | `@payloadcms/ui/rsc` | | `CollectionCards` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` | | `DefaultNav` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` | | `DocumentHeader` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` | | `HierarchyTypeFieldServer` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` | | `Logo` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` | | `DefaultNavClient` | `@payloadcms/next/client` | `@payloadcms/ui` | | `HierarchyTypeField` | `@payloadcms/next/client` | `@payloadcms/ui` | | `NavSidebarToggle` | `@payloadcms/next/client` | `@payloadcms/ui` | | `NavWrapper` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsAccessCell` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsColumnField` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsColumnsCell` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsGroupByCell` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsGroupByField` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsWhereCell` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsWhereField` | `@payloadcms/next/client` | `@payloadcms/ui` | | `SlugField` | `@payloadcms/next/client` | `@payloadcms/ui` | ```diff - import { DefaultTemplate, MinimalTemplate } from '@payloadcms/next/templates' - import { DocumentHeader, Logo } from '@payloadcms/next/rsc' - import { HierarchyTypeField, SlugField } from '@payloadcms/next/client' + import { DefaultTemplate, DocumentHeader, Logo, MinimalTemplate } from '@payloadcms/ui/rsc' + import { HierarchyTypeField, SlugField } from '@payloadcms/ui' ``` If you reference any of these components by string path in your Payload config (e.g. `Component: '@payloadcms/next/rsc#CollectionCards'`), update the path string too — then regenerate the import map with `payload generate:importmap`. Run `npx @payloadcms/codemod --transform migrate-next-subpath-exports` to migrate automatically. It rewrites both `import` declarations and string component paths. ### Document title state has moved to its own hook For performance reasons, the document title state has been split out of `DocumentInfoContext` into its own `DocumentTitleContext`. Access it through the `useDocumentTitle` hook, which exposes the same `title` and `setDocumentTitle` API. Components that subscribed to `useDocumentInfo` solely for the title will no longer re-render when unrelated document state changes. ```diff 'use client' - import { useDocumentInfo } from '@payloadcms/ui' + import { useDocumentTitle } from '@payloadcms/ui' const Heading: React.FC = () => { - const { title, setDocumentTitle } = useDocumentInfo() + const { title, setDocumentTitle } = useDocumentTitle() return

{title}

} ``` If you only used `useDocumentInfo` for `title` / `setDocumentTitle`, drop the import entirely. If you used it for other properties as well, leave the original `useDocumentInfo()` call in place and add a separate `useDocumentTitle()` call alongside it. Run `npx @payloadcms/codemod --transform migrate-document-title-context` to migrate automatically. The codemod splits mixed destructures, removes the `useDocumentInfo` import when no other properties are still used, and preserves any rename aliases (e.g. `{ title: docTitle }`). ### Locale hooks can now return null The return type of `useLocale` is now `Locale | null`. It returns `null` when localization is disabled or configured without any locales. In Payload 3, the hook was typed as `Locale` and returned an empty object when localization was unavailable. Custom Client Components that read locale properties directly must handle the nullable value: ```diff 'use client' import { useLocale } from '@payloadcms/ui' const Greeting: React.FC = () => { - const { code } = useLocale() + const locale = useLocale() + + if (!locale) { + return null + } + + const { code } = locale return {code} } ``` When a missing locale is already valid for the receiving API, optional chaining is sufficient: ```ts const locale = useLocale()?.code ``` ### `readOnly` removed from the `useField` return type The deprecated `readOnly` property has been removed from `FieldType`, the object returned by `useField`. `useField` already stopped returning it in 3.x, so it was always `undefined` at runtime. Read `readOnly` from your field component's props instead: ```diff 'use client' import type { TextFieldClientComponent } from 'payload' import { useField } from '@payloadcms/ui' -export const MyField: TextFieldClientComponent = ({ path }) => { - const { readOnly, value } = useField({ path }) +export const MyField: TextFieldClientComponent = ({ path, readOnly }) => { + const { value } = useField({ path }) ``` ### Deprecated `TableColumnsProvider` props removed The following deprecated props have been removed from `TableColumnsProvider` and its `TableColumnsProviderProps` type: - `docs` - `enableRowSelections` - `listPreferences` - `preferenceKey` - `renderRowTypes` - `setTable` - `sortColumnProps` - `tableAppearance` `TableColumnsProvider` has ignored these props since 3.26.0, when column state moved to the URL. Remove them where you render the provider. It now only accepts `children`, `collectionSlug`, `columnState`, and `LinkedCellOverride`. ```diff {children} ``` ### Rich text adapters no longer expose translations The `i18n` property on `RichTextAdapter` (and its return type from adapter provider functions) has been removed. It was deprecated in v3 in favour of merging translations directly into `config.i18n.translations` inside the adapter provider. If you authored a custom rich-text adapter that returned an `i18n` object, move that logic into the adapter provider function and merge the translations into the config yourself: ```diff // my-richtext-adapter/index.ts export const myRichTextAdapter = (): RichTextAdapterProvider => { return ({ config }) => { const myTranslations = { en: { myKey: 'My translation' } } - return { - // ... other adapter properties - i18n: myTranslations, - } + config.i18n.translations = deepMergeSimple(config.i18n.translations, myTranslations) + return { + // ... other adapter properties + } } } ``` ### Config and rich-text sanitization are now synchronous Config and field sanitizers now return their result directly instead of returning a promise. This affects projects that call `sanitizeConfig`, `sanitizeField`, or `sanitizeFields` from `payload` directly: ```diff - const sanitizedConfig = await sanitizeConfig(config) + const sanitizedConfig = sanitizeConfig(config) ``` The same change applies to `sanitizeServerEditorConfig` and the methods on `editorConfigFactory` from `@payloadcms/richtext-lexical`. Remove `await`, and replace any `.then()` or `.catch()` usage with a direct call and `try`/`catch`. The optional `richTextSanitizationPromises` argument on the field sanitizers has been renamed to `richTextSanitizers`. Its callbacks are now synchronous: ```diff - richTextSanitizationPromises: Array<(config: SanitizedConfig) => Promise> + richTextSanitizers: Array<(config: SanitizedConfig) => void> ``` Custom rich-text adapter providers and Lexical server feature callbacks must also return their value synchronously: ```diff export const MyFeature = createServerFeature({ key: 'my-feature', - feature: async () => ({ + feature: () => ({ ClientFeature: '@my-package/client#MyFeature', sanitizedServerFeatureProps: null, }), }) ``` If an affected adapter or feature performs asynchronous work, move that work to an asynchronous plugin before config sanitization or to a runtime hook. Projects that only use `buildConfig(...)` and `lexicalEditor(...)` normally are not affected; `buildConfig` remains asynchronous. ### Transaction requests now use a single identifier property The `transactionIDPromise` property on `PayloadRequest` has been removed. It was deprecated and unused — `transactionID` (which already supports `Promise`) is the correct property to use for both resolved values and pending transactions. ### `headers` and `languageCode` removed from `AdminContext` The `headers` and `languageCode` properties have been removed from `AdminContext`, the object returned by `initAdminContext`. Both duplicated values that are already available on `req`: ```diff const context = await initAdminContext(args) -const userAgent = context.headers.get('user-agent') +const userAgent = context.req.headers.get('user-agent') -const languageCode = context.languageCode +const languageCode = context.req.i18n.language ``` `req.i18n.language` is typed as `string`. Cast it to `AcceptedLanguages` if you need the narrower type. `languageCode` has also been removed from `PartialAdminContext`. Use `i18n.language` instead. `getRootLayoutData` from `@payloadcms/ui` no longer accepts `headers` or `languageCode` and reads both from `req`. #### Codemod To migrate automatically, run: ```bash npx @payloadcms/codemod --transform migrate-admin-context-properties ``` The codemod rewrites property reads and destructures on `initAdminContext()` results and on parameters or variables typed as `AdminContext` or `PartialAdminContext`. It also updates `Pick` and `AdminContext['...']` types and drops both arguments from `getRootLayoutData()` calls. Element access such as `context['headers']`, and contexts passed through your own wrapper functions, are left unchanged for manual migration. ### `DiffMethod` type removed The deprecated `DiffMethod` type has been removed from `payload`. It mirrored an enum from `react-diff-viewer-continued`, which Payload no longer depends on. Remove any imports of `DiffMethod`. ### `diffMethod` prop removed from Diff components Custom Diff components (`admin.components.Diff`) no longer receive a `diffMethod` prop. It has been removed from `FieldDiffClientProps` and every field-specific variant like `TextFieldDiffClientProps`. Payload always passed the same `'diffWordsWithSpace'` value, so the prop carried no information. Each Diff component handles its own diffing. Remove `diffMethod` from your props. If you passed it on to a diffing utility, pass `'diffWordsWithSpace'` directly instead: ```diff export const MyTextDiff: React.FC = ({ - diffMethod, valueFrom, valueTo, }) => { ``` ### Collection after-operation hooks no longer report read operations The deprecated `'read'` value for the `operation` argument of collection `afterOperation` hooks has been removed. Payload has not dispatched `afterOperation` with `operation: 'read'` for some time — the `find` and `findByID` operations report their own names (`'find'` and `'findByID'`) instead. If a custom `afterOperation` hook branches on `operation === 'read'`, split it to handle `'find'` and `'findByID'`: ```diff const afterOperation: CollectionAfterOperationHook = ({ operation, result }) => { - if (operation === 'read') { + if (operation === 'find' || operation === 'findByID') { // ... } return result } ``` `'find'` resolves the paginated result (`result.docs`), while `'findByID'` resolves a single document — narrow on the specific operation if your logic depends on the result shape. Run `npx @payloadcms/codemod --transform migrate-after-operation-read` to migrate inline hooks automatically. The codemod rewrites `operation === 'read'` (and `!==`/`==`/`!=`) comparisons against a destructured, aliased, or property-accessed `operation` argument. Non-inline hooks (hooks referenced by name) and `switch` statements with a `'read'` case are surfaced as notes for manual review. The `operation: 'read'` value on the `beforeOperation` hook is unchanged. ### Static configuration defaults have been removed ([#17103](https://github.com/payloadcms/payload/pull/17103)) The `defaults` object exported from `payload` has been removed. It was deprecated because it was a single shared object and mutating it (or any config that used it as a base) leaked changes into every other consumer of the defaults. If you depended on reading a specific default value at runtime, read it from the sanitized config returned by buildConfig (or payload.config) instead of from the static defaults object. Alternatively, you can call `addDefaultsToConfig` to get the unsanitized config but populated with the default properties. ### Import and export field callbacks have moved to hooks The `toCSV` and `fromCSV` field options in `custom['plugin-import-export']` have been removed. Use `hooks.beforeExport` and `hooks.beforeImport` instead — they work for both CSV and JSON formats. The argument shapes differ slightly from the old API: | Old (`toCSV`) | New (`hooks.beforeExport`) | | ------------- | ----------------------------------- | | `row` | `siblingData` | | `doc` | `data` | | `data` | _(removed — was alias for `row`)_ | | — | `format` (new: `'csv'` \| `'json'`) | The `fromCSV` → `hooks.beforeImport` change is non-breaking for the `data` parameter — both receive the full flat row. ```diff { name: 'author', type: 'relationship', relationTo: 'users', custom: { 'plugin-import-export': { - toCSV: ({ value, columnName, row, doc }) => { - row[`${columnName}_id`] = (value as any).id - return doc.title - }, - fromCSV: ({ value, data }) => data[`${value}_key`], + hooks: { + beforeExport: ({ value, columnName, siblingData, data }) => { + siblingData[`${columnName}_id`] = (value as any).id + return data.title + }, + beforeImport: ({ value, data }) => data[`${value}_key`], + }, }, }, } ``` Run `npx @payloadcms/codemod --transform migrate-import-export-hooks` to migrate automatically. The codemod moves the function values into the correct hook positions and merges with any existing `hooks` object. Review argument names in the migrated functions — `row` references must be renamed to `siblingData` and `doc` references renamed to `data`. ### Database adapter types now use the main package exports The `/types` subpath export has been removed from all Drizzle-based database adapter packages. All types are available directly from the main package entry point. | Old import | New import | | -------------------------------------- | -------------------------------- | | `@payloadcms/drizzle/types` | `@payloadcms/drizzle` | | `@payloadcms/db-postgres/types` | `@payloadcms/db-postgres` | | `@payloadcms/db-sqlite/types` | `@payloadcms/db-sqlite` | | `@payloadcms/db-vercel-postgres/types` | `@payloadcms/db-vercel-postgres` | | `@payloadcms/db-d1-sqlite/types` | `@payloadcms/db-d1-sqlite` | ```diff - import type { DrizzleAdapter } from '@payloadcms/drizzle/types' - import type { PostgresAdapter } from '@payloadcms/db-postgres/types' - import type { SQLiteAdapter } from '@payloadcms/db-sqlite/types' - import type { VercelPostgresAdapter } from '@payloadcms/db-vercel-postgres/types' - import type { SQLiteD1Adapter } from '@payloadcms/db-d1-sqlite/types' + import type { DrizzleAdapter } from '@payloadcms/drizzle' + import type { PostgresAdapter } from '@payloadcms/db-postgres' + import type { SQLiteAdapter } from '@payloadcms/db-sqlite' + import type { VercelPostgresAdapter } from '@payloadcms/db-vercel-postgres' + import type { SQLiteD1Adapter } from '@payloadcms/db-d1-sqlite' ``` If you use a `declare module` augmentation to extend `GeneratedDatabaseSchema` (typically found in generated schema files), update the module path as well: ```diff - declare module '@payloadcms/db-postgres/types' { + declare module '@payloadcms/db-postgres' { export interface GeneratedDatabaseSchema { schema: DatabaseSchema } } ``` Run `npx @payloadcms/codemod --transform migrate-db-types-subpath` to migrate automatically. The codemod handles import declarations, re-export declarations, and `declare module` augmentations. ### `findMigrationDir` removed from `@payloadcms/drizzle` The deprecated `findMigrationDir` re-export has been removed from `@payloadcms/drizzle`. Import it from `payload` instead: ```diff - import { findMigrationDir } from '@payloadcms/drizzle' + import { findMigrationDir } from 'payload' ``` ### Minimum supported Node.js version is now 24.22.1 ([#16540](https://github.com/payloadcms/payload/pull/16540)) The minimum supported Node.js version is now **24.22.1** (previously 18.20.2). Upgrade your runtime — along with any CI, Docker images, and deployment targets — to the latest node LTS version or newer before upgrading to 4.0. ### Minimum supported Next.js version is now 16.4.0 ([#16537](https://github.com/payloadcms/payload/pull/16537)) Older versions of Next.js are no longer supported. Update Next.js to **16.4.0** or newer. ### Minimum supported TypeScript version is now 6.0.3 ([#16692](https://github.com/payloadcms/payload/pull/16692)) Payload's generated types are no longer guaranteed to work on TypeScript versions below **6.0.3** (previously 5.7.3). Upgrade the `typescript` dependency in your project to 6.0.3 or newer. ### The Slate rich text editor has been removed ([#16359](https://github.com/payloadcms/payload/pull/16359)) Starting in Payload 4.0, `@payloadcms/richtext-lexical` is the only supported rich text editor. The `@payloadcms/richtext-slate` package has been deleted and will no longer be published. The following are removed: - The `@payloadcms/richtext-slate` package - The Slate → Lexical migration utilities in `@payloadcms/richtext-lexical/migrate` - The `payload-plugin-lexical` migration utilities - All Slate references in the documentation The rich text adapter pattern itself is unchanged — `editor: lexicalEditor({})` continues to work exactly as before, and third-party adapters following the same interface remain supported. If you are still using Slate, **do not upgrade to 4.0 yet.** Stay on the latest 3.x release and complete the [Slate → Lexical migration](/docs/v4/rich-text/migration.md) first. ### Legacy Lexical HTML converters have been removed ([#16548](https://github.com/payloadcms/payload/pull/16548)) The deprecated `HTMLConverterFeature`, `lexicalHTML`, and the per-node `converters.html` API have been removed from `@payloadcms/richtext-lexical`. Use `convertLexicalToHTML` and `lexicalHTMLField` from the non-deprecated converter instead. ```diff - import { HTMLConverterFeature, lexicalHTML, convertLexicalToHTML } from '@payloadcms/richtext-lexical' + import { lexicalHTMLField, convertLexicalToHTML } from '@payloadcms/richtext-lexical' editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, - HTMLConverterFeature({}), ], }), fields: [ { name: 'content', type: 'richText' }, - lexicalHTML('content', { name: 'content_html' }), + lexicalHTMLField({ htmlFieldName: 'content_html', lexicalFieldName: 'content' }), ] ``` Custom nodes that define `converters.html` should export their converters instead. ### Lexical now uses its built-in HTML element check ([#17104](https://github.com/payloadcms/payload/pull/17104)) The deprecated `isHTMLElement` utility has been removed from `@payloadcms/richtext-lexical`. It was a thin wrapper around `x instanceof HTMLElement` that duplicated a utility already exported by `lexical`. Import it directly from `lexical` instead. ```diff - import { isHTMLElement } from '@payloadcms/richtext-lexical/client' + import { isHTMLElement } from 'lexical' ``` `lexical`'s built-in `isHTMLElement` is the canonical implementation and narrows against the node's owning-document `HTMLElement`, so it is also safe across iframes and other realms. ### Lexical now uses the upstream Markdown package `@payloadcms/richtext-lexical` no longer ships a vendored copy of `@lexical/markdown`. The package is now a direct dependency, every `@lexical/*` package was upgraded from `0.41.0` to `0.53.0`, and `@payloadcms/richtext-lexical/lexical/markdown` is a pure passthrough of `@lexical/markdown`. Most projects use markdown through the editor or through `convertMarkdownToLexical` / `convertLexicalToMarkdown` and need no changes. You are only affected if you import from `@payloadcms/richtext-lexical/lexical/markdown` directly. **`$convertFromMarkdownString` no longer merges adjacent lines by default.** This matches upstream lexical. If you call it directly and relied on the old behavior, pass `shouldMergeAdjacentLines: true` (5th argument): ```diff - $convertFromMarkdownString(markdown, transformers) + $convertFromMarkdownString(markdown, transformers, undefined, false, true) ``` **Markdown transformer defaults now match upstream lexical (not Payload's features).** The vendored fork shipped a curated `TRANSFORMERS` set (no `CODE`, no `LINK`). Both `@payloadcms/richtext-lexical/lexical/markdown` (now a pure passthrough) and the default argument to `$convertFromMarkdownString` exported from `@payloadcms/richtext-lexical` now use lexical's `TRANSFORMERS`, which bundles `CODE`, `LINK`, and `HEADING` transformers that conflict with Payload's features. You are affected if you imported a group (`TRANSFORMERS`, `ELEMENT_TRANSFORMERS`, ...) from the passthrough path, or called `$convertFromMarkdownString(markdown)` from the main package without passing a transformer list. Use the converters instead, which build the list from your editor config: ```diff - import { $convertFromMarkdownString, TRANSFORMERS } from '@payloadcms/richtext-lexical/lexical/markdown' - $convertFromMarkdownString(markdown, TRANSFORMERS) + import { convertMarkdownToLexical } from '@payloadcms/richtext-lexical' + convertMarkdownToLexical({ editorConfig, markdown }) ``` If you keep calling `$convertFromMarkdownString` directly, pass `editorConfig.features.markdownTransformers` as the second argument. Enable `payload/no-conflicting-lexical-markdown-imports` (included in `@payloadcms/eslint-config`) to catch imports and calls that rely on these mismatching defaults. **Whitespace-only markdown table cells now import as an empty paragraph** instead of a text node of padding spaces. This comes from the lexical upgrade, not Payload's own code. No action is required unless you assert on those padding spaces in stored content. **Markdown export escapes literal brackets and HTML-like text.** `convertLexicalToMarkdown` now writes text such as `[note]` as `[note]` and `` as `<b>`, so literal text no longer turns into links or HTML when the Markdown is parsed again. Ordinary text such as `AT&T` and `a < b` is unchanged, and `convertMarkdownToLexical` restores the original text. No action is required unless you compare exported Markdown against fixed strings or pass it to a parser that does not decode numeric character references. If you import lexical APIs directly through `@payloadcms/richtext-lexical/lexical/*`, also review the [lexical breaking changes](https://github.com/facebook/lexical/releases) between `0.41.0` and `0.53.0`. None of them require action for typical Payload usage. If your app imports Lexical types or adds custom features, check these changes from `0.50.0`. They also apply to imports through `@payloadcms/richtext-lexical/lexical` and its subpaths: - Lexical packages are ESM only. Use ESM imports; CommonJS `require()` needs Node.js 20.19 or later. - `SELECTION_CHANGE_COMMAND` now runs before DOM reconciliation. Defer DOM lookups and floating UI positioning with `$onUpdate(() => editor.read('latest', callback))`, or use an update listener for work that should run after every commit. - Call `node.getLatest().exportJSON()` when serializing a node reference retained across an update. Payload continues to write the full JSON format; compact export is not enabled. - `SerializedEditorState`, `SerializedElementNode`, and `SerializedRootNode` no longer accept a type argument. Remove `` if you only need the base node type. For a document with your own node types, use Payload's `TypedEditorState` instead. Without a type argument, Lexical's `children` are still typed as `SerializedLexicalNode[]`. - `ReturnType` can now infer partial JSON, making fields such as `version` and `url` optional. If your code expects the full format, use the node's explicit serialized type, such as `SerializedLinkNode`. A normal `node.exportJSON()` call still returns the full format. - `LexicalNode.getCommonAncestor` was removed. Use `$getCommonAncestor(a, b)?.commonAncestor ?? null`; pass the parents of non-element nodes if you need the old method's behavior. - Node traversal and key lookup methods no longer accept a type argument: `getParent()`, `getParentOrThrow()`, `getChildren()`, `getFirstChild()`, `getNextSibling()`, `$getNodeByKey()` and the rest of that family. Remove `` and narrow the result with a node guard, for example `node.getChildren().filter($isTableCellNode)`. Runtime behavior is unchanged. - The deprecated `LinkImportExtension`, `ListImportExtension` and `RichTextImportExtension` were removed. Use `LinkExtension`, `ListExtension` and `RichTextExtension` instead. `HorizontalRuleImportExtension` was also removed; use `HorizontalRuleExtension` from `@lexical/extension` together with an extension that includes `CoreImportExtension`. The `*ImportRules` arrays remain. ### Lexical table support is no longer experimental `EXPERIMENTAL_TableFeature` has been renamed to `TableFeature` now that the table feature is no longer experimental. ```diff - import { EXPERIMENTAL_TableFeature } from '@payloadcms/richtext-lexical' + import { TableFeature } from '@payloadcms/richtext-lexical' editor: lexicalEditor({ - features: ({ rootFeatures }) => [...rootFeatures, EXPERIMENTAL_TableFeature()], + features: ({ rootFeatures }) => [...rootFeatures, TableFeature()], }) ``` Stored content is unaffected. ### Lexical: `LexicalEditorProps` renamed to `LexicalEditorArgs` The `LexicalEditorProps` type exported from `@payloadcms/richtext-lexical` has been renamed to `LexicalEditorArgs`. It describes the arguments of the `lexicalEditor` function, not component props. The shape is unchanged. ```diff - import type { LexicalEditorProps } from '@payloadcms/richtext-lexical' + import type { LexicalEditorArgs } from '@payloadcms/richtext-lexical' ``` ### Lexical: `UploadDataImproved` replaced by `UploadData` ([#18606](https://github.com/payloadcms/payload/pull/18606)) The internal `UploadDataImproved` type has been removed, and its definition is now used by the public `UploadData` type. Replace any `UploadDataImproved` imports and type references with `UploadData`: ```ts import type { UploadData } from '@payloadcms/richtext-lexical' ``` ### Lexical: the `version` of nodes is ignored Lexical deprecated the `version` property of serialized nodes, and now ignores it when it loads a document. Payload no longer reads it either. **Rich text written by the Payload 3.0 betas is no longer upgraded when it's loaded.** Payload used `version: 1` to detect two older shapes: - Block nodes whose fields were wrapped in an extra `data` object (`fields.data`) - Relationship and upload nodes that stored the whole related document in `value`, instead of its ID If your database may still contain rich text from the 3.0 betas, run `upgradeLexicalData` on the latest 3.x release before you upgrade to 4.0. It re-saves every document, which converts these shapes: ```ts import { upgradeLexicalData } from '@payloadcms/richtext-lexical' await upgradeLexicalData({ payload }) ``` **`version` is optional in the JSON schema of Lexical nodes.** The CLI and MCP no longer require it, and leave it out of the schema they show agents. They still accept it, so existing documents, which contain `version`, stay valid input. **Payload doesn't add or remove `version` when it saves rich text.** Rich text created by Lexical, like in the admin panel, still has `version: 1` on every node, because Lexical's JSON export writes it. Payload's own nodes used to write their own numbers instead (like `2` for blocks), and now write `1` as well. Rich text sent through the API, CLI or MCP is saved as sent, so it may not contain `version`. The generated types still include `version`, marked as deprecated, so don't rely on it being there. ### Field schema configuration now reflects JSON Schema `typescriptSchema` accepted JSON Schema, not TypeScript, so the name lied. Renamed to `jsonSchema`. Same shape, same array of transforms. The underlying schema is now also consumed by the MCP plugin for runtime validation, not just type generation. ```diff { name: 'tags', type: 'json', - typescriptSchema: [() => ({ type: 'array', items: { type: 'string' } })], + jsonSchema: [() => ({ type: 'array', items: { type: 'string' } })], } ``` ### Rich text output schemas have been renamed The Rich Text adapter's `outputSchema` property has been renamed to `jsonSchema`. The arguments object also gains `typeStringDefinitions: Set` — a sink for raw TS source appended to `payload-types.ts`. ```diff myAdapter: RichTextAdapter = { - outputSchema: ({ collectionIDFieldTypes, config, field, i18n, interfaceNameDefinitions, isRequired }) => { + jsonSchema: ({ collectionIDFieldTypes, config, field, i18n, interfaceNameDefinitions, isRequired, typeStringDefinitions }) => { // return JSONSchema4 }, } ``` `@payloadcms/richtext-lexical` is updated. Only third-party adapters need attention. ### Schema generation now returns definitions alongside JSON Schema `configToJSONSchema` (from `payload`) used to return a `JSONSchema4` directly. It now returns an object with the schema plus the TS-source sink. Destructure to pull the schema out. ```diff - const schema = configToJSONSchema(sanitizedConfig, 'text') + const { jsonSchema: schema, typeStringDefinitions } = configToJSONSchema(sanitizedConfig, 'text') ``` If you run your own type generation, append `[...typeStringDefinitions].join('\n\n')` to the compiled output. Otherwise ignore it. ### Field schema generation now uses object arguments `fieldsToJSONSchema` used to take 6 positional arguments. It now takes one object. The new shape also requires `typeStringDefinitions`, and `forceInlineBlocks` is a top-level sibling instead of nested under `opts`. ```diff - fieldsToJSONSchema( - collectionIDFieldTypes, - fields, - interfaceNameDefinitions, - config, - i18n, - { forceInlineBlocks: true }, - ) + fieldsToJSONSchema({ + collectionIDFieldTypes, + config, + fields, + forceInlineBlocks: true, + i18n, + interfaceNameDefinitions, + typeStringDefinitions, + }) ``` ### Entity schema generation now requires type definitions `entityToJSONSchema` now requires a `typeStringDefinitions` argument at position 5, between `defaultIDType` and `collectionIDFieldTypes`. The old `opts: ConfigToJSONSchemaOptions` is replaced by an optional positional `forceInlineBlocks?: boolean` at the end. ```diff entityToJSONSchema( config, entity, interfaceNameDefinitions, defaultIDType, + typeStringDefinitions, collectionIDFieldTypes, i18n, - { forceInlineBlocks: true }, + true, ) ``` ### Lexical nodes now provide their own JSON Schemas `generatedTypes.modifyJSONSchema` on lexical features (and the sanitized `modifyJSONSchemas` array) is removed. Features now contribute per-node JSON Schemas via `createNode({ jsonSchema, node })` instead of mutating a whole-field schema after the fact. The editor composes per-node schemas into the field's union automatically. If you wrote a custom feature that overrode `modifyJSONSchema`, move per-node generation into `createNode({ jsonSchema: ... })`: ```diff export const MyFeature = createServerFeature({ feature: () => ({ - generatedTypes: { - modifyJSONSchema: ({ currentSchema, interfaceNameDefinitions }) => { - // ... mutate currentSchema or interfaceNameDefinitions ... - return currentSchema - }, - }, nodes: [ createNode({ node: MyNode, + jsonSchema: ({ elementNodeSchema, nodeUnionName, typeStringDefinitions }) => { + typeStringDefinitions.add(`export interface SerializedMyNode { type: 'my'; /* ... */ }`) + return elementNodeSchema({ + nodeType: 'my', + tsType: `SerializedMyNode<${nodeUnionName}>`, + }) + }, }), ], }), }) ``` Nodes without a `jsonSchema` function are omitted from the generated node union entirely — they're filtered out, with no fallback shape. Define `jsonSchema` for every custom node you want reflected in the types; otherwise that node won't appear in the field's generated type, and editor state containing it won't be assignable to it. ### Lexical: duplicate node registration throws `sanitizeServerFeatures` now rejects features that register a node type already present in the editor (matched on `getType()`, or on the replacement target for `LexicalNodeReplacement`). The old behaviour silently let the last write win. The built-in lists features coordinate registration through `shouldRegisterListBaseNodes`, so the default editor is unaffected. Custom features that re-registered an existing node to swap in a subclass should use lexical's `LexicalNodeReplacement` shape: ```diff createNode({ - node: MyExtendedHeadingNode, + node: { replace: HeadingNode, with: (node) => new MyExtendedHeadingNode(node.__tag) }, }) ``` ### Every block now auto-generates a top-level TypeScript interface Before, a block's fields only became a top-level interface in `payload-types.ts` if you set `interfaceName` on the block. Without it, the fields were inlined wherever the block appeared in your types. Now **every block always emits a top-level interface**. The name comes from the slug, converted to PascalCase (`'content-block'` → `ContentBlock`, `'richTextBlock'` → `RichTextBlock`). `interfaceName` still works, but it's now an _override_ for that default name rather than the switch that enables generation. The same rule applies to lexical block nodes — they now emit as `SerializedBlockNode<…>` / `SerializedInlineBlockNode<…>` referencing the per-block interface, instead of inlining a `{ [k: string]: unknown }` shape. When two **different** blocks resolve to the same auto-derived name (same slug, different fields — e.g. a shared block overridden in one collection), the first keeps the clean name and any later block with a different shape gets a stable content-hash suffix (`Hero_3F2A1B0C`), so neither is silently mistyped. To choose the name yourself — or to resolve a collision with another type in your generated file (a collection, an array's `interfaceName`, etc.) — set an explicit `interfaceName` on the block. See [Block interface name collisions](/docs/v4/typescript/generating-types.md#block-interface-name-collisions). ### Block references now share the blocks array The `blockReferences` property on blocks fields has been removed. The `blocks` property now accepts both inline block configs and string slugs for blocks defined in the top-level Payload Config `blocks` array. Run `npx @payloadcms/codemod --transform migrate-block-references-to-blocks` to migrate automatically. ```diff export const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'layout', type: 'blocks', - blockReferences: ['hero', 'content'], - blocks: [], + blocks: ['hero', 'content'], }, ], } ``` Inline blocks continue to work as before: ```ts { name: 'layout', type: 'blocks', blocks: [HeroBlock, ContentBlock], } ``` ### Generated Lexical types are now strict `richText` fields backed by `lexicalEditor` used to emit a permissive `{ [k: string]: unknown }` per field. They now emit a discriminated union of strict per-node interfaces (`SerializedTextNode`, `SerializedParagraphNode`, `SerializedHeadingNode`, `SerializedBlockNode`, etc.) under a hash-named alias like `LexicalNodes_AB12CD34`. Block nodes come through as `SerializedBlockNode` / `SerializedInlineBlockNode` referencing the per-block interface (see the `block.interfaceName` change above). The auto-added `id` is now required (`id: string`), since a stored lexical block always has one. Code that imported field types from `payload-types.ts` and cast through `unknown` may now type-check tighter and surface real bugs. The hand-written `TypedEditorState` / `DefaultTypedEditorState` helpers (used to type rich text in converters, custom renderers, and so on) got stricter to match. `TypedEditorState` dropped the `{ [k: string]: unknown }` at the editor-state root, and a node's `children` are now the real node union at any depth — they used to fall back to the loose `SerializedLexicalNode`. Runtime data is unchanged, but narrow by `node.type` where you read nested nodes loosely. Relationship and upload node `value` - and the exported `RelationshipData` / `UploadData` types - now use each collection's own ID type instead of `number | string`. This is narrower, but it only removes an ID type that was never valid for that collection (e.g. `number` for a string-ID collection). Element nodes are now generic over their `children`, and you pass the node union into them yourself (previously `TypedEditorState` did this for you via an internal `RecursiveNodes` helper): ```diff import type { SerializedParagraphNode, SerializedTextNode, TypedEditorState, } from '@payloadcms/richtext-lexical' - type MyNodes = SerializedParagraphNode | SerializedTextNode + type MyNodes = SerializedParagraphNode | SerializedTextNode function renderRichText(state: TypedEditorState) { // ... } ``` `SerializedTextNode` is a leaf, so it stays bare. For the built-in nodes plus a few of your own, `WithDefaultNodes` threads the union for you - pass your custom node(s) as the generic (`type MyNodes = WithDefaultNodes>`), and they're usable anywhere a node can appear, not just at the top level. `DefaultTypedEditorState` with only built-in nodes needs no change. ### Generated JSON Schema now uses modern definitions Payload builds a JSON Schema from your config to generate `payload-types.ts`. That schema now stores its reusable types under the modern `$defs` key instead of the older `definitions` key, and every internal reference points at `#/$defs/...` instead of `#/definitions/...`. This follows the current JSON Schema spec. **Your generated `payload-types.ts` does not change.** You are only affected if you read or write that schema yourself — most commonly through `typescript.schema` on your config, a custom rich text adapter, or a custom Lexical feature. Update any code that adds to the schema: ```diff // payload.config.ts { typescript: { schema: [ ({ jsonSchema }) => { - jsonSchema.definitions.MyType = { type: 'object', properties: { /* ... */ } } + jsonSchema.$defs.MyType = { type: 'object', properties: { /* ... */ } } return jsonSchema }, ], }, } ``` And update any `$ref` pointers you wrote by hand to point at `$defs`: ```diff - { $ref: '#/definitions/posts' } + { $ref: '#/$defs/posts' } ``` Do not keep your additions on `definitions` while new ones land on `$defs`. ### Jobs: access is enforced by default The generated `payload-jobs` collection now denies generic create, read, update, and delete access by default. This affects REST and GraphQL CRUD, plus Local API collection operations because `overrideAccess` now defaults to `false`. Dedicated operations such as `payload.jobs.queue()`, `payload.jobs.run()`, `payload.jobs.runByID()`, `payload.jobs.cancel()`, and `payload.jobs.cancelByID()` also default `overrideAccess` to `false`. They now run the corresponding `jobs.access.queue`, `jobs.access.run`, or `jobs.access.cancel` function when the option is omitted. Add `overrideAccess: true` to trusted server-side calls that must preserve the Payload 3 behavior of bypassing Jobs access control. If your application intentionally exposes the internal payload-jobs collection, configure only the access it needs through `jobsCollectionOverrides`. For example, the following enables read-only Admin Panel access for trusted administrators: ```ts jobs: { jobsCollectionOverrides: ({ defaultJobsCollection }) => ({ ...defaultJobsCollection, access: { ...defaultJobsCollection.access, read: ({ req }) => Boolean(req.user?.roles?.includes('admin')), }, admin: { ...defaultJobsCollection.admin, hidden: false, }, }), } ``` Job documents can contain inputs, outputs, logs, and execution state, so avoid enabling write access unless your application specifically requires it. ### Job queries and hooks now have fixed behavior ([#16414](https://github.com/payloadcms/payload/pull/16414)) The deprecated `jobs.depth` and `jobs.runHooks` config options have been removed. Job operations now always use direct database calls with depth `0`. If you relied on either option, remove it from your `jobs` config. If you depended on collection hooks running for jobs, migrate that logic out of those hooks. The `jobsCollectionOverrides` warning that detected extra hooks and prompted users to set `runHooks` has also been removed. ### The legacy base job type has been removed The deprecated `BaseJob` type is no longer exported. Use `Job` everywhere you previously used `BaseJob`. ```diff - import type { BaseJob } from 'payload' + import type { Job } from 'payload' - function processJob(job: BaseJob) { + function processJob(job: Job) { // ... } ``` `Job` now handles its own fallback. When the generated `payload-jobs` collection type exists, `Job` uses it. Otherwise, it uses the default job document from `UntypedPayloadTypes`. Consumers no longer need to choose between a generated job type and a separate fallback type. The `false` fallback used by job and workflow generics has also been removed. Omit the generic when you want the default `object` input type. ```diff - const processJob = (job: Job) => { + const processJob = (job: Job) => { // ... } - const workflow: WorkflowConfig = { + const workflow: WorkflowConfig = { slug: 'my-workflow', // ... } ``` Generated workflow slugs and explicit input objects continue to work as before. For example, `Job<'my-workflow'>` uses the generated workflow input, while `Job<{ message: string }>` uses the provided input type. ### Jobs: stats and metadata are enabled by default When jobs are enabled, Payload now always adds the `payload-jobs-stats` global and the `meta` field on the `payload-jobs` collection. Previously, these were only added when a task or workflow used a schedule. Existing projects that use sqlite/postgres and jobs may need a database migration. ### Jobs: concurrency controls and parent task logging are always enabled Concurrency controls add the indexed `concurrencyKey` field to the `payload-jobs` collection. Parent task logging adds parent information to nested task logs. Task log input is also now required and is stored as an empty object when a task has no input. The `jobs.enableConcurrencyControl` and `jobs.addParentToTaskLog` options have been removed. Existing SQL projects that use jobs should generate a database migration for these schema changes. ### Jobs: task and workflow types have been renamed The `TaskType` and `WorkflowTypes` exports have been renamed to `TaskSlug` and `WorkflowSlug`. ```diff - import type { TaskType, WorkflowTypes } from 'payload' + import type { TaskSlug, WorkflowSlug } from 'payload' ``` The deprecated `RunningJob` and `RunningJobSimple` types have also been removed. Use `Job` instead. JSON workflow steps without a `task` must now provide an `inlineTask` handler. This removes a type escape that previously allowed an incomplete step even though it could not run. ### Custom database adapters must support job claiming Custom database adapters must now implement `updateJobs` so Payload can use a unique `processingToken` when claiming jobs and prevent multiple workers from running the same job. This adds an internal, nullable `processingToken` field to the `payload-jobs` collection. Existing SQL databases need a migration that adds this column. MongoDB does not require a migration. The default `updateJobs` implementation has been removed because it could not safely claim jobs across multiple workers. If you maintain a custom database adapter, you must now provide `updateJobs`. It must only claim jobs that still match the provided `where` condition and, when a `processingToken` is provided, return only the jobs claimed with that token. The official Payload database adapters already implement this behavior. ### Jobs: crashed workers are recovered with processing leases The permanent `processing` flag on `payload-jobs` has been replaced by a renewable `processingUntil` lease. If a worker exits, a later jobs runner call can claim the job after its lease expires. Worker crashes do not consume the job's handler retry configuration. Worker updates use a 30-second safety buffer by default, so an update only starts when enough lease time remains for it to finish before another claim. Configure the lease with `jobs.processingLease.duration` and `jobs.processingLease.safetyBuffer`. Existing SQL databases need a migration that replaces `processing` with the nullable, indexed `processingUntil` column. MongoDB does not require a migration. ### Job task results no longer include state ([#16416](https://github.com/payloadcms/payload/pull/16416)) Job task handlers and `inlineTask` callbacks can no longer use the `state` field to signal success or failure. Both shapes were deprecated in v3 and are now removed from tasks and inline tasks. - `return { state: 'failed', errorMessage }` — throw an error instead. - `return { state: 'succeeded', output }` — return `{ output }` directly. ```ts // Before const myTask: TaskHandler<'MyTask'> = async ({ input }) => { if (!input.id) { return { state: 'failed', errorMessage: 'Missing id' } } return { state: 'succeeded', output: { ok: true } } } // After const myTask: TaskHandler<'MyTask'> = async ({ input }) => { if (!input.id) { throw new Error('Missing id') } return { output: { ok: true } } } ``` ### Cron expressions now require explicit ranges ([#16546](https://github.com/payloadcms/payload/pull/16546)) The `sloppyRanges: true` setting that kept Croner v9 stepping syntax working after the v10 upgrade has been removed. Croner now enforces stricter cron parsing rules, so stepping expressions must use either `*` or an explicit range before the `/`. This affects job autorun crons (`jobs.autoRun[].cron`), scheduled task and workflow runs (`schedule.cron`), and the `payload` bin script `--cron` flag. In strict mode (now the default), only these stepping forms are valid: - `*/N` — wildcard with step - `start-end/N` — explicit range with step The following forms now throw a `TypeError` at registration: - `/N` — missing prefix - `M/N` — numeric prefix (single number before `/`) Migrate any affected cron expressions: ```diff # Missing prefix — the leading `*` is now required (both fire every 10 minutes) - cron: '/10 * * * *' + cron: '*/10 * * * *' # Numeric prefix "every N starting from M" — make the range explicit (fires at 5, 20, 35, 50) - cron: '5/15 * * * *' + cron: '5-59/15 * * * *' # Numeric prefix that produces a single value — drop the redundant step (only fires at minute 30) - cron: '30/30 * * * *' + cron: '30 * * * *' ``` ### Relationship and upload limits now use row-based options ([#16547](https://github.com/payloadcms/payload/pull/16547)) The deprecated `min` and `max` properties have been removed from `relationship` and `upload` fields with `hasMany: true`. Use `minRows` and `maxRows` instead. ```diff { name: 'tags', type: 'relationship', relationTo: 'tags', hasMany: true, - min: 1, - max: 5, + minRows: 1, + maxRows: 5, } ``` The same change applies to `upload` fields with `hasMany: true`. ### Nested localized fields no longer need a compatibility flag ([#16693](https://github.com/payloadcms/payload/pull/16693)) `config.compatibility.allowLocalizedWithinLocalized` and the `PAYLOAD_DO_NOT_SANITIZE_LOCALIZED_PROPERTY` environment variable have been removed. Sanitize no longer strips `localized: true` from fields nested under a localized parent — `fieldShouldBeLocalized` decides this at runtime instead. **Who is affected:** - Users of `compatibility: { allowLocalizedWithinLocalized: true }`. - Anyone with `localized: true` nested under a localized parent. The end behavior is unchanged (Payload's own code already uses `fieldShouldBeLocalized`), but `field.localized` is no longer deleted. Custom plugin or hook code that reads `field.localized` directly will now see `true` where it previously saw `undefined`. **How to migrate:** - Remove the `compatibility` block from your config. If your MongoDB data still has the redundant nested-localized shape, flatten the config and run a data migration. - Replace any direct `field.localized` reads in custom code with `fieldShouldBeLocalized({ field, parentIsLocalized })` from `payload/shared`. ### Publishing now defaults to the active locale The Admin UI Publish button now always defaults to publishing only the active locale when a collection or global has localized fields and you have publish permission. "Publish all locales" is available as a secondary option in the Publish button's dropdown. This previously required opting in via `localization.defaultLocalePublishOption: 'active'`; the config option has been removed since active-locale-only is now the permanent default. **What changes:** - `localization.defaultLocalePublishOption` no longer exists as a config property or type — remove it from your config. - If you had it set to `'active'`, there is no change in Admin UI behavior. - If you had it set to `'all'`, or left it unset, the Publish button now publishes only the active locale by default. Use the "Publish all locales" option in the button's dropdown to publish every locale at once. ```diff export default buildConfig({ localization: { defaultLocale: 'en', - defaultLocalePublishOption: 'active', locales: ['en', 'es'], }, }) ``` Run `npx @payloadcms/codemod --transform remove-default-locale-publish-option` to remove the property automatically. The codemod surfaces a note for any config that had it set to `'all'`, since that combination previously changed the default and now has no config equivalent. ### Storage adapters have moved to top-level configuration ([#16596](https://github.com/payloadcms/payload/pull/16596)) Storage adapter packages (`@payloadcms/storage-s3`, `-azure`, `-gcs`, `-r2`, `-vercel-blob`) must now be placed under the top-level `storage` key in your Payload config instead of inside `plugins`. Declaring adapters inside `plugins` no longer works. The `storage` key ensures upload hooks, static file handlers, and upload instructions are wired up before any plugin runs. ```diff import { s3Storage } from '@payloadcms/storage-s3' import { buildConfig } from 'payload' export default buildConfig({ - plugins: [ - s3Storage({ bucket: process.env.S3_BUCKET, collections: { media: true }, config: { region: process.env.S3_REGION } }), - otherPlugin(), - ], + plugins: [otherPlugin()], + storage: [ + s3Storage({ bucket: process.env.S3_BUCKET, collections: { media: true }, config: { region: process.env.S3_REGION } }), + ], }) ``` Run `npx @payloadcms/codemod --transform migrate-storage-adapters-to-config` to migrate automatically. ### Direct uploads now use Payload's shared upload-instructions endpoint Official storage packages now use `POST /api/upload-instructions` instead of provider-specific endpoints. #### If you use an official Payload storage adapter If you only enable `clientUploads` in your storage adapter config, no migration is needed. The Admin Panel uses the new endpoint automatically. Payload now checks `upload.limits.fileSize` before returning upload instructions. This is new for Azure, GCS, R2, and Vercel Blob; S3 already checked it. Increase the limit if your direct uploads previously exceeded it. The old S3, Azure, and GCS signing endpoints (e.g. `/api/storage-s3-generate-signed-url`) were removed. If you called one directly, call `/api/upload-instructions` instead: ```ts const response = await fetch('/api/upload-instructions', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ collectionSlug: 'media', docPrefix: 'gallery', filename: file.name, filesize: file.size, mimeType: file.type, }), }) ``` For S3 and GCS, send the file using the returned HTTP request, then pass `instructions.file` unchanged when creating the document: ```ts const instructions = await response.json() if (instructions.type !== 'http') { throw new Error(`Unsupported upload instruction: ${instructions.type}`) } const { url, ...request } = instructions.request await fetch(url, { ...request, body: file }) const formData = new FormData() formData.append('_payload', JSON.stringify({})) formData.append('file', JSON.stringify(instructions.file)) await fetch('/api/media', { method: 'POST', credentials: 'include', body: formData, }) ``` Remove manual registrations of `S3ClientUploadHandler`, `AzureClientUploadHandler`, and `GcsClientUploadHandler`; Payload now registers them. The S3 and GCS `/client` exports were also removed. If you typed `clientUploads.access`, replace `ClientUploadsAccess` with `UploadInstructionsAccess`: ```diff - import type { ClientUploadsAccess } from '@payloadcms/plugin-cloud-storage/types' + import type { UploadInstructionsAccess } from 'payload' - const access: ClientUploadsAccess = ({ req }) => Boolean(req.user) + const access: UploadInstructionsAccess = ({ req }) => Boolean(req.user) ``` #### If you maintain a custom storage adapter File versioning adds a required `GeneratedAdapter.copyFile` method. Copy `from` to `to` (complete storage keys) without changing the source, reject an existing destination, and return only once the destination is readable. `moveFile` stays optional. See [Custom Storage Adapters](/docs/v4/upload/storage-adapters.md) for the full contract. Replace `clientUploadContext` with `uploadReference` in generated file metadata and static handler parameters. `handleUpload` no longer receives it because Payload skips uploading files that are already stored. Payload keeps only `_objectKey`, `prefix`, and `signedReceipt` at the top level of `uploadReference`, plus signed `adapterData` when `requiresUploadReceipt` is set. Read any other field your adapter or client added from `uploadReference.clientData`: ```diff - const objectID = uploadReference.providerObjectID + const objectID = uploadReference.clientData?.providerObjectID ``` `clientData` is not verified. To trust a value, sign it into the upload receipt as `adapterData` instead. See [Upload receipts](/docs/v4/upload/storage-adapters.md#custom-upload-receipts). Set `uploadInstructions.enabled`. When false, Payload adds `adminHandler` to the import map without rendering it. When true, Payload also renders it and exposes the instructions and supporting endpoint. Move access checks into the generator or supporting endpoint that authorizes the upload: ```diff uploadInstructions: { - access, - generate, + enabled: Boolean(clientUploads), + generate: generateUploadInstructions({ access }), } ``` `useUploadHandlers().setUploadHandler` was removed. Name custom client handlers and return the same name in a `dispatch` instruction: ```ts import { createClientUploadHandler } from '@payloadcms/plugin-cloud-storage/client' export const MyClientUploadHandler = createClientUploadHandler({ name: 'uploadToMyProvider', handler: async ({ data, file }) => { await uploadToMyProvider({ data, file }) }, }) uploadInstructions: { enabled: true, generate: ({ filename, filesize, mimeType }) => ({ file: { uploadReference: {}, filename, mimeType, size: filesize, }, type: 'dispatch', name: 'uploadToMyProvider', data: {}, }), useInAdmin: true, } ``` Set `useInAdmin` to `true` when the Admin panel should use the instructions instead of attaching the whole file to the document request. This can also benefit chunked uploads that still pass through Payload. R2's multipart client route is one example: it passes chunks through Payload, but the Admin still uses it to avoid sending the whole file in one request. The `data` value is passed from the server instruction to the client handler. The names must match exactly. If the handler returns an object, it becomes `file.uploadReference` in the document request. Finally, remove `initClientUploads` and declare its Admin handler and supporting endpoint on `uploadInstructions`: ```diff - import { initClientUploads } from '@payloadcms/plugin-cloud-storage/utilities' - initClientUploads({ - clientHandler: '/MyClientUploadHandler', - collections, - config, - enabled: true, - extraClientHandlerProps: () => ({ option: 'value' }), - serverHandler, - serverHandlerPath: '/my-upload-endpoint', - }) uploadInstructions: { + adminHandler: { + path: '/MyClientUploadHandler', + props: { option: 'value' }, + }, + enabled: Boolean(clientUploads), + endpoint: { + handler: serverHandler, + path: '/my-upload-endpoint', + }, generate, + useInAdmin: true, } ``` Both are optional. Custom handler props are now passed as `props`, and the supporting route is available as `endpointPath` instead of `serverHandlerPath`. The old Admin component `enabled` prop was removed. ### The Uploadthing storage adapter has been removed The Uploadthing storage adapter, deprecated in a previous release, has been deleted and will no longer be published. Migrate to another storage adapter such as `@payloadcms/storage-s3` before upgrading to 4.0. ```diff - import { uploadthingStorage } from '@payloadcms/storage-uploadthing' + import { s3Storage } from '@payloadcms/storage-s3' export default buildConfig({ storage: [ - uploadthingStorage({ - collections: { media: true }, - options: { token: process.env.UPLOADTHING_TOKEN }, - }), + s3Storage({ + collections: { media: true }, + bucket: process.env.S3_BUCKET, + config: { region: process.env.S3_REGION }, + }), ], }) ``` The `migrate-storage-adapters-to-config` codemod no longer recognizes `uploadthingStorage` — migrate uploadthing configs to another adapter manually. ### Azure client uploads now use the Azure Blob SDK — update your CORS rules `@payloadcms/storage-azure` client uploads (`clientUploads`) now upload through the Azure Blob SDK, which splits files into blocks (`Put Block` + `Put Block List`). This lifts the ~5GB single-request limit and raises the effective ceiling to Azure's block-blob maximum (~190TiB). **This is an infrastructure change, not a code change — it cannot be auto-migrated.** The SDK sends additional `x-ms-*` headers and issues CORS preflight (`OPTIONS`) requests, which is broader than the previous single-`PUT` flow required. If your storage account's CORS rules are scoped narrowly to the old flow, browser uploads will start failing with `RestError: Failed to fetch`. Update the CORS rules on your storage account (Storage account → **Resource sharing (CORS)** → Blob service) so that they allow: | Field | Value | | --------------- | ---------------------------------------------------------- | | Allowed origins | Your site origin (e.g. `https://example.com`) | | Allowed methods | `GET,PUT,OPTIONS` (add `HEAD` if reading blobs in-browser) | | Allowed headers | `*` (or at minimum `x-ms-*,content-type,content-length`) | | Exposed headers | `*` | | Max age | `3600` | If you already set `Allowed headers` to `*`, no change is required. If you enabled large-file uploads in v3 via the `clientUploads.chunkLargeFiles` flag, remove it — chunked uploads are now the default in v4 and the option no longer exists: ```diff azureStorage({ - clientUploads: { chunkLargeFiles: true }, + clientUploads: true, // ... }) ``` Run `npx @payloadcms/codemod --transform migrate-azure-chunk-large-files` to remove the flag automatically (it collapses `clientUploads: { chunkLargeFiles: true }` to `clientUploads: true` and keeps any other `clientUploads` options). ### Sharp is now an optional file transformer #sharp-moved-out-of-core-into-payloadcmstransformer-sharp Image resizing is no longer built into `payload` itself. It now ships as an official [file transformer](/docs/v4/upload/transformers.md), `@payloadcms/transformer-sharp`, that you register under `upload.transformers`. This affects four things: 1. The top-level `sharp` config property has been removed. Passing a Sharp instance now happens through `sharpTransformer({ sharp })` instead. 2. Per-collection Sharp options — `constructorOptions`, `formatOptions`, `resizeOptions`, `trimOptions`, and `withMetadata` — have been removed from `CollectionConfig['upload']`. They're now authored through `sharpTransformer({ collections })`. - `imageSizes` has also been removed from `CollectionConfig['upload']`. Image sizes are now declared as `variants` on `sharpTransformer({ collections: { : { variants } } })`. The entries themselves (`name`, `width`, `height`, `generateImageName`, and so on) are unchanged. A collection that still sets `upload.imageSizes` fails the build. 3. `sharp` is no longer a dependency of `payload` at all (previously a `devDependency`, not a runtime `dependency`). Install `@payloadcms/transformer-sharp` explicitly. 4. The Sharp-specific types `SharpDependency`, `ImageUploadFormatOptions`, `ImageUploadTrimOptions`, and `SharpImageSizeOptions` are no longer exported from `payload`. Import `SharpDependency` from `@payloadcms/transformer-sharp`. For the option types, use `SharpCollectionConfig['formatOptions']`, `SharpCollectionConfig['trimOptions']`, and `NonNullable[number]` from the same package. ```sh pnpm add @payloadcms/transformer-sharp ``` ```diff + import { sharpTransformer } from '@payloadcms/transformer-sharp' import { buildConfig } from 'payload' export default buildConfig({ collections: [ { slug: 'media', upload: { - resizeOptions: { width: 2048, height: 2048 }, - imageSizes: [{ name: 'thumbnail', width: 400, height: 300 }], }, }, ], - sharp, upload: { + transformers: [ + sharpTransformer({ + sharp, + collections: { + media: { + resizeOptions: { width: 2048, height: 2048 }, + variants: [{ name: 'thumbnail', width: 400, height: 300 }], + }, + }, + }), + ], }, }) ``` Run `npx @payloadcms/codemod --transform migrate-sharp-to-transformer` to migrate automatically. The codemod moves a top-level `sharp` dependency and inline collection Sharp options — including `crop` and `focalPoint` — into `sharpTransformer({ collections })` (writing `imageSizes` as `variants`), appends to an existing `upload.transformers` array if you already have one, and leaves anything it can't safely rewrite in place with a note — for example, a `collections` array built by a function call, or a collection defined outside the `buildConfig` call. Review those manually. For a config that already registers `sharpTransformer`, it only renames `imageSizes` to `variants` inside the existing `sharpTransformer` call and flags a leftover top-level `sharp` property; it doesn't move any remaining collection options. Your `variants`, `crop`, and `focalPoint` are readable at `collection.upload.variants`/`.crop`/`.focalPoint` on the **sanitized** config (for example `req.payload.collections.media.config.upload.variants`). Code that read the sanitized `upload.imageSizes` in 3.x must read `upload.variants` instead. `sharpTransformer` writes a projection of what you configure back onto the sanitized collection at startup, so the Admin Panel and the rest of core see the same configuration. For a collection configured on `sharpTransformer`, a `crop` or `focalPoint` set on the transformer replaces the collection's own value; one left unset there keeps the collection's. Neither `imageSizes` nor `variants` can be set on the collection at all. If you inject a custom Sharp build or version, pass it the same way: ```diff - buildConfig({ sharp: myCustomSharp }) + sharpTransformer({ sharp: myCustomSharp }) ``` `sharpTransformer` can also resize images at request time (`/api/media/file/photo.png?width=400`). This is new in Payload 4 and **disabled by default**, so a migrated config keeps serving only the original file and your pre-generated `variants`. The codemod doesn't enable it. To opt in, pass `dynamic: true` or `dynamic: { collections: ['media'] }` — see [Dynamic (request-time) resizing](/docs/v4/upload/transformers.md#dynamic-request-time-resizing), and [Securing dynamic resizing](/docs/v4/upload/transformers.md#securing-dynamic-resizing) before enabling it on a publicly readable collection. #### Upload image data now uses variants The group field that holds each generated image size on an upload document is now `variants` instead of `sizes`. Every place that reads or queries it changes: ```diff - doc.sizes?.thumbnail?.url + doc.variants?.thumbnail?.url - where: { 'sizes.thumbnail.filename': { equals: 'photo-400x300.png' } } + where: { 'variants.thumbnail.filename': { equals: 'photo-400x300.png' } } - select: { sizes: { thumbnail: true } } + select: { variants: { thumbnail: true } } ``` GraphQL queries select `variants { thumbnail { url } }` instead of `sizes { thumbnail { url } }`, and regenerated types expose `variants?: { ... }` (and `variants?: T | { ... }` on the `*Select` types) in place of `sizes`. Update any frontend code that reads `media.sizes`, including values you pass to helpers such as `getBestFitFromSizes({ sizes: doc.variants })`. Existing documents must be migrated. Create the predefined migration for your database adapter and run it: > **Warning** > > Payload 4 uses `variants` as the top-level field for generated upload > variants. If an upload collection already defines a custom field named > `variants`, rename that field before you run the predefined migration. Choose > a new name, update your config, and write a custom migration that moves the > existing custom data from `variants` to the new field. Then run the predefined > `sizes-to-variants` migration. If legacy `sizes` data or columns and custom > `variants` data or columns are both present, the predefined migration stops > before it changes data or indexes. It cannot choose a new name or merge this > custom data for you. ```sh # MongoDB payload migrate:create --file @payloadcms/db-mongodb/sizes-to-variants # Postgres payload migrate:create --file @payloadcms/db-postgres/sizes-to-variants # Vercel Postgres payload migrate:create --file @payloadcms/db-vercel-postgres/sizes-to-variants # SQLite payload migrate:create --file @payloadcms/db-sqlite/sizes-to-variants # Cloudflare D1 payload migrate:create --file @payloadcms/db-d1-sqlite/sizes-to-variants payload migrate ``` **Postgres and SQLite (including Vercel Postgres and D1):** the columns are renamed in place from `sizes__` to `variants__`, on each upload collection's table and its versions table, along with their indexes. Use the predefined migration above **instead of** a plain `payload migrate:create` for this change. A generated migration would drop the `sizes_*` columns and add new `variants_*` columns, losing the stored image size data. The predefined migration also writes the schema snapshot, so the next generated migration starts from the renamed columns. On MongoDB the migration renames `sizes` (and `version.sizes` on versions) with `$rename` and re-creates the indexes on those fields under the new path. Run the migration before the upgraded app writes to upload documents. #### The file endpoint changes once any transformer is registered Registering any transformer under `upload.transformers` — including `sharpTransformer()` with dynamic resizing off — routes every file request through the transformer-aware handler: - A file must belong to an upload document to be served. A file that exists in storage without a matching document now returns `404`. - For files whose MIME type a transformer declares (every image, with `sharpTransformer()`), `access.read` can run twice per request: once with `req.fileTransform` set to `true` to decide a possible transform, and once without it for an ordinary read. Keep `access.read` cheap, and use `req.fileTransform` to restrict transformed reads — see [Access control](/docs/v4/upload/transformers.md#access-control). - Responses produced by a transformer always carry Payload's CORS headers, which `modifyResponseHeaders` can't override. To link to a file from your own code, including with transformer query parameters, use the new `generatePayloadFileURL` helper (exported from `payload` and `payload/shared`). It always builds the access-controlled Payload file endpoint, so links never bypass `access.read` by pointing straight at storage. ### Storage prefixes are now always included The `alwaysInsertFields` option (on `cloudStoragePlugin` and every `storage-*` adapter: `-s3`, `-azure`, `-gcs`, `-r2`, `-vercel-blob`) has been removed. The behavior it used to opt into — always inserting the hidden `prefix` field into the collection schema — is now unconditional, so the schema stays consistent whether or not a collection configures a `prefix` and whether or not the plugin is `enabled`. **Who is affected:** - Anyone with an upload collection that uses a storage adapter or `cloudStoragePlugin`. Every such collection now gets a hidden `prefix` field, including collections that never configured a `prefix` and collections where the plugin is disabled (`enabled: false`). - Anyone who set `alwaysInsertFields` explicitly — the option no longer exists on any storage adapter or plugin option type. - Postgres, SQLite, and D1 users: the new field is a new database column. Running `payload migrate:create` will generate a migration that adds a `prefix` column to every affected collection's table. **How to migrate:** - Remove `alwaysInsertFields` from your storage adapter or `cloudStoragePlugin` config — it has no effect and no longer exists on the option types. - For Postgres, SQLite, or D1 projects, run `payload migrate:create` to generate the migration that adds the `prefix` column, then run `payload migrate` to apply it. ### Upload collections retain originals and file history Upload collections now store `original` metadata and a `prefix` and `_objectKey` for each stored file (the main file, `original`, and each variant), so saved versions can find their files. New uploads keep their uploaded filename. With a storage adapter, each new file is stored under its own `_objectKey` folder, so update any code or infrastructure that relies on the old object key. When a processed main file and its original share a filename, request the original with `?original=true`; `original.url` already includes it. With local storage, the original gets a counter name instead (for example `photo-1.png`). Files saved by earlier v4 beta releases with an `-original` filename keep that name and are still served. Custom storage adapters should handle the new `params.isOriginal` in `staticHandler`; see [Custom Storage Adapters](/docs/v4/upload/storage-adapters.md#custom-storage-adapters). **Postgres and SQLite:** after updating, run `payload migrate:create`, check that it adds these columns to each upload collection table and its versions table, then run `payload migrate`. Existing rows keep `null` values until their next write. ### File size types have been consolidated ([#16593](https://github.com/payloadcms/payload/pull/16593)) `FileSizeImproved` has been merged into `FileSize`. The `url`, `width`, `height`, `filesize`, `mimeType`, and `filename` properties now accept `null` directly on `FileSize`, matching what the database stores for sizes that were not generated. ```diff - import type { FileSizeImproved } from 'payload' + import type { FileSize } from 'payload' ``` ### Image size admin controls have been consolidated ([#16593](https://github.com/payloadcms/payload/pull/16593)) The `disableListColumn`, `disableListFilter`, and `disableGroupBy` properties under `ImageSize.admin` have been replaced with a single `disabled` object, consistent with the shape used by all other fields. ```diff { name: 'thumbnail', width: 400, height: 300, admin: { - disableListColumn: true, - disableGroupBy: true, + disabled: { column: true, groupBy: true }, }, } ``` Run `npx @payloadcms/codemod --transform migrate-disabled-fields` to migrate automatically. ### MongoDB queries no longer use facet aggregation ([#16612](https://github.com/payloadcms/payload/pull/16612)) The `useFacet` option in `mongooseAdapter` config has been removed. Payload no longer uses `$facet` aggregation, so the option has no effect. ```diff mongooseAdapter({ connectOptions: { - useFacet: false, }, }) ``` ### Search plugin API paths are now derived automatically ([#16597](https://github.com/payloadcms/payload/pull/16597)) The deprecated `apiBasePath` option on the search plugin has been removed. The plugin now derives the correct path from the root config automatically. Remove any `apiBasePath` key from your search plugin config. ### API keys created before v3.46.0 must be regenerated ([#16628](https://github.com/payloadcms/payload/pull/16628)) The sha1 HMAC fallback for API key authentication has been removed. API keys are now matched exclusively against the sha256 index. Any API key that was created before v3.46.0 and never re-saved will no longer authenticate. **How to migrate:** Open the user document for each affected API key holder and save it — this regenerates the key under sha256. Alternatively, generate a new API key from the admin panel. ### API key fields now use a factory The `baseAPIKeyFields` export from `payload` has been removed. Use `createAPIKeyFields()` instead. It returns the same default auth API key storage fields, and also accepts overrides for projects or plugins that need to customize the generated field configs. ```diff - import { baseAPIKeyFields } from 'payload' + import { createAPIKeyFields } from 'payload' export const MyCollection = { fields: [ - ...baseAPIKeyFields, + ...createAPIKeyFields(), ], } ``` If you previously cloned or modified `baseAPIKeyFields`, pass field overrides to `createAPIKeyFields()` instead: ```ts createAPIKeyFields({ apiKeyField: { admin: { components: { Field: './APIKeyField#APIKeyField' }, }, }, includeEnableAPIKey: false, }) ``` ### MCP plugin configuration and authentication have changed `@payloadcms/plugin-mcp` has a new public config API, built-in tool inputs, and API-key authentication flow. Update your plugin config and recreate MCP credentials as user API keys. **Collections and globals are now opt-out.** Drop the `enabled` flag. Every collection and global is exposed by default through the generic built-in tools. For collections, this includes tools like `getCollectionSchema`, `findDocuments`, `countDocuments`, `createDocuments`, `updateDocument`, `deleteDocuments`, `duplicateDocument`, and `findDistinct`, plus version tools when versions are enabled. For globals, this includes `getGlobalSchema`, `findGlobal`, and `updateGlobal`, plus version tools when versions are enabled. Turn individual operations off via simple config keys like `tools: { create: false }`. ```diff mcpPlugin({ collections: { - posts: { enabled: true }, - users: { enabled: { find: true } }, + // posts is exposed automatically, no entry needed + users: { + tools: { + create: false, + delete: false, + getCollectionSchema: false, + update: false, + }, + }, // find only }, }) ``` After upgrading, collections you never listed before are reachable over MCP. Review what's exposed and disable anything that shouldn't be. **Custom tools, prompts and resources moved from arrays under `mcp` to top-level records.** Use the new `defineTool` / `defineCollectionTool` / `defineGlobalTool` builders for tools, and `definePrompt` for prompts. Resources are plain objects in the top-level `resources` record. For tools, `parameters` (a Zod raw shape) became `input` (a Zod/Valibot schema or raw JSON Schema), and handlers take one object argument instead of positional ones. ```diff mcpPlugin({ - mcp: { - tools: [ - { - name: 'getPostScores', - description: 'Score recent posts', - parameters: z.object({ since: z.string() }).shape, - handler: async (args, req) => ({ content: [...] }), - }, - ], - }, + tools: { + getPostScores: defineTool({ + description: 'Score recent posts', + input: z.object({ since: z.string() }), + }).handler(async ({ input, req }) => ({ content: [...] })), + }, }) ``` Prompts and resources move the same way, into top-level `prompts` and `resources` records. Tool handlers now receive `authorizedMCP` in that object argument. It contains the authorized MCP items and the `overrideAccess` value, but not the user. `req.user` is now the single source of truth for the caller's identity. Unlike the v3 MCP API-key resolver, the default v4 resolver assigns the authenticated user to `req.user` before custom handlers run. If a custom tool calls Payload's local API, pass `req` and `authorizedMCP.overrideAccess`. The local API reads the user from `req.user`: ```diff const result = await req.payload.find({ collection: 'posts', req, - overrideAccess: false, + overrideAccess: authorizedMCP.overrideAccess, }) ``` Collection and global tool calls now use `slug` instead of `collectionSlug` or `globalSlug`. Update saved tool inputs and any code that calls these tools directly. For custom collection and global tools, do not add `slug` yourself. Payload adds it to the MCP schema automatically, and the handler receives the resolved slug as a top-level argument. ```ts defineCollectionTool({ description: 'Publish a draft post by ID.', input: z.object({ id: z.string(), }), }).handler(async ({ slug, input, req }) => { // slug is resolved from the tool call }) ``` **Built-in collection tools are now generic.** Per-collection names like `createPosts` / `updatePosts` were replaced by `createDocuments` / `updateDocument`. Pass the target collection as `slug`. For creates, pass a `documents` array whose items contain `data` and an optional `file`; for updates, pass `data` with an `id` or `where` at the top level. Options such as `depth`, `draft`, and `locale` also remain at the top level. `_status`, `id`, `createdAt`, and `updatedAt` are no longer accepted inside `data`; set publish state with a custom tool if you need to. ```ts // arguments to a `createDocuments` tool call // before { title: 'Hello', _status: 'draft', draft: true, depth: 2 } // after { slug: 'posts', documents: [{ data: { title: 'Hello' } }], draft: true, depth: 2, } ``` `createDocuments` and `updateDocument` now return affected document IDs instead of complete documents by default. Add `returning: true` to preserve the previous response. If the call uses `select`, it must also set `returning: true`. **MCP URL uploads require an explicit allow-list.** Previously, MCP could download a URL when the upload collection's `pasteURL` was unset. Configure `upload.pasteURL.allowList` for collections that accept MCP URL uploads. The initial URL and any redirect targets must match; `skipSafeFetch` does not bypass this requirement. Base64 and upload-reference inputs are unaffected. ```ts upload: { pasteURL: { allowList: [{ hostname: 'cdn.example.com', protocol: 'https' }], }, } ``` **Auth tools are per-collection and opt-in.** The old `experimental.tools.auth` block is gone. Opt in per auth collection via its `tools` map, and pass the target auth collection as `slug`. Like every MCP item, auth tools now use default MCP access unless you provide an `access` callback, so unauthenticated auth tools such as `login` need `access: () => true`. ```diff - mcpPlugin({ experimental: { tools: { auth: { enabled: true } } } }) + mcpPlugin({ + collections: { + users: { + tools: { + login: { access: () => true }, + }, + }, + }, + }) ``` **`mcp.handlerOptions` is gone.** `verboseLogs` moved to `mcp.verboseLogs`. `onEvent`, `maxDuration`, `disableSse`, `redisUrl` and `basePath` were removed. The `experimental.tools` block (auth, plus the collection / job / config codegen tools) was removed entirely. If you opted into SSE/server-push with `disableSse: false`, remove that config; v4 only supports POST streamable HTTP. **`overrideAuth` was replaced by `overrideGetAuthorizedMCP`.** The hook receives one argument object and returns an `AuthorizedMCP` instead of the old `MCPAccessSettings` type. In v3, the resolved user was stored on `MCPAccessSettings.user`. In v4, a custom resolver must assign the user directly to `req.user` and return only `items` and `overrideAccess`. Always assign `req.user`, including `null` for an anonymous caller. The custom hook replaces the default authentication and item filtering, so it must also return only the MCP items that caller may use. ```diff mcpPlugin({ - overrideAuth: async (req, getDefaultMcpAccessSettings) => { - return getDefaultMcpAccessSettings() - }, + overrideGetAuthorizedMCP: async ({ overrideAccess, pluginConfig, req }) => { + const user = await authenticateExternalRequest(req) + req.user = user ?? null + + return { + items: req.user ? pluginConfig.items : [], + overrideAccess, + } + }, }) ``` The default v4 resolver assigns `req.user` automatically when it authenticates a normal Payload user API key. Only custom `overrideGetAuthorizedMCP` implementations need to assign it themselves. **MCP now uses user API keys.** The `payload-mcp-api-keys` collection was removed. API keys sent to MCP now use Payload's normal `Authorization: API-Key ` header and Payload's normal `auth.useAPIKey` fields. Enable `auth.useAPIKey` on the auth collection whose users should call MCP. Existing `payload-mcp-api-keys` documents no longer authenticate, and `overrideApiKeyCollection` was removed. The `userCollection` option was also removed. MCP now accepts any Payload user authenticated through API-key auth, using the collection slug from the `Authorization` header. Because the `payload-mcp-api-keys` collection config was removed, run your normal database migration workflow after upgrading so the old collection/table is removed where your database adapter requires schema migrations. Back up any existing MCP API key documents first if you need to reference them while recreating credentials as user API keys. **MCP tool access is configured in code.** The old per-key permissions UI was removed with the MCP API-key collection. Tools, prompts, and resources can now define an `access` callback; built-in collection/global tools accept `access` in their override object. By default, MCP items require an authenticated user. Built-in collection and global tools also include Payload permission checks in their default `access` callbacks. If you provide an `access` callback for a built-in tool, it replaces the default callback. **Dependencies.** The plugin now builds on `@modelcontextprotocol/server` instead of `@modelcontextprotocol/sdk` + `mcp-handler`. It no longer depends on Zod directly; its built-in tools use the Zod Mini export from `payload`. If your custom tools use regular Zod, install Zod v4 in your project and import it directly. **HTTP requests have a size limit.** `POST /api/mcp` now answers request bodies over 4 MiB with `413`. Base64 file uploads count toward this limit. Raise it with `mcp.maxRequestBodySize`. ### Admin UI styles now use CSS and semantic tokens The Admin UI styles were migrated from SCSS to plain CSS. The `@payloadcms/ui/scss` package export and its SCSS source files have been removed, and the legacy `--theme-*` CSS variables were retired in favor of semantic `--color-*` tokens and the raw `--ramp-*` color palette. **Stylesheet imports.** SCSS partials and mixins are gone. All design tokens are now global CSS variables on `:root`, so custom stylesheets no longer need to import anything to consume them — remove any `@import '~@payloadcms/ui/scss';` line. If you referenced the package export directly, use the CSS export instead: ```diff - @import '~@payloadcms/ui/scss'; ``` ```diff - @payloadcms/ui/scss + @payloadcms/ui/css - @payloadcms/ui/scss/app.scss + @payloadcms/ui/css/app.css ``` **Token renames.** The `--theme-*` variables were replaced by semantic `--color-*` tokens. Update any custom CSS that referenced them: ```diff .my-component { - background-color: var(--theme-bg); - color: var(--theme-text); + background-color: var(--color-bg); + color: var(--color-text); } ``` The `--theme-elevation-*` scale was removed. Use the semantic `--color-*` tokens (e.g. `--color-bg`, `--color-text`, `--color-border`, `--color-icon`) or the raw `--ramp-*` palette instead. See [Customizing CSS](/docs/v4/admin/customizing-css.md) for the full list of available tokens. Any custom component `.scss` files you authored should be renamed to `.css` and updated to plain CSS syntax — Payload no longer ships a SCSS toolchain. ### Ecommerce Stripe confirmations use an atomic transaction claim Stripe confirmation now atomically claims the existing ecommerce transaction before creating an order, marking the cart as purchased, and decrementing inventory. Retries return the canonical order only when the succeeded transaction and order are linked in both directions. Ambiguous state fails closed. PostgreSQL users must generate and run a database migration that adds `processing` to the ecommerce transaction status enum. No order field, unique index, or data backfill is required. The public `confirm-order` response continues to include `transactionID`, including on retries. This is a breaking security change for custom payment adapters: after provider validation, they must call the new `finalizeOrder` argument instead of creating orders themselves. The bundled Stripe adapter is updated in the same release. See the [payment confirmation guidance](/docs/v4/ecommerce/payments.md#confirmation-retries) for transactionless database failure behavior and reconciliation guidance. ### Stripe REST proxy requires an explicit method allowlist `@payloadcms/plugin-stripe` no longer accepts `rest: true`. To enable the REST proxy in v4, use an object with a non-empty `allowedMethods` array of exact Stripe SDK method names, such as `subscriptions.list`: ```diff stripePlugin({ - rest: true, + rest: { + allowedMethods: ['subscriptions.list'], + }, }) ``` Wildcards are not supported. The optional `rest.access` callback can further restrict the endpoint. Without it, the proxy requires an authenticated user allowed to access the Payload admin panel. See the [Stripe REST proxy documentation](/docs/v4/plugins/stripe.md#stripe-rest-proxy) for configuration and security guidance. If you previously used `rest: false`, remove the property; omitting `rest` disables the proxy. ### API keys: v4-only changes on top of the shared 3.x/4 security fix - `apiKeyLast4` is a new persisted field on every `auth.useAPIKey` collection. Generate and run a schema migration. Legacy rows backfill on the next successful API-key authentication. - `enableAPIKey` is removed. Revoke by setting `apiKey: null`. - `hasAPIKey` (3.x virtual field) is removed. Use `apiKeyLast4` and `docPermissions.fields.apiKey`. - Custom `access.read` on the generated `apiKey` field is inert — reads are hardcoded to deny. ### Uploads now reject oversized files by default The Payload-wide [`upload.abortOnLimit`](/docs/v4/upload/overview.md#payload-wide-upload-options) option now defaults to `true`. A file that is bigger than the configured `fileSize` limit now returns an HTTP 413 error by default. Previously, the file was truncated to the limit without any error, and the upload continued. If your application depends on the old truncate-on-limit behavior, set `abortOnLimit: false` explicitly: ```diff export default buildConfig({ upload: { + abortOnLimit: false, limits: { fileSize: 5000000, }, }, }) ``` ### Nodemailer adapter now uses nodemailer 10 ([#18737](https://github.com/payloadcms/payload/pull/18737)) `@payloadcms/email-nodemailer` now depends on nodemailer 10, which ships its own types. If you pass your own `transport` created with nodemailer 9, it no longer type-checks against the adapter. Upgrade `nodemailer` and remove `@types/nodemailer`: ```diff "dependencies": { - "nodemailer": "^9.1.1" + "nodemailer": "^10.1.1" }, "devDependencies": { - "@types/nodemailer": "^8.0.0" } ``` If you use `transportOptions`, or pass no config, no changes are needed. # Versioning and Breaking Changes Policy Source: https://payloadcms.com/docs/beta/migration-guide/versioning Payload follows [Semantic Versioning](https://semver.org/). This document explains what that means in practice: which parts of Payload are covered, what counts as a breaking change, and what is excluded from our breaking changes policy. ## Summary | Release | May contain | Upgrade expectation | | ----------------------------- | ------------------------------------- | -------------------------------------------------------------------------------- | | **Major** (`3.x` → `4.0`) | Breaking changes | Follow the [migration guide](/docs/v4/migration-guide/overview.md) and run the codemod | | **Minor** (`4.1` → `4.2`) | New features, deprecations, bug fixes | Usually safe to upgrade without code changes (see exceptions below) | | **Patch** (`4.2.0` → `4.2.1`) | Bug fixes | Guaranteed safe to upgrade without code changes | Minor releases can still change anything listed under [Exceptions](#exceptions). ## What counts as breaking A change is breaking if it makes working code fail or behave differently, as long as that code only relies on documented behavior. This includes changing a default (e.g. 4.0 lowered the default `depth` to `1`), requiring a database migration for an existing config to continue working, or changing types so correct code no longer compiles. Adding things isn't breaking, unless you have to implement them, like a new required method on an adapter interface. ## Exceptions We keep breaking changes in minor releases to a minimum, but the following can still change: - **Internals.** `payload/internal`, anything marked `@internal`, undocumented exports, imports from `dist/`, and undocumented subpaths like `@payloadcms/ui/utilities/*`. These exist so our own packages can share code. - **Experimental and beta APIs.** Anything marked `@experimental` in its types, or labeled experimental or beta in the docs. Once the label is removed, the usual rules apply. - **Prereleases.** Canary and beta builds make no promises to each other. Migration guides only cover changes between stable versions. - **Admin UI markup.** DOM structure, class names and layout may change between releases. Relying on them in custom CSS or JavaScript is at your own risk. Documented [CSS variables](/docs/v4/admin/customizing-css.md), including design tokens, are covered by our breaking changes policy. - **Wording.** Error messages, logs and translation strings. - **Bugs.** Fixing unintended behavior isn't a breaking change, even if your project relies on it. Examples include correcting types to match runtime behavior or applying a configured default that was previously ignored. We'll call out fixes that are likely to affect a lot of projects. - **Generated database indexes.** Payload may add, rename, or reorder generated indexes in minor or patch releases to improve performance, provided the change preserves documented behavior and existing databases continue working without a migration. Generated index names and column order are implementation details. You may need to generate and run a database migration to receive the performance improvement; that alone does not make the change breaking. - **Security fixes.** If we can't fix a vulnerability without changing behavior, the fix ships anyway. Where we can, we'll give you a way to keep the old behavior, but this may not always be possible. To report a vulnerability, see our [security policy](https://github.com/payloadcms/payload/blob/main/SECURITY.md). - **Dependencies.** Most dependency upgrades don't affect you, but some need changes on your end. We may still upgrade or replace a dependency when it needs a security fix, or proactively when it reaches end of life or stops being maintained, since staying on an unmaintained dependency is a risk in itself. Common examples: - **Node.js.** Once a Node.js version reaches [end of life](https://github.com/nodejs/release#release-schedule), we may raise the minimum Node.js version, but only to the oldest version that's still supported. - **Next.js, TanStack Start and React.** We may raise the minimum supported version within the same major when required by a security fix or by upstream API changes that cannot reasonably be supported across both versions. Non-security changes only ship in Payload minor releases, use the breaking-change marker, and include migration instructions. We won't drop support for an upstream major outside a Payload major release. - **Lexical.** Lexical hasn't reached 1.0, so any of its releases can break things. If you use Lexical's APIs directly, for example through `@payloadcms/richtext-lexical/lexical/*`, check [Lexical's release notes](https://github.com/facebook/lexical/releases) when you upgrade. Payload's own rich text APIs are still covered. ## How breaking changes are announced Breaking commits and PR titles use the `!` marker (`feat!:`, `fix(storage-azure)!:`), and release notes list them under **⚠️ BREAKING CHANGES**. # The Payload Config Source: https://payloadcms.com/docs/beta/configuration/overview Payload is a _config-based_, code-first CMS and application framework. The Payload Config is central to everything that Payload does, allowing for deep configuration of your application through a simple and intuitive API. The Payload Config is a fully-typed JavaScript object that can be infinitely extended upon. Everything from your [Database](/docs/v4/database/overview.md) choice to the appearance of the [Admin Panel](/docs/v4/admin/overview.md) is fully controlled through the Payload Config. From here you can define [Fields](/docs/v4/fields/overview.md), add [Localization](/docs/v4/configuration/localization.md), enable [Authentication](/docs/v4/authentication/overview.md), configure [Access Control](/docs/v4/access-control/overview.md), and so much more. The Payload Config is a `payload.config.ts` file typically located in the root of your project: ```ts import { buildConfig } from 'payload' export default buildConfig({ // Your config goes here }) ``` The Payload Config is strongly typed and ties directly into Payload's TypeScript codebase. This means your IDE (such as VSCode) will provide helpful information like type-ahead suggestions while you write your config. > **Tip:** The location of your Payload Config can be customized. [More > details](#customizing-the-config-location). ## Config Options To author your Payload Config, first determine which [Database](/docs/v4/database/overview.md) you'd like to use, then use [Collections](/docs/v4/configuration/collections.md) or [Globals](/docs/v4/configuration/globals.md) to define the schema of your data through [Fields](/docs/v4/fields/overview.md). Here is one of the simplest possible Payload configs: ```ts import { buildConfig } from 'payload' import { mongooseAdapter } from '@payloadcms/db-mongodb' export default buildConfig({ secret: process.env.PAYLOAD_SECRET, db: mongooseAdapter({ url: process.env.DATABASE_URL, }), collections: [ { slug: 'pages', fields: [ { name: 'title', type: 'text', }, ], }, ], }) ``` > **Note:** For more complex examples, see the > [Templates](https://github.com/payloadcms/payload/tree/main/templates) and > [Examples](https://github.com/payloadcms/payload/tree/main/examples) > directories in the Payload repository. The following options are available: | Option | Description | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`admin`** | The configuration options for the Admin Panel, including Custom Components, Live Preview, etc. [More details](/docs/v4/admin/overview.md#admin-options). | | **`cli`** | Register custom Payload CLI commands. [More details](/docs/v4/configuration/cli.md). | | **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](/docs/v4/rich-text/overview.md). | | **`db`** \* | The Database Adapter which will be used by Payload. [More details](/docs/v4/database/overview.md). | | **`serverURL`** | A string used to define the absolute URL of your app. This includes the protocol, for example `https://example.com`. No paths allowed, only protocol, domain and (optionally) port. | | **`collections`** | An array of Collections for Payload to manage. [More details](/docs/v4/configuration/collections.md). | | **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). | | **`globals`** | An array of Globals for Payload to manage. [More details](/docs/v4/configuration/globals.md). | | **`cors`** | Cross-origin resource sharing (CORS) is a mechanism that accept incoming requests from given domains. You can also customize the `Access-Control-Allow-Headers` header. [More details](#cors). | | **`localization`** | Opt-in to translate your content into multiple locales. [More details](/docs/v4/configuration/localization.md). | | **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](#logger). | | **`loggingLevels`** | An object to override the level to use in the logger for Payload's errors. | | **`graphQL`** | Manage GraphQL-specific functionality, including custom queries and mutations, query complexity limits, etc. [More details](/docs/v4/graphql/overview.md#graphql-options). | | **`cookiePrefix`** | A string that will be prefixed to all cookies that Payload sets. | | **`csrf`** | A whitelist array of URLs to allow Payload to accept cookies from. [More details](/docs/v4/authentication/cookies.md#csrf-attacks). | | **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](/docs/v4/queries/depth.md). | | **`defaultMaxTextLength`** | The maximum allowed string length to be permitted application-wide. Helps to prevent malicious public document creation. | | `folders` | An optional object to configure global folder settings. [More details](/docs/v4/folders/overview.md). | | `queryPresets` | An object that to configure Collection Query Presets. [More details](/docs/v4/query-presets/overview.md). | | **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](/docs/v4/queries/depth.md). | | **`indexSortableFields`** | Automatically index all sortable top-level fields in the database to improve sort performance and add database compatibility for Azure Cosmos and similar. | | **`upload`** | Base Payload upload configuration. [More details](/docs/v4/upload/overview.md#payload-wide-upload-options). | | **`routes`** | Control the routing structure that Payload binds itself to. [More details](/docs/v4/admin/overview.md#root-level-routes). | | **`email`** | Configure the Email Adapter for Payload to use. [More details](/docs/v4/email/overview.md). | | **`onInit`** | A function that is called immediately following startup that receives the Payload instance as its only argument. | | **`debug`** | Enable to expose more detailed error information. | | **`telemetry`** | Disable Payload telemetry by passing `false`. [More details](#telemetry). | | **`hooks`** | An array of Root Hooks. [More details](/docs/v4/hooks/overview.md). | | **`plugins`** | An array of Plugins. [More details](/docs/v4/plugins/overview.md). | | **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](/docs/v4/rest-api/overview.md#custom-endpoints). | | **`custom`** | Extension point for adding custom data (e.g. for plugins). | | **`i18n`** | Internationalization configuration. Pass all i18n languages you'd like the admin UI to support. Defaults to English-only. [More details](/docs/v4/configuration/i18n.md). | | **`secret`** \* | A secure, unguessable string that Payload will use for any encryption workflows - for example, password salt / hashing. | | **`sharp`** | If you would like Payload to offer cropping, focal point selection, and automatic media resizing, install and pass the Sharp module to the config here. | | **`typescript`** | Configure TypeScript settings here. [More details](#typescript). | _\* An asterisk denotes that a property is required._ > **Note:** Some properties are removed from the client-side bundle. [More > details](/docs/v4/custom-components/overview.md#accessing-the-payload-config). ### TypeScript Config Payload exposes a variety of TypeScript settings that you can leverage. These settings are used to auto-generate TypeScript interfaces for your [Collections](/docs/v4/configuration/collections.md) and [Globals](/docs/v4/configuration/globals.md), and to ensure that Payload uses your [Generated Types](/docs/v4/typescript/overview.md) for all [Local API](/docs/v4/local-api/overview.md) methods. To customize the TypeScript settings, use the `typescript` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... // highlight-start typescript: { // ... }, // highlight-end }) ``` The following options are available: | Option | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`autoGenerate`** | By default, Payload will auto-generate TypeScript interfaces for all collections and globals that your config defines. Opt out by setting `typescript.autoGenerate: false`. [More details](/docs/v4/typescript/overview.md). | | **`generateInputTypes`** | Opt in (defaults to `false`) to also generate a write-shaped input type (e.g. `PostInput`) alongside each read type, by setting `typescript.generateInputTypes: true`. [More details](/docs/v4/typescript/generating-types.md#input-and-output-types). | | **`declare`** | By default, Payload adds a `declare` block to your generated types, which makes sure that Payload uses your generated types for all Local API methods. Opt out by setting `typescript.declare: false`. | | **`outputFile`** | Control the output path and filename of Payload's auto-generated types by defining the `typescript.outputFile` property to a full, absolute path. | | **`strictDraftTypes`** | Enable strict type safety for draft mode. When enabled: (1) Query operations (`find`, `findByID`) with `draft: true` will type required fields as optional, since validation is skipped for drafts. (2) The `draft` property is forbidden for collections without drafts in create operations. (3) Create operations enforce proper data requirements via discriminated unions. Defaults to `false`. **This will become the default behavior in v4.0.** | ## Config Location For Payload command-line scripts, we need to be able to locate your Payload Config. We'll check a variety of locations for the presence of `payload.config.ts` by default, including: 1. The root current working directory 1. The `compilerOptions` in your `tsconfig`\* 1. The `dist` directory\* _\* Config location detection is different between development and production environments. See below for more details._ > **Important:** Ensure your `tsconfig.json` is properly configured for Payload > to auto-detect your config location. If it does not exist, or does not specify > the proper `compilerOptions`, Payload will default to the current working > directory. **Development Mode** In development mode, if the configuration file is not found at the root, Payload will attempt to read your `tsconfig.json`, and attempt to find the config file specified in the `rootDir`: ```json { // ... // highlight-start "compilerOptions": { "rootDir": "src" } // highlight-end } ``` **Production Mode** In production mode, Payload will first attempt to find the config file in the `outDir` of your `tsconfig.json`, and if not found, will fallback to the `rootDir` directory: ```json { // ... // highlight-start "compilerOptions": { "outDir": "dist", "rootDir": "src" } // highlight-end } ``` If none was in either location, Payload will finally check the `dist` directory. ### Customizing the Config Location In addition to the above automated detection, you can specify your own location for the Payload Config. This can be useful in situations where your config is not in a standard location, or you wish to switch between multiple configurations. To do this, Payload exposes an [Environment Variable](/docs/v4/configuration/environment-vars.md) to bypass all automatic config detection. To use a custom config location, set the `PAYLOAD_CONFIG_PATH` environment variable: ```json { "scripts": { "payload": "PAYLOAD_CONFIG_PATH=/path/to/custom-config.ts payload" } } ``` > **Tip:** `PAYLOAD_CONFIG_PATH` can be either an absolute path, or path > relative to your current working directory. ## Telemetry Payload collects **completely anonymous** telemetry data about general usage. This data is super important to us and helps us accurately understand how we're growing and what we can do to build the software into everything that it can possibly be. The telemetry that we collect also help us demonstrate our growth in an accurate manner, which helps us as we seek investment to build and scale our team. If we can accurately demonstrate our growth, we can more effectively continue to support Payload as free and open-source software. To opt out of telemetry, you can pass `telemetry: false` within your Payload Config. For more information about what we track, take a look at our [privacy policy](/privacy). ## Cross-origin resource sharing (CORS)#cors Cross-origin resource sharing (CORS) can be configured with either a whitelist array of URLS to allow CORS requests from, a wildcard string (`*`) to accept incoming requests from any domain, or an object with the following properties: | Option | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **`origins`** | Either a whitelist array of URLS to allow CORS requests from, or a wildcard string (`'*'`) to accept incoming requests from any domain. | | **`headers`** | A list of allowed headers that will be appended in `Access-Control-Allow-Headers`. | Here's an example showing how to allow incoming requests from any domain: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... // highlight-start cors: '*', // highlight-end }) ``` Here's an example showing how to append a new header (`x-custom-header`) in `Access-Control-Allow-Headers`: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... // highlight-start cors: { origins: ['http://localhost:3000'], headers: ['x-custom-header'], }, // highlight-end }) ``` ## TypeScript You can import types from Payload to help make writing your config easier and type-safe. There are two main types that represent the Payload Config, `Config` and `SanitizedConfig`. The `Config` type represents a raw Payload Config in its full form. Only the bare minimum properties are marked as required. The `SanitizedConfig` type represents a Payload Config after it has been fully sanitized. Generally, this is only used internally by Payload. ```ts import type { Config, SanitizedConfig } from 'payload' ``` ## Server vs. Client The Payload Config only lives on the server and is not allowed to contain any client-side code. That way, you can load up the Payload Config in any server environment or standalone script, without having to use Bundlers or Node.js loaders to handle importing client-only modules (e.g. scss files or React Components) without any errors. Behind the curtains, the Admin Panel generates a ClientConfig, which strips away any server-only code and enriches the config with React Components. ## Logger Payload uses [Pino](https://getpino.io) as its logger. By default, Payload logs pretty-printed output to stdout. You can customise this through the `logger` option in your config. The `logger` option accepts one of three forms: ### 1. Logger options with an optional destination stream Pass a `{ options, destination? }` object to configure the logger. `options` accepts standard [Pino logger options](https://getpino.io/#/docs/api?id=options) such as `level` and `name`. An optional `destination` stream can be supplied as a second argument. ```ts import { buildConfig } from 'payload' export default buildConfig({ // highlight-start logger: { options: { level: 'debug', }, }, // highlight-end }) ``` To write logs to a file, pass a destination stream: ```ts import { buildConfig } from 'payload' import { pino } from 'pino' export default buildConfig({ // highlight-start logger: { options: { level: 'info' }, destination: pino.destination('/var/log/payload.log'), }, // highlight-end }) ``` > **Compatibility note:** The `transport` property of Pino's `LoggerOptions` may > fail with `"unable to determine transport target"` in some environments. Pino > transports spawn a worker thread with their own module resolution, which can > break when modules are bundled or the project uses ESM. If you encounter this > error, use a pre-instantiated logger instead (see below). ### 2. Pre-instantiated logger Pass a fully configured Pino logger instance. This is the recommended approach when you need custom transports, formatters, or `pino-pretty` in development: ```ts import { buildConfig } from 'payload' import { pino } from 'pino' import pinoPretty from 'pino-pretty' const logger = pino({ level: 'debug' }, pinoPretty({ colorize: true })) export default buildConfig({ // highlight-start logger, // highlight-end }) ``` ### 3. Disabling logging Set the `DISABLE_LOGGING` environment variable to `'true'` to suppress all log output: ```sh DISABLE_LOGGING=true node server.js ``` # Collection Configs Source: https://payloadcms.com/docs/beta/configuration/collections A Collection is a group of records, called Documents, that all share a common schema. You can define as many Collections as your application needs. Each Document in a Collection is stored in the [Database](/docs/v4/database/overview.md) based on the [Fields](/docs/v4/fields/overview.md) that you define, and automatically generates a [Local API](/docs/v4/local-api/overview.md), [REST API](/docs/v4/rest-api/overview.md), and [GraphQL API](/docs/v4/graphql/overview.md) used to manage your Documents. Collections are also used to achieve [Authentication](/docs/v4/authentication/overview.md) in Payload. By defining a Collection with `auth` options, that Collection receives additional operations to support user authentication. Collections are the primary way to structure recurring data in your application, such as users, products, pages, posts, and other types of content that you might want to manage. Each Collection can have its own unique [Access Control](/docs/v4/access-control/overview.md), [Hooks](/docs/v4/hooks/overview.md), [Admin Options](#admin-options), and more. To define a Collection Config, use the `collections` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... collections: [ // highlight-line // Your Collections go here ], }) ``` > **Tip:** If your Collection is only ever meant to contain a single Document, > consider using a [Global](/docs/v4/configuration/globals.md) instead. ## Config Options It's often best practice to write your Collections in separate files and then import them into the main [Payload Config](/docs/v4/configuration/overview.md). Here is what a simple Collection Config might look like: ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', fields: [ { name: 'title', type: 'text', }, ], } ``` > **Reminder:** For more complex examples, see the > [Templates](https://github.com/payloadcms/payload/tree/main/templates) and > [Examples](https://github.com/payloadcms/payload/tree/main/examples) > directories in the Payload repository. The following options are available: | Option | Description | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `admin` | The configuration options for the Admin Panel. [More details](#admin-options). | | `access` | Provide Access Control functions to define exactly who should be able to do what with Documents in this Collection. [More details](/docs/v4/access-control/collections.md). | | `auth` | Specify options if you would like this Collection to feature authentication. [More details](/docs/v4/authentication/overview.md). | | `authorship` | Automatically track the user that created and last updated each document via polymorphic `createdBy` / `updatedBy` fields. Enabled by default. Set to `false` to disable, or to `{ createdBy: false }` / `{ updatedBy: false }` to toggle each field. [More details](#authorship). | | `custom` | Extension point for adding custom data (e.g. for plugins) | | `disableDuplicate` | When true, do not show the "Duplicate" button while editing documents within this Collection and prevent `duplicate` from all APIs. | | `defaultSort` | Pass a top-level field to sort by default in the Collection List View. Prefix the name of the field with a minus symbol ("-") to sort in descending order. Multiple fields can be specified by using a string array. | | `dbName` | Custom table or Collection name depending on the Database Adapter. Auto-generated from slug if not defined. | | `endpoints` | Add custom routes to the REST API. Set to `false` to disable routes. [More details](/docs/v4/rest-api/overview.md#custom-endpoints). | | `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](/docs/v4/fields/overview.md). | | `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) | | `hooks` | Entry point for Hooks. [More details](/docs/v4/hooks/collections.md). | | `hierarchy` | Enable hierarchical tree structure with automatic parent-child relationships and path generation. [More details](/docs/v4/hierarchy/overview.md). | | `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [orderable](#orderable) for efficient reordering. | | `labels` | Singular and plural labels for use in identifying this Collection throughout Payload. Auto-generated from slug if not defined. | | `enableQueryPresets` | Enable query presets for this Collection. [More details](/docs/v4/query-presets/overview.md). | | `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](/docs/v4/admin/locked-documents.md). | | `slug` \* | Unique, URL-friendly string that will act as an identifier for this Collection. | | `timestamps` | Set to false to disable documents' automatically generated `createdAt` and `updatedAt` timestamps. | | `trash` | A boolean to enable soft deletes for this collection. Defaults to `false`. [More details](/docs/v4/trash/overview.md). | | `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. | | `upload` | Specify options if you would like this Collection to support file uploads. For more, consult the [Uploads](/docs/v4/upload/overview.md) documentation. | | `versions` | Set to true to enable default options, or configure with object properties. [More details](/docs/v4/versions/overview.md#collection-config). | | `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](/docs/v4/queries/select.md#defaultpopulate-collection-config-property). | | `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. | | `select` | Function that receives the current `operation`, `req`, and caller's `select`, and returns the final `select` to apply. Useful for forcing fields to be populated for hooks / access control. [More details](/docs/v4/queries/select.md#entity-level-select-config). | | `disableBulkDelete` | Disable the bulk delete operation for the collection in the admin panel and the REST API | | `disableBulkEdit` | Disable the bulk edit operation for the collection in the admin panel and the REST API | _\* An asterisk denotes that a property is required._ ### Fields Fields define the schema of the Documents within a Collection. To learn more, go to the [Fields](/docs/v4/fields/overview.md) documentation. ### Access Control [Collection Access Control](/docs/v4/access-control/collections.md) determines what a user can and cannot do with any given Document within a Collection. To learn more, go to the [Access Control](/docs/v4/access-control/overview.md) documentation. ### Hooks [Collection Hooks](/docs/v4/hooks/collections.md) allow you to tie into the lifecycle of your Documents so you can execute your own logic during specific events. To learn more, go to the [Hooks](/docs/v4/hooks/overview.md) documentation. ### Orderable When `orderable` is enabled, Payload uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) to efficiently manage document order. When enabled on collections, this allows you to manually drag and drop documents in the Admin Panel to reorder them, as well as programmatically set the order of documents via the Local API, REST API, or GraphQL API. Orderable can also be added to [joins fields](/docs/v4/fields/join.md). #### Fractional indexing If you need to generate order keys programmatically (e.g., when creating documents via the Local API with a specific order), you can import the utilities directly: ```ts import { generateKeyBetween, generateNKeysBetween } from 'payload/shared' // Generate a key between two existing keys (or null for start/end) const newKey = generateKeyBetween('a0', 'a1') // Generate a key at the start (before 'a0') const startKey = generateKeyBetween(null, 'a0') // Generate a key at the end (after 'a1') const endKey = generateKeyBetween('a1', null) // Generate multiple keys between two existing keys const keys = generateNKeysBetween('a0', 'a1', 5) ``` These utilities are useful when you need fine-grained control over document ordering, such as inserting documents at specific positions or batch-creating documents with predetermined order. ## Authorship Payload automatically tracks the user that created and last updated each document through two polymorphic relationship fields, `createdBy` and `updatedBy`. This is the authorship equivalent of the `createdAt` / `updatedAt` [timestamps](#config-options). Both fields relate to every authentication-enabled collection in your config, so setups with multiple user collections are supported out of the box. Their value is stored as `{ relationTo, value }`: ```ts { // ... createdBy: { relationTo: 'users', value: '648b...' }, updatedBy: { relationTo: 'admins', value: '92f1...' }, } ``` Authorship is **enabled by default**. Control it per-collection (and per-global) with the `authorship` option: ```ts export const Posts: CollectionConfig = { slug: 'posts', // Disable both fields authorship: false, // Or toggle each field independently // authorship: { updatedBy: false }, fields: [ // ... ], } ``` How the values are set: - On create, both `createdBy` and `updatedBy` are set from the authenticated user on the request (`req.user`). Passing either field in your `data` overrides the system-derived value. - On update, `updatedBy` is set from `req.user`; passing `updatedBy` in your `data` overrides it, and passing `null` clears it. `createdBy` is immutable once set, so passing it on update has no effect. - When there is no user on the request (e.g. a Local API call without `req.user`), the existing value is left unchanged. > **Warning** > > These fields are hidden from the document edit view but are available in the > API response and the versions view. Because they relate to your auth > collections, enabling authorship adds new database columns — existing projects > will need a migration when upgrading. Use the > [`@payloadcms/codemod`](https://github.com/payloadcms/payload/tree/main/packages/codemod) > `migrate-authorship-default` transform to add `authorship: false` everywhere > and preserve the previous behaviour. ### Customizing the authorship fields To change how `createdBy` / `updatedBy` are rendered — for example to make them visible, relabel them, or move them into the sidebar — add the exported `createCreatedByField` / `createUpdatedByField` builders to your collection's `fields`. Fields you add this way are used as-is (Payload won't re-inject its own), and the stamping hooks are preserved: ```ts import { createCreatedByField, createUpdatedByField } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', fields: [ // ...your fields createCreatedByField({ overrides: { label: 'Author', admin: { hidden: false }, }, }), // updatedBy is still auto-injected with its defaults — only add the builder for // the field you want to customize. ], } ``` `overrides` win for `label`, `relationTo`, and other top-level props; `admin` and `access` are merged onto the secure defaults, and the stamping hooks are always kept (any hooks you pass are appended). The polymorphic `relationTo` is filled in automatically with your auth-enabled collections during sanitization, so you don't need to enumerate them. If instead you define your own raw `createdBy` / `updatedBy` field (not via these builders), Payload accepts it wholesale and does **not** attach the stamping hooks — you're responsible for setting the value. ## Admin Options The behavior of Collections within the [Admin Panel](/docs/v4/admin/overview.md) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](/docs/v4/custom-components/overview.md), selecting which fields to display in the List View, and more. To configure Admin Options for Collections, use the `admin` property in your Collection Config: ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { // highlight-line // ... }, } ``` The following options are available: | Option | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `group` | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide it from the sidebar, command palette, Collection Cards, pin picker, and Pinned and Recently viewed tabs (including existing pins), while keeping its routes accessible. | | `hidden` | Set to true or a function, called with the current user, returning true to exclude this Collection from navigation and admin routing. | | `useAsTitle` | Specify a top-level field to use for a document title throughout the Admin Panel. If no field is defined, the ID of the document is used as the title. Computed virtual fields (`virtual: true`) cannot be used — only relationship-path virtual fields (e.g. `virtual: 'author.name'`) are supported. | | `useAsThumbnail` | Upload field for document thumbnails in grid views and the Recents and Pinned dashboard widget. Defaults to the first upload field. Explicit values must name a top-level upload field; a document icon is shown when no thumbnail is available. | | `description` | Text to display below the Collection label in the List View to give editors more information. Alternatively, you can use the `admin.components.Description` to render a React component. [More details](#custom-components). | | `defaultColumns` | Array of field names that correspond to which columns to show by default in this Collection's List View. | | `disableCopyToLocale` | Disables the "Copy to Locale" button while editing documents within this Collection. Only applicable when localization is enabled. | | `enableRichTextLink` | The [Rich Text](/docs/v4/fields/rich-text.md) field features a `Link` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. | | `enableRichTextRelationship` | The [Rich Text](/docs/v4/fields/rich-text.md) field features a `Relationship` element which allows for users to automatically reference related documents within their rich text. Set to `true` by default. | | `folders` | A boolean to enable folders for a given collection. Defaults to `false`. [More details](/docs/v4/folders/overview.md). | | `formatDocURL` | Function to customize document links in the List View. Return `null` to disable linking, or a string for custom URLs. [More details](#format-document-urls). | | `meta` | Page metadata overrides to apply to this Collection within the Admin Panel. [More details](/docs/v4/admin/metadata.md). | | `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](/docs/v4/admin/preview.md). | | `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](/docs/v4/live-preview/overview.md). | | `components` | Swap in your own React components to be used within this Collection. [More details](#custom-components). | | `listSearchableFields` | Specify which fields should be searched in the List search view. [More details](#list-searchable-fields). | | `pagination` | Set pagination-specific options for this Collection in the List View. [More details](#pagination). | | `baseFilter` | Defines a default base filter which will be applied to the List View (along with any other filters applied by the user) and internal links in Lexical Editor, | > **Using a relationship or join field as `useAsTitle`:** Payload will display > only the ID of the related document. To show a specific field from the related > document instead, define a relationship-path virtual field (e.g. `virtual: > 'author.name'`) and point `useAsTitle` to that field. Note that this is the > _only_ type of virtual field supported by `useAsTitle` — computed virtual > fields (`virtual: true`) are not queryable and cannot be used. ### Custom Components Collections can set their own [Custom Components](/docs/v4/custom-components/overview.md) which only apply to Collection-specific UI within the [Admin Panel](/docs/v4/admin/overview.md). This includes elements such as the Save Button, or entire layouts such as the Edit View. To override Collection Components, use the `admin.components` property in your Collection Config: ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-line // ... }, }, } ``` The following options are available: | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `afterList` | An array of components to inject _after_ the built-in List View. [More details](/docs/v4/custom-components/list-view.md#afterlist). | | `afterListTable` | An array of components to inject _after_ the built-in List View's table. [More details](/docs/v4/custom-components/list-view.md#afterlisttable). | | `beforeList` | An array of components to inject _before_ the built-in List View. [More details](/docs/v4/custom-components/list-view.md#beforelist). | | `beforeListTable` | An array of components to inject _before_ the built-in List View's table. [More details](/docs/v4/custom-components/list-view.md#beforelisttable). | | `listMenuItems` | An array of components to render within a menu next to the List Controls (after the Columns and Filters options) | | `Description` | A component to render below the Collection label in the List View. An alternative to the `admin.description` property. [More details](/docs/v4/custom-components/list-view.md#description). | | `edit` | Override specific components within the Edit View. [More details](#edit-view-options). | | `views` | Override or create new views within the Admin Panel. [More details](/docs/v4/custom-components/custom-views.md). | #### Edit View Options Custom components within the Edit View are configured under `admin.components.edit`: ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { edit: { // highlight-start Status: '/path/to/CustomStatus', SaveButton: '/path/to/CustomSaveButton', PublishButton: '/path/to/CustomPublishButton', // highlight-end }, }, }, } ``` The following components can be customized within `admin.components.edit`: | Option | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](/docs/v4/custom-components/edit-view.md#beforedocumentcontrols). | | `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document controls bar. [More details](/docs/v4/custom-components/edit-view.md#editmenuitems). | | `SaveButton` | Replace the default Save Button within the Edit View. [Drafts](/docs/v4/versions/drafts.md) must be disabled. [More details](/docs/v4/custom-components/edit-view.md#savebutton). | | `SaveDraftButton` | Replace the default Save Draft Button within the Edit View. [Drafts](/docs/v4/versions/drafts.md) must be enabled and autosave must be disabled. [More details](/docs/v4/custom-components/edit-view.md#savedraftbutton). | | `Status` | Replace the default Status component within the Edit View. [Drafts](/docs/v4/versions/drafts.md) must be enabled. [More details](/docs/v4/custom-components/edit-view.md#status). | | `PublishButton` | Replace the default Publish Button within the Edit View. [Drafts](/docs/v4/versions/drafts.md) must be enabled. [More details](/docs/v4/custom-components/edit-view.md#publishbutton). | | `UnpublishButton` | Replace the default Unpublish Button within the Edit View. [Drafts](/docs/v4/versions/drafts.md) must be enabled. [More details](/docs/v4/custom-components/edit-view.md#unpublishbutton). | | `PreviewButton` | Replace the default Preview Button within the Edit View. [Preview](/docs/v4/admin/preview.md) must be enabled. [More details](/docs/v4/custom-components/edit-view.md#previewbutton). | | `Upload` | Replace the default Upload component within the Edit View. [Upload](/docs/v4/upload/overview.md) must be enabled. [More details](/docs/v4/custom-components/edit-view.md#upload). | > **Important:** All edit view components must be configured under > `admin.components.edit`, not directly under `admin.components`. > > For example, use `admin.components.edit.Status`, not `admin.components.Status`. > **Note:** For details on how to build Custom Components, see [Building Custom > Components](/docs/v4/custom-components/overview.md#building-custom-components). ### Pagination All Collections receive their own List View which displays a paginated list of documents that can be sorted and filtered. The pagination behavior of the List View can be customized on a per-Collection basis, and uses the same [Pagination](/docs/v4/queries/pagination.md) API that Payload provides. To configure pagination options, use the `admin.pagination` property in your Collection Config: ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { // ... admin: { // highlight-start pagination: { defaultLimit: 10, limits: [10, 20, 50], }, // highlight-end }, } ``` The following options are available: | Option | Description | | -------------- | --------------------------------------------------------------------------------------------------- | | `defaultLimit` | Integer that specifies the default per-page limit that should be used. Defaults to 10. | | `limits` | Provide an array of integers to use as per-page options for admins to choose from in the List View. | ### List Searchable Fields In the List View, there is a "search" box that allows you to quickly find a document through a simple text search. By default, it searches on the ID field. If defined, the `admin.useAsTitle` field is used. Or, you can explicitly define which fields to search based on the needs of your application. To define which fields should be searched, use the `admin.listSearchableFields` property in your Collection Config: ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { // ... admin: { // highlight-start listSearchableFields: ['title', 'slug'], // highlight-end }, } ``` > **Tip:** If you are adding `listSearchableFields`, make sure you index each of > these fields so your admin queries can remain performant. ## List View Select API The List View uses the [Select API](/docs/v4/queries/select.md) to query only the _active_ columns as opposed to entire documents. This greatly improves performance, especially for collections with large documents or many fields. This may cause unexpected behavior in some cases, such as when using hooks that rely on the full document data. For example, if your component relies on a "title" field, this field will not be populated if the column is inactive: ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { // ... fields: [ // ... { name: 'myField', type: 'text', hooks: { afterRead: [ ({ doc }) => doc.title, // The `title` field will not be populated by default, unless the column is active ], }, }, ], } ``` To ensure title is always present, define a top-level [`select`](/docs/v4/queries/select.md#entity-level-select-config) function on your Collection Config that merges the caller's `select` with the fields you require: ```ts export const Posts: CollectionConfig = { // ... select: ({ select }) => ({ ...select, title: true }), } ``` ### Format Document URLs The `formatDocURL` function allows you to customize how document links are generated in the List View. This is useful for disabling links for certain documents, redirecting to custom destinations, or modifying URLs based on user context or document state. To define a custom document URL formatter, use the `admin.formatDocURL` property in your Collection Config: ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { // ... admin: { formatDocURL: ({ doc, defaultURL, req, collectionSlug, viewType }) => { // Disable linking for documents with specific status if (doc.status === 'private') { return null } // Custom destination for featured posts if (doc.featured) { return '/admin/featured-posts' } // Add query parameters based on user role if (req.user?.role === 'admin') { return defaultURL + '?admin=true' } // Use default URL for all other cases return defaultURL }, }, } ``` The `formatDocURL` function receives the following arguments: | Argument | Description | | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | `doc` | The document data for the current row | | `defaultURL` | The default URL that Payload would normally generate for this document. You can return this as-is, modify it, or replace it entirely. | | `req` | The full [PayloadRequest](/docs/v4/types/payload-request.md) object, providing access to user context, payload instance, and other request data | | `collectionSlug` | The slug of the current collection | | `viewType` | The current view context (`'list'`, `'trash'`, etc.) where the link is being generated | The function should return: - `null` to disable the link entirely (no link will be rendered) - A `string` containing the custom URL to use for the link - The `defaultURL` parameter to use Payload's default linking behavior > **Tip:** The `defaultURL` parameter saves you from having to reconstruct URLs > manually. You can modify it by appending query parameters or use it as a > fallback for your custom logic. #### Examples **Disable linking for certain users:** ```ts formatDocURL: ({ defaultURL, req }) => { if (req.user?.role === 'editor') { return null // No link rendered } return defaultURL } ``` ## GraphQL You can completely disable GraphQL for this collection by passing `graphQL: false` to your collection config. This will completely disable all queries, mutations, and types from appearing in your GraphQL schema. You can also pass an object to the collection's `graphQL` property, which allows you to define the following properties: | Option | Description | | ------------------ | ----------------------------------------------------------------------------------- | | `singularName` | Override the "singular" name that will be used in GraphQL schema generation. | | `pluralName` | Override the "plural" name that will be used in GraphQL schema generation. | | `disableQueries` | Disable all GraphQL queries that correspond to this collection by passing `true`. | | `disableMutations` | Disable all GraphQL mutations that correspond to this collection by passing `true`. | ## TypeScript You can import types from Payload to help make writing your Collection configs easier and type-safe. There are two main types that represent the Collection Config, `CollectionConfig` and `SanitizedCollectionConfig`. The `CollectionConfig` type represents a raw Collection Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedCollectionConfig` type represents a Collection Config after it has been fully sanitized. Generally, this is only used internally by Payload. ```ts import type { CollectionConfig, SanitizedCollectionConfig } from 'payload' ``` # Global Configs Source: https://payloadcms.com/docs/beta/configuration/globals Globals are in many ways similar to [Collections](/docs/v4/configuration/collections.md), except that they correspond to only a single Document. You can define as many Globals as your application needs. Each Global Document is stored in the [Database](/docs/v4/database/overview.md) based on the [Fields](/docs/v4/fields/overview.md) that you define, and automatically generates a [Local API](/docs/v4/local-api/overview.md), [REST API](/docs/v4/rest-api/overview.md), and [GraphQL API](/docs/v4/graphql/overview.md) used to manage your Documents. Globals are the primary way to structure singletons in Payload, such as a header navigation, site-wide banner alerts, or app-wide localized strings. Each Global can have its own unique [Access Control](/docs/v4/access-control/overview.md), [Hooks](/docs/v4/hooks/overview.md), [Admin Options](#admin-options), and more. To define a Global Config, use the `globals` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... globals: [ // highlight-line // Your Globals go here ], }) ``` > **Tip:** If you have more than one Global that share the same structure, > consider using a [Collection](/docs/v4/configuration/collections.md) instead. ## Config Options It's often best practice to write your Globals in separate files and then import them into the main [Payload Config](/docs/v4/configuration/overview.md). Here is what a simple Global Config might look like: ```ts import type { GlobalConfig } from 'payload' export const Nav: GlobalConfig = { slug: 'nav', fields: [ { name: 'items', type: 'array', required: true, maxRows: 8, fields: [ { name: 'page', type: 'relationship', relationTo: 'pages', // "pages" is the slug of an existing collection required: true, }, ], }, ], } ``` > **Reminder:** For more complex examples, see the > [Templates](https://github.com/payloadcms/payload/tree/main/templates) and > [Examples](https://github.com/payloadcms/payload/tree/main/examples) > directories in the Payload repository. The following options are available: | Option | Description | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `access` | Provide Access Control functions to define exactly who should be able to do what with this Global. [More details](/docs/v4/access-control/globals.md). | | `admin` | The configuration options for the Admin Panel. [More details](#admin-options). | | `authorship` | Automatically track the user that created and last updated this Global via polymorphic `createdBy` / `updatedBy` fields. Enabled by default. Set to `false` to disable, or to `{ createdBy: false }` / `{ updatedBy: false }` to toggle each field. [More details](/docs/v4/configuration/collections.md#authorship). | | `custom` | Extension point for adding custom data (e.g. for plugins) | | `dbName` | Custom table or collection name for this Global depending on the Database Adapter. Auto-generated from slug if not defined. | | `description` | Text or React component to display below the Global header to give editors more information. | | `endpoints` | Add custom routes to the REST API. [More details](/docs/v4/rest-api/overview.md#custom-endpoints). | | `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Global. [More details](/docs/v4/fields/overview.md). | | `graphQL` | Manage GraphQL-related properties related to this global. [More details](#graphql) | | `hooks` | Entry point for Hooks. [More details](/docs/v4/hooks/globals.md). | | `label` | Text for the name in the Admin Panel or an object with keys for each language. Auto-generated from slug if not defined. | | `lockDocuments` | Enables or disables document locking. By default, document locking is enabled. Set to an object to configure, or set to `false` to disable locking. [More details](/docs/v4/admin/locked-documents.md). | | `slug` \* | Unique, URL-friendly string that will act as an identifier for this Global. | | `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. | | `versions` | Set to true to enable default options, or configure with object properties. [More details](/docs/v4/versions/overview.md#global-config). | | `select` | Function that receives the current `operation`, `req`, and caller's `select`, and returns the final `select` to apply. Useful for forcing fields to be populated for hooks / access control. [More details](/docs/v4/queries/select.md#entity-level-select-config). | _\* An asterisk denotes that a property is required._ ### Fields Fields define the schema of the Global. To learn more, go to the [Fields](/docs/v4/fields/overview.md) documentation. ### Access Control [Global Access Control](/docs/v4/access-control/globals.md) determines what a user can and cannot do with any given Global Document. To learn more, go to the [Access Control](/docs/v4/access-control/overview.md) documentation. ### Hooks [Global Hooks](/docs/v4/hooks/globals.md) allow you to tie into the lifecycle of your Documents so you can execute your own logic during specific events. To learn more, go to the [Hooks](/docs/v4/hooks/overview.md) documentation. ## Admin Options The behavior of Globals within the [Admin Panel](/docs/v4/admin/overview.md) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](/docs/v4/custom-components/overview.md), setting page metadata, and more. To configure Admin Options for Globals, use the `admin` property in your Global Config: ```ts import type { GlobalConfig } from 'payload' export const MyGlobal: GlobalConfig = { // ... admin: { // highlight-line // ... }, } ``` The following options are available: | Option | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `group` | Text or localization object used to group Collection and Global links in the admin navigation. Set to `false` to hide the Global from the sidebar, command palette, and Collection Cards while keeping its routes accessible. | | `hidden` | Set to true or a function, called with the current user, returning true to exclude this Global from navigation and admin routing. | | `components` | Swap in your own React components to be used within this Global. [More details](#custom-components). | | `preview` | Function to generate a preview URL within the Admin Panel for this Global that can point to your app. [More details](/docs/v4/admin/preview.md). | | `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](/docs/v4/live-preview/overview.md). | | `meta` | Page metadata overrides to apply to this Global within the Admin Panel. [More details](/docs/v4/admin/metadata.md). | ### Custom Components Globals can set their own [Custom Components](/docs/v4/custom-components/overview.md) which only apply to Global-specific UI within the [Admin Panel](/docs/v4/admin/overview.md). This includes elements such as the Save Button, or entire layouts such as the Edit View. To override Global Components, use the `admin.components` property in your Global Config: ```ts import type { SanitizedGlobalConfig } from 'payload' export const MyGlobal: SanitizedGlobalConfig = { // ... admin: { components: { // highlight-line // ... }, }, } ``` The following options are available: #### General | Option | Description | | ------------- | -------------------------------------------------------------------------------------------------------------------------- | | `Description` | A component to render below the Global label in the Edit View. [More details](/docs/v4/custom-components/edit-view.md#description). | | `edit` | Override or create new components within the Edit View. [More details](#edit-view-options). | | `views` | Override or create new views within the Admin Panel. [More details](/docs/v4/custom-components/custom-views.md). | #### Edit View Options Custom components within the Edit View are configured under `admin.components.edit`: ```ts import type { GlobalConfig } from 'payload' export const MyGlobal: GlobalConfig = { // ... admin: { components: { edit: { // highlight-start Status: '/path/to/CustomStatus', SaveButton: '/path/to/CustomSaveButton', PublishButton: '/path/to/CustomPublishButton', // highlight-end }, }, }, } ``` The following components can be customized within `admin.components.edit`: | Option | Description | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `beforeDocumentControls` | Inject custom components before the Save button. [More details](/docs/v4/custom-components/edit-view.md#beforedocumentcontrols). | | `editMenuItems` | Inject custom components within the 3-dot menu dropdown. [More details](/docs/v4/custom-components/edit-view.md#editmenuitems). | | `SaveButton` | Replace the default Save Button with a Custom Component. [Drafts](/docs/v4/versions/drafts.md) must be disabled. [More details](/docs/v4/custom-components/edit-view.md#savebutton). | | `SaveDraftButton` | Replace the default Save Draft Button with a Custom Component. [Drafts](/docs/v4/versions/drafts.md) must be enabled and autosave must be disabled. [More details](/docs/v4/custom-components/edit-view.md#savedraftbutton). | | `PublishButton` | Replace the default Publish Button with a Custom Component. [Drafts](/docs/v4/versions/drafts.md) must be enabled. [More details](/docs/v4/custom-components/edit-view.md#publishbutton). | | `UnpublishButton` | Replace the default Unpublish Button with a Custom Component. [Drafts](/docs/v4/versions/drafts.md) must be enabled. [More details](/docs/v4/custom-components/edit-view.md#unpublishbutton). | | `PreviewButton` | Replace the default Preview Button with a Custom Component. [Preview](/docs/v4/admin/preview.md) must be enabled. [More details](/docs/v4/custom-components/edit-view.md#previewbutton). | | `Status` | Replace the default Status component with a Custom Component. [Drafts](/docs/v4/versions/drafts.md) must be enabled. [More details](/docs/v4/custom-components/edit-view.md#status). | > **Note:** For details on how to build Custom Components, see [Building Custom > Components](/docs/v4/custom-components/overview.md#building-custom-components). ## GraphQL You can completely disable GraphQL for this global by passing `graphQL: false` to your global config. This will completely disable all queries, mutations, and types from appearing in your GraphQL schema. You can also pass an object to the global's `graphQL` property, which allows you to define the following properties: | Option | Description | | ------------------ | ------------------------------------------------------------------------------- | | `name` | Override the name that will be used in GraphQL schema generation. | | `disableQueries` | Disable all GraphQL queries that correspond to this global by passing `true`. | | `disableMutations` | Disable all GraphQL mutations that correspond to this global by passing `true`. | ## TypeScript You can import types from Payload to help make writing your Global configs easier and type-safe. There are two main types that represent the Global Config, `GlobalConfig` and `SanitizedGlobalConfig`. The `GlobalConfig` type represents a raw Global Config in its full form, where only the bare minimum properties are marked as required. The `SanitizedGlobalConfig` type represents a Global Config after it has been fully sanitized. Generally, this is only used internally by Payload. ```ts import type { GlobalConfig, SanitizedGlobalConfig } from 'payload' ``` # I18n Source: https://payloadcms.com/docs/beta/configuration/i18n The [Admin Panel](/docs/v4/admin/overview.md) is translated in over [30 languages and counting](https://github.com/payloadcms/payload/tree/main/packages/translations). With I18n, editors can navigate the interface and read API error messages in their preferred language. This is similar to [Localization](/docs/v4/configuration/localization.md), but instead of managing translations for the data itself, you are managing translations for your application's interface. By default, Payload comes preinstalled with English, but you can easily load other languages into your own application. Languages are automatically detected based on the request. If no language is detected, or if the user's language is not yet supported by your application, English will be chosen. To add I18n to your project, you first need to install the `@payloadcms/translations` package: ```bash pnpm install @payloadcms/translations ``` Once installed, it can be configured using the `i18n` key in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... i18n: { // highlight-line // ... }, }) ``` > **Note:** If there is a language that Payload does not yet support, we accept > [code > contributions](https://github.com/payloadcms/payload/blob/main/CONTRIBUTING.md). ## Config Options You can easily customize and override any of the i18n settings that Payload provides by default. Payload will use your custom options and merge them in with its own. ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... // highlight-start i18n: { fallbackLanguage: 'en', // default }, // highlight-end }) ``` The following options are available: | Option | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------ | | `fallbackLanguage` | The language to fall back to if the user's preferred language is not supported. Default is `'en'`. | | `translations` | An object containing the translations. The keys are the language codes and the values are the translations. | | `supportedLanguages` | An object containing the supported languages. The keys are the language codes and the values are the translations. | ## Adding Languages You can easily add new languages to your Payload app by providing the translations for the new language. Payload maintains a number of built-in translations that can be imported from `@payloadcms/translations`, but you can also provide your own [Custom Translations](#custom-translations) to support any language. To add a new language, use the `i18n.supportedLanguages` key in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' import { en } from '@payloadcms/translations/languages/en' import { de } from '@payloadcms/translations/languages/de' export default buildConfig({ // ... // highlight-start i18n: { supportedLanguages: { en, de }, }, // highlight-end }) ``` > **Tip:** It's best to only support the languages that you need so that the > bundled JavaScript is kept to a minimum for your project. ### Text direction Payload sets the Admin Panel's text direction for you based on the user's selected language: - **Right-to-left (RTL):** Arabic (`ar`), Persian (`fa`), and Hebrew (`he`). - **Left-to-right (LTR):** All other supported languages. To enable an RTL language, add it to `i18n.supportedLanguages`. There's no extra configuration needed. Text direction follows the language used for the admin interface, independently of the locale used to edit your content. ### Custom Translations You can customize Payload's built-in translations either by extending existing languages or by adding new languages entirely. This can be done by injecting new translation strings into existing languages, or by providing an entirely new language keys altogether. To add Custom Translations, use the `i18n.translations` key in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' export default buildConfig({ //... i18n: { // highlight-start translations: { en: { custom: { // namespace can be anything you want key1: 'Translation with {{variable}}', // translation }, // override existing translation keys general: { dashboard: 'Home', }, }, }, // highlight-end }, //... }) ``` ### Project Translations While Payload's built-in features come fully translated, you may also want to translate parts of your own project. This is possible in places like [Collections](/docs/v4/configuration/collections.md) and [Globals](/docs/v4/configuration/globals.md), such as on their labels and groups, field labels, descriptions or input placeholder text. To do this, provide the translations wherever applicable, keyed to the language code: ```ts import type { CollectionConfig } from 'payload' export const Articles: CollectionConfig = { slug: 'articles', labels: { singular: { // highlight-start en: 'Article', es: 'Artículo', // highlight-end }, plural: { // highlight-start en: 'Articles', es: 'Artículos', // highlight-end }, }, admin: { group: { // highlight-start en: 'Content', es: 'Contenido', // highlight-end }, }, fields: [ { name: 'title', type: 'text', label: { // highlight-start en: 'Title', es: 'Título', // highlight-end }, admin: { placeholder: { // highlight-start en: 'Enter title', es: 'Introduce el título', // highlight-end }, }, }, ], } ``` ## Changing Languages Users can change their preferred language in their account settings or by otherwise manipulating their [User Preferences](/docs/v4/admin/preferences.md). ## Node.js#node Payload's backend sets the language on incoming requests before they are handled. This allows backend validation to return error messages in the user's own language or system generated emails to be sent using the correct translation. You can make HTTP requests with the `accept-language` header and Payload will use that language. Anywhere in your Payload app that you have access to the `req` object, you can access Payload's extensive internationalization features assigned to `req.i18n`. To access text translations you can use `req.t('namespace:key')`. ## TypeScript In order to use [Custom Translations](#custom-translations) in your project, you need to provide the types for the translations. Here we create a shareable translations object. We will import this in both our custom components and in our Payload config. In this example we show how to extend English, but you can do the same for any language you want. ```ts // /custom-translations.ts import { enTranslations } from '@payloadcms/translations/languages/en' import type { NestedKeysStripped } from '@payloadcms/translations' export const customTranslations = { en: { general: { myCustomKey: 'My custom english translation', }, fields: { addLabel: 'Add!', }, }, } export type CustomTranslationsObject = typeof customTranslations.en & typeof enTranslations export type CustomTranslationsKeys = NestedKeysStripped ``` Import the shared translations object into our Payload config so they are available for use: ```ts // /payload.config.ts import { buildConfig } from 'payload' import { customTranslations } from './custom-translations' export default buildConfig({ //... i18n: { translations: customTranslations, }, //... }) ``` Import the shared translation types to use in your [Custom Component](/docs/v4/custom-components/overview.md): ```ts // /components/MyComponent.tsx 'use client' import type React from 'react' import { useTranslation } from '@payloadcms/ui' import type { CustomTranslationsObject, CustomTranslationsKeys, } from '../custom-translations' export const MyComponent: React.FC = () => { const { i18n, t } = useTranslation< CustomTranslationsObject, CustomTranslationsKeys >() // These generics merge your custom translations with the default client translations return t('general:myCustomKey') } ``` Additionally, Payload exposes the `t` function in various places, for example in labels. Here is how you would type those: ```ts // /fields/myField.ts import type { TFunction } from '@payloadcms/translations' import type { Field } from 'payload' import { CustomTranslationsKeys } from '../custom-translations' const field: Field = { name: 'myField', type: 'text', label: ({ t: defaultT }) => { const t = defaultT as TFunction return t('fields:addLabel') }, } ``` # Localization Source: https://payloadcms.com/docs/beta/configuration/localization Localization is one of the most important features of a modern CMS. It allows you to manage content in multiple languages, then serve it to your users based on their requested language. This is similar to [I18n](/docs/v4/configuration/i18n.md), but instead of managing translations for your application's interface, you are managing translations for the data itself. With Localization, you can begin to serve personalized content to your users based on their specific language preferences, such as a multilingual website or multi-site application. There are no limits to the number of locales you can add to your Payload project. You can also [validate one or more locales without saving](/docs/v4/validation/overview.md). To configure Localization, use the `localization` key in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... localization: { // highlight-line // ... }, }) ``` ## Config Options Add the `localization` property to your Payload Config to enable Localization project-wide. You'll need to provide a list of all locales that you'd like to support as well as set a few other options. To configure locales, use the `localization.locales` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... localization: { locales: ['en', 'es', 'de'], // required defaultLocale: 'en', // required }, }) ``` You can also define locales using [full configuration objects](#locale-object): ```ts import { buildConfig } from 'payload' export default buildConfig({ collections: [ // collections go here ], localization: { locales: [ { label: 'English', code: 'en', }, { label: 'Arabic', code: 'ar', // opt-in to setting default text-alignment on Input fields to rtl (right-to-left) // when current locale is rtl rtl: true, }, ], defaultLocale: 'en', // required fallback: true, // defaults to true }, }) ``` > **Tip:** Localization works very well alongside > [I18n](/docs/v4/configuration/i18n.md). The following options are available: | Option | Description | | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`locales`** | Array of all the languages that you would like to support. [More details](#locales) | | **`defaultLocale`** | Required string that matches one of the locale codes from the array provided. By default, if no locale is specified, documents will be returned in this locale. | | **`fallback`** | Boolean enabling "fallback" locale functionality. If a document is requested in a locale, but a field does not have a localized value corresponding to the requested locale, then if this property is enabled, the document will automatically fall back to the fallback locale value. If this property is not enabled, the value will not be populated unless a fallback is explicitly provided in the request. True by default. | | **`filterAvailableLocales`** | A function that is called with the array of `locales` and the `req`, it should return locales to show in admin UI selector. [See more](#filter-available-options). | ### Locales The locales array is a list of all the languages that you would like to support. This can be strings for each language code, or [full configuration objects](#locale-object) for more advanced options. The locale codes do not need to be in any specific format. It's up to you to define how to represent your locales. Common patterns are to use two-letter ISO 639 language codes or four-letter language and country codes (ISO 3166‑1) such as `en-US`, `en-UK`, `es-MX`, etc. #### Locale Object | Option | Description | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | **`code`** \* | Unique code to identify the language throughout the APIs for `locale` and `fallbackLocale`. | | **`label`** | A string to use for the selector when choosing a language, or an object keyed on the i18n keys for different languages in use. | | **`rtl`** | A boolean that when true will make the admin UI display in Right-To-Left. | | **`fallbackLocale`** | The code for this language to fallback to when properties of a document are not present. This can be a single locale or array of locales. | _\* An asterisk denotes that a property is required._ #### Filter Available Options In some projects you may want to filter the available locales shown in the admin UI selector. You can do this by providing a `filterAvailableLocales` function in your Payload Config. This is called on the server side and is passed the array of locales. This means that you can determine what locales are visible in the localizer selection menu at the top of the admin panel. You could do this per user, or implement a function that scopes these to tenants and more. Here is an example using request headers in a multi-tenant application: ```ts // ... rest of Payload config localization: { defaultLocale: 'en', locales: ['en', 'es'], filterAvailableLocales: async ({ req, locales }) => { if (getTenantFromCookie(req.headers, 'text')) { const fullTenant = await req.payload.findByID({ id: getTenantFromCookie(req.headers, 'text') as string, collection: 'tenants', overrideAccess: true, req, }) if (fullTenant && fullTenant.supportedLocales?.length) { return locales.filter((locale) => { return fullTenant.supportedLocales?.includes(locale.code as 'en' | 'es') }) } } return locales }, } ``` Since the filtering happens at the root level of the application and its result is not calculated every time you navigate to a new page, you may want to call `router.refresh` in a custom component that watches when values that affect the result change. In the example above, you would want to do this when `supportedLocales` changes on the tenant document. ## Field Localization Payload Localization works on a **field** level—not a document level. In addition to configuring the base Payload Config to support Localization, you need to specify each field that you would like to localize. **Here is an example of how to enable Localization for a field:** ```js { name: 'title', type: 'text', // highlight-start localized: true, // highlight-end } ``` With the above configuration, the `title` field will now be saved in the database as an object of all locales instead of a single string. All field types with a `name` property support the `localized` property—even the more complex field types like `array`s and `block`s. > **Note:** Enabling Localization for field types that support nested fields > will automatically create localized "sets" of all fields contained within the > field. For example, if you have a page layout using a blocks field type, you > have the choice of either localizing the full layout, by enabling Localization > on the top-level blocks field, or only certain fields within the layout. > **Important:** When converting an existing field to or from `localized: true` > the data structure in the document will change for this field and so existing > data for this field will be lost. Before changing the Localization setting on > fields with existing data, you may need to consider a field migration > strategy. ## Status Localization When [Localization](/docs/v4/configuration/localization.md) is enabled and a collection or global has **drafts enabled** and contains at least one localized field, Payload automatically localizes the `_status` field. This means publication status is tracked independently per locale, so you can publish in English while keeping Spanish as a draft. When the status field is localized, it is stored as an object keyed by locale: ```ts status: { en: 'published', es: 'draft', de: 'published', } ``` If localization is not configured, or the collection/global has no localized fields, the `status` field remains a single string (`'draft'` or `'published'`) representing document status across all locales. ### Publishing and unpublishing per locale When the `_status` field is localized, publishing and unpublishing are scoped to a single locale by default. The locale being acted on is the one provided to the operation (via the `locale` parameter, or the request locale). This means publishing in one locale leaves the other locales' statuses untouched. ```ts // Publishes ONLY the Spanish status; English, German, etc. are unchanged await payload.update({ collection: 'posts', overrideAccess: true, id, locale: 'es', data: { _status: 'published' }, }) ``` To publish or unpublish every locale at once, use `publishAllLocales` or `unpublishAllLocales`. These set the status for all locales the user has access to, regardless of the active locale: ```ts // Publishes every locale await payload.update({ collection: 'posts', overrideAccess: true, id, publishAllLocales: true, data: { _status: 'published' }, }) // Unpublishes every locale await payload.update({ collection: 'posts', overrideAccess: true, id, unpublishAllLocales: true, data: { _status: 'draft' }, }) ``` Passing `locale: 'all'` together with `_status: 'published'` also publishes every locale, and is equivalent to `publishAllLocales: true`. > **Note** > > When the `_status` field is **not** localized (localization disabled, or no > localized fields), publishing and unpublishing always apply to the whole > document, and `publishAllLocales` / `unpublishAllLocales` have no effect. The same parameters are available on `payload.updateGlobal()` for globals, as well as on the REST and Local APIs. ## Retrieving Localized Docs When retrieving documents, you can specify which locale you'd like to receive as well as which fallback locale should be used. #### REST API REST API locale functionality relies on URL query parameters. **`?locale=`** Specify your desired locale by providing the `locale` query parameter directly in the endpoint URL. **`?fallback-locale=`** Specify fallback locale to be used by providing the `fallback-locale` query parameter. This can be provided as either a valid locale as provided to your base Payload Config, or `'null'`, `'false'`, or `'none'` to disable falling back. **Example:** ``` fetch('https://localhost:3000/api/pages?locale=es&fallback-locale=none'); ``` #### GraphQL API In the GraphQL API, you can specify `locale` and `fallbackLocale` args to all relevant queries and mutations. The `locale` arg will only accept valid locales, but locales will be formatted automatically as valid GraphQL enum values (dashes or special characters will be converted to underscores, spaces will be removed, etc.). If you are curious to see how locales are auto-formatted, you can use the [GraphQL playground](/docs/v4/graphql/overview.md#graphql-playground). The `fallbackLocale` arg will accept valid locales, an array of locales, as well as `none` to disable falling back. **Example:** ```graphql query { Posts(locale: de, fallbackLocale: none) { docs { title } } } ``` > **Note** > > In GraphQL, specifying the locale at the top level of a query will > automatically apply it throughout all nested relationship fields. You can > override this behavior by re-specifying locale arguments in nested related > document queries. #### Local API You can specify `locale` as well as `fallbackLocale` within the Local API as well as properties on the `options` argument. The `locale` property will accept any valid locale, and the `fallbackLocale` property will accept any valid locale, array of locales, as well as `'null'`, `'false'`, `false`, and `'none'`. **Example:** ```js const posts = await payload.find({ collection: 'posts', overrideAccess: true, locale: 'es', fallbackLocale: false, }) ``` > **Tip:** The REST and Local APIs can return all Localization data in one > request by passing 'all' or '*' as the **locale** parameter. The response will > be structured so that field values come back as the full objects keyed for > each locale instead of the single, translated value. # Environment Variables Source: https://payloadcms.com/docs/beta/configuration/environment-vars Environment Variables are a way to store sensitive information that your application needs to function. This could be anything from API keys to [Database](/docs/v4/database/overview.md) credentials. Payload allows you to easily use Environment Variables within your config and throughout your application. ## Next.js Applications If you are using Next.js, no additional setup is required other than creating your `.env` file. To use Environment Variables, add a `.env` file to the root of your project: ```plaintext project-name/ ├─ .env ├─ package.json ├─ payload.config.ts ``` Here is an example of what an `.env` file might look like: ```plaintext SERVER_URL=localhost:3000 DATABASE_URL=mongodb://localhost:27017/my-database ``` To use Environment Variables in your Payload Config, you can access them directly from `process.env`: ```ts import { buildConfig } from 'payload' export default buildConfig({ serverURL: process.env.SERVER_URL, // highlight-line // ... }) ``` ## Client-side Environments For security and safety reasons, the [Admin Panel](/docs/v4/admin/overview.md) does **not** include Environment Variables in its _client-side_ bundle by default. But, both Next.js and TanStack Start provide a mechanism to expose Environment Variables to the client-side bundle when needed. If you are building a [Custom Component](/docs/v4/custom-components/overview.md) and need to access Environment Variables from the client-side, you can do so by prefixing them with `NEXT_PUBLIC_` on Next.js, or with `VITE_` on TanStack Start. > **Important:** Be careful about what variables you provide to your client-side > code. Analyze every single one to make sure that you're not accidentally > leaking sensitive information. Only ever include keys that are safe for the > public to read in plain text. For example, if you've got the following Environment Variable: ```bash NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_XXXXXXXXXXXXXXXXXX ``` This key will automatically be made available to the client-side Payload bundle and can be referenced in your Custom Component as follows: ```tsx 'use client' import React from 'react' const stripeKey = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY // highlight-line const MyClientComponent = () => { // do something with the key return
My Client Component
} ``` For more information, check out the [Next.js documentation](https://nextjs.org/docs/app/building-your-application/configuring/environment-variables). On TanStack Start, the same rule applies but the mechanism is Vite's. Prefix any variable you want exposed to the client-side bundle with `VITE_`, then read it from `import.meta.env`: ```bash VITE_STRIPE_PUBLISHABLE_KEY=pk_test_XXXXXXXXXXXXXXXXXX ``` ```tsx 'use client' import React from 'react' const stripeKey = import.meta.env.VITE_STRIPE_PUBLISHABLE_KEY // highlight-line const MyClientComponent = () => { // do something with the key return
My Client Component
} ``` For more information, check out the [Vite documentation](https://vite.dev/guide/env-and-mode). ## Outside of Next.js If you are using Payload outside of Next.js, we suggest using the [`dotenv`](https://www.npmjs.com/package/dotenv) package to handle Environment Variables from `.env` files. This will automatically load your Environment Variables into `process.env`. To do this, import the package as high up in your application as possible: ```ts import dotenv from 'dotenv' dotenv.config() // highlight-line import { buildConfig } from 'payload' export default buildConfig({ serverURL: process.env.SERVER_URL, // ... }) ``` > **Tip:** Be sure that `dotenv` can find your `.env` file. By default, it will > look for a file named `.env` in the root of your project. If you need to > specify a different file, pass the path into the config options. # Payload CLI Source: https://payloadcms.com/docs/beta/configuration/cli The Payload CLI lets you work with your project from a terminal. You can use it to generate types, run migrations, inspect your setup, run jobs, and more. The CLI loads the Payload Config from your project. Run commands from your project directory, or use [`PAYLOAD_CONFIG_PATH`](/docs/v4/configuration/overview.md#customizing-the-config-location) if your config is somewhere else. The examples on this page use pnpm, but you can also use `npx payload` or the equivalent command for your package manager. ```sh pnpm payload --help ``` ## Built-in commands Payload includes the following commands by default. ### Core commands | Command | What it does | | ----------------------------- | ------------------------------------------------------------------------------------- | | `build` | Generate the Admin Panel import map and TypeScript types, then run the project build. | | `generate:db-schema` | Generate a database schema. Not every database adapter supports this command. | | `generate:importmap` | Generate the import map used by the Admin Panel. | | `generate:types` | Generate TypeScript types from your Payload Config. | | `help [command]` | Show help for all commands or for one command. | | `info` | Print Node.js, package, operating system, memory, and CPU information. | | `jobs:handle-schedules` | Put jobs whose schedule is due into the queue. | | `jobs:run` | Run jobs from the queue. | | `run [args...]` | Run a local script in the Payload environment. | ### Migration commands | Command | What it does | | ----------------------- | -------------------------------------------------- | | `migrate` | Run all pending migrations. | | `migrate:create [name]` | Create a migration. | | `migrate:down` | Roll back the latest migration batch. | | `migrate:fresh` | Clear the database and run all migrations again. | | `migrate:refresh` | Roll back completed migrations and run them again. | | `migrate:reset` | Roll back all migrations. | | `migrate:status` | Show which migrations have and have not run. | Use `--help` to see the arguments and options for a command: ```sh pnpm payload migrate:create --help pnpm payload jobs:run --help ``` ## JSON output, help, and input Every command accepts the global `--json` option. It returns a predictable response that scripts and coding agents can read: ```sh pnpm payload generate:types --json ``` ```json { "command": "generate:types", "result": { "outputFile": "/project/src/payload-types.ts" }, "success": true } ``` You can place `--json` before or after the command. Commands that do not return data omit `result`. A failed command returns `success: false`, an error code, and an error message. Input validation errors also include the issues and the command's input schema. In JSON mode, stdout contains exactly one JSON response while logs and other diagnostics stream to stderr. Parse stdout and display or store stderr separately. Do not use `2>&1`: merging the streams makes stdout invalid JSON. Stderr intentionally remains a streaming text channel rather than a second buffered JSON value. Set `PAYLOAD_CLI_JSON` to use JSON output for every command without repeatedly passing `--json`. This is useful when a script or coding agent runs several commands: ```sh export PAYLOAD_CLI_JSON=1 pnpm payload generate:types pnpm payload migrate:status ``` Unset the variable to return to regular terminal output, or use `--no-json` to override it for one command: ```sh PAYLOAD_CLI_JSON=1 pnpm payload info --no-json ``` Payload can return help as JSON. This is useful for scripts and coding agents that need to know which commands exist and what input they accept. ```sh # Describe every command pnpm payload help --json # Describe one command pnpm payload help migrate:create --json ``` For example, you would normally pass the migration name as an argument: ```sh pnpm payload migrate:create add-authors ``` You can pass the same input as JSON with `--input`: ```sh pnpm payload migrate:create --input '{"migrationName":"add-authors"}' ``` To read the input from a file, create `migration-input.json`: ```json { "migrationName": "add-authors" } ``` Then add `@` before the file path: ```sh pnpm payload migrate:create --input @migration-input.json ``` To read the same JSON from stdin, pipe it into the command and use `-`: ```sh echo '{"migrationName":"add-authors"}' | pnpm payload migrate:create --input - ``` The JSON is validated in the same way as normal command arguments and options. Do not combine `--input` with other command input. ## Custom commands Use `defineCLICommand` to add your own command. Built-in and custom commands use the same API, help system, and input validation. ### Define a command The following example adds a `seed` command. Its code lives in `seed.ts` next to `payload.config.ts`: ```ts import { strictObject, z } from 'payload' import { defineCLICommand } from 'payload/cli' export const seedCommand = defineCLICommand({ description: 'Seed the database.', examples: ['payload seed', 'payload seed --clear'], input: strictObject({ clear: z ._default(z.boolean(), false) .check(z.describe('Delete existing documents first.')), }), handler: async ({ args, getPayload }) => { const payload = await getPayload() if (args.clear) { // Delete existing documents... } await payload.create({ collection: 'pages', overrideAccess: true, data: { title: 'My page' }, }) payload.logger.info('Successfully seeded!') return { result: { seeded: true } } }, }) ``` The `input` schema defines the values the command accepts. Payload uses it to type `args`, validate input, and generate help. Add `examples` when a complete command makes the arguments easier to understand. Payload includes them in both terminal and JSON help. The `z` export from `payload` is Zod Mini. Payload already uses Zod Mini, so this export adds no extra schema code to the bundle. Use `strictObject` for the root input schema so Payload can also generate JSON help. Payload does not export regular Zod because its larger API would increase the bundle size. If you prefer regular Zod's chaining API, add `zod` to your own project and import it directly: ```sh pnpm add zod ``` ```ts import { z } from 'zod' ``` You can also use another schema library. Its schema must support both [Standard Schema](https://standardschema.dev/) validation and Standard JSON Schema output. Call `getPayload()` only when the command needs the Local API. Payload closes the instance when the command finishes. Returning `{ result }` makes that value available in JSON mode through `--json` or `PAYLOAD_CLI_JSON`. Return a number to set only the process exit code, or return `{ exitCode, result }` when you need both. Payload sends `payload.logger.*` and `console.*` output to stderr in JSON mode. Use `isJSON` only when the command should behave differently in JSON mode. ### Register the command Add the command to `cli.commands` in your Payload Config. The map key becomes the command name: ```ts export default buildConfig({ cli: { commands: { seed: './seed.js#seedCommand', }, }, }) ``` Command paths use the same format as other [`PayloadComponent`](/docs/v4/custom-components/overview.md#component-paths) paths. A path without `#` uses the default export. Add `#exportName` to use a named export, or use the object form: ```ts cli: { commands: { seed: './seed.js', cleanup: './cleanup.js#cleanupCommand', importData: { path: './data.js', exportName: 'importDataCommand', }, }, } ``` You can also pass a command directly. This example defines a small command inside the Payload Config: ```ts import { buildConfig } from 'payload' import { strictObject } from 'payload' import { defineCLICommand } from 'payload/cli' export default buildConfig({ cli: { commands: { hello: defineCLICommand({ description: 'Print a greeting.', input: strictObject({}), handler: ({ isJSON }) => { if (!isJSON) { console.log('Hello!') } return { result: { message: 'Hello!' } } }, }), }, }, }) ``` Both forms work, but import paths are recommended. A direct command and all of its imports are loaded with the Payload Config, even when you are not using the CLI. An import path is only loaded when the CLI starts, so command-only dependencies stay out of the Payload Config bundle. Relative paths start from the folder containing `payload.config.ts`. You can now run the `seed` command like any built-in command: ```sh pnpm payload seed pnpm payload seed --clear pnpm payload seed --help ``` ### Arguments and options By default, each field in `input` becomes a command option. Payload changes camelCase field names to kebab-case flags. For example, `migrationName` becomes `--migration-name`. Use `cli` when a field needs different command-line behavior. For example, this makes `filePath` a positional argument: ```ts defineCLICommand({ input: strictObject({ filePath: z.string().check(z.describe('File to import.')), }), cli: { filePath: 'argument', }, // ... }) ``` Set a field to `false` to accept it only through `--input`. For less common cases, you can use an object to change the flag, argument position, or parser. ### Add, replace, or disable commands A new key in `cli.commands` adds a command. Using the name of a built-in command replaces it. Setting a command to `false` disables it: ```ts export default buildConfig({ cli: { commands: { seed: './seed.js#seedCommand', 'generate:types': './generateTypes.js#generateTypesCommand', info: false, }, }, }) ``` After Payload loads the config, `config.cli.commands` shows the final list of built-in and custom commands. Set `cli` itself to `false` to disable the entire Payload CLI: ```ts export default buildConfig({ cli: false, }) ``` ## Working with data from the CLI Payload includes local commands for inspecting the config and working with collections and globals. They use the Local API and load the current Payload config on every invocation. > **Warning** > > Data commands bypass access control by default. Run them only from a trusted > environment with access to the project. Pass `--override-access false` to > enforce access control as an anonymous request because the CLI does not have a > logged-in user. `getCollectionSchema` and `getGlobalSchema` also return their [LLM instructions](/docs/v4/configuration/llm-instructions.md) alongside `schema`. With `--json`, these are available as `result.instructions`. Read them before creating or updating content. Start by discovering the available slugs and inspecting the input schema for a collection: ```sh pnpm payload getConfigInfo pnpm payload getCollectionSchema --slug posts ``` You can then query and update data without starting the development server: ```sh pnpm payload findDocuments \ --slug posts \ --where '{"status":{"equals":"published"}}' pnpm payload updateDocument \ --slug posts \ --id 1 \ --data '{"title":"Updated title"}' ``` To explicitly enforce access control: ```sh pnpm payload findDocuments --slug posts --override-access false ``` The same option is named `overrideAccess` in JSON input: ```sh pnpm payload findDocuments --input '{"slug":"posts","overrideAccess":false}' ``` `createDocuments` accepts a JSON array, keeping each document's data and optional upload file together: ```sh pnpm payload createDocuments \ --slug media \ --documents '[{"data":{"alt":"Company logo"},"file":"./images/logo.png"}]' ``` Upload input can also use an external URL. Payload applies the upload collection's `pasteURL`, allow-list, safe-fetch, header, and file-size settings before creating or updating the document: The trusted local CLI does not require an explicit allow-list. If one is configured, both the initial URL and redirects must match it. `pasteURL: false` disables URL uploads. ```sh pnpm payload createDocuments \ --slug media \ --documents '[{"data":{"alt":"Company logo"},"file":{"source":"externalURL","url":"https://example.com/logo.png"}}]' ``` For `updateDocument`, pass the same object through `--file`: ```sh pnpm payload updateDocument \ --slug media \ --id 1 \ --data '{"alt":"Updated logo"}' \ --file '{"source":"externalURL","url":"https://example.com/logo.png"}' ``` For longer input, put the array in a file: ```json [{ "data": { "title": "First post" } }, { "data": { "title": "Second post" } }] ``` ```sh pnpm payload createDocuments --slug posts --documents @documents.json ``` You can instead pass the command input through `--input`. Explicit arguments and options override values from the JSON input: ```json { "slug": "posts", "documents": [ { "data": { "title": "First post" } }, { "data": { "title": "Second post" } } ] } ``` ```sh pnpm payload createDocuments --input @create-posts.json ``` `createDocuments` and `updateDocument` return affected document IDs by default. Pass `--returning` when you need the complete documents: ```sh pnpm payload updateDocument \ --slug posts \ --id 1 \ --data '{"title":"Updated title"}' \ --returning ``` Use `--select` with `--returning` to choose which fields come back: ```sh pnpm payload updateDocument \ --slug posts \ --id 1 \ --data '{"title":"Updated title"}' \ --returning \ --select '{"title":true}' ``` The built-in data commands cover collection queries, counts, distinct values, create, update, delete, duplicate, and version operations. Globals have matching schema, read, update, count-version, find-version, and restore-version commands. Use `payload help --json` to list them or `payload help --json` to inspect the exact input accepted by one command. ## Running commands on a schedule Use the global `--cron` option to run a command on a cron schedule: ```sh pnpm payload jobs:run --cron "*/5 * * * *" ``` This example runs queued jobs every five minutes. The CLI process stays open while the schedule is active. # LLM Instructions Source: https://payloadcms.com/docs/beta/configuration/llm-instructions LLM instructions guide AI agents working with your collections and globals. Payload core manages configured Markdown instructions and additional instructions saved through the Admin Panel. The feature works without the MCP plugin. Both the [CLI](/docs/v4/configuration/cli.md) and [MCP plugin](/docs/v4/plugins/mcp.md) return configured instructions combined with saved additional instructions as Markdown. Only `getCollectionSchema` and `getGlobalSchema` include them, so agents should read the schema before creating or updating content. Instructions are omitted when both sources are empty. ## Configuration Set `llmInstructions` on a collection or global to provide read-only Markdown instructions. Use the root `llmInstructions` option to control instruction management: ```ts import { buildConfig } from 'payload' export default buildConfig({ collections: [ { slug: 'pages', llmInstructions: 'Use the configured layout blocks and keep page titles concise.', fields: [/* ... */], }, ], globals: [ { slug: 'settings', llmInstructions: 'Preserve existing navigation links unless asked to change them.', fields: [/* ... */], }, ], llmInstructions: { access: ({ req }) => req.user?.roles?.includes('admin') ?? false, }, }) ``` ## Editing instructions Open **Edit LLM instructions** from a collection's list menu or a global's edit menu. The hidden **LLM Instructions** collection has one configuration-owned entry per visible collection or global. Its **Additional instructions** tab saves editable instructions; **System instructions (read-only)** displays the configured Markdown and cannot be edited through the Admin Panel or API. ## Editor Lexical uses a dedicated instructions editor with paragraphs, H2–H4 headings, bold, italic, strikethrough, ordered and unordered lists, and a fixed toolbar. Its feature set is independent of the root editor's features. Set `llmInstructions.editor` to a separate rich-text adapter to override it. ## Access control Authenticated users can read saved instructions for collections and globals they have read access to. Editing also requires update access to the target and permission through `llmInstructions.access`, which defaults to users of the admin auth collection. Target access is evaluated at the entity level, as in the Admin Panel. ## Disabling management Set the root `llmInstructions` option to `false` to disable the management collection and menu items. Configured collection and global instructions are still included in CLI and MCP schema responses. # Database Source: https://payloadcms.com/docs/beta/database/overview Payload is database agnostic, meaning you can use any type of database behind Payload's familiar APIs. Payload is designed to interact with your database through a Database Adapter, which is a thin layer that translates Payload's internal data structures into your database's native data structures. Currently, Payload officially supports the following Database Adapters: - [MongoDB](/docs/v4/database/mongodb.md) with [Mongoose](https://mongoosejs.com/) - [Postgres](/docs/v4/database/postgres.md) with [Drizzle](https://drizzle.team/) - [SQLite](/docs/v4/database/sqlite.md) with [Drizzle](https://drizzle.team/) To configure a Database Adapter, use the `db` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' import { mongooseAdapter } from '@payloadcms/db-mongodb' export default buildConfig({ // ... // highlight-start db: mongooseAdapter({ url: process.env.DATABASE_URL, }), // highlight-end }) ``` > **Reminder:** The Database Adapter is an external dependency and must be > installed in your project separately from Payload. You can find the > installation instructions for each Database Adapter in their respective > documentation. ## Selecting a Database There are several factors to consider when choosing which database technology and hosting option is right for your project and workload. Payload can theoretically support any database, but it's up to you to decide which database to use. There are two main categories of databases to choose from: - [Non-Relational Databases](#non-relational-databases) - [Relational Databases](#relational-databases) ### Non-Relational Databases If your project has a lot of dynamic fields, and you are comfortable with allowing Payload to enforce data integrity across your documents, MongoDB is a great choice. With it, your Payload documents are stored as _one_ document in your database—no matter if you have localization enabled, how many block or array fields you have, etc. This means that the shape of your data in your database will very closely reflect your field schema, and there is minimal complexity involved in storing or retrieving your data. You should prefer MongoDB if: - You prefer simplicity within your database - You don't want to deal with keeping production / staging databases in sync via [DDL changes](https://en.wikipedia.org/wiki/Data_definition_language) - Most (or everything) in your project is [Localized](/docs/v4/configuration/localization.md) - You leverage a lot of [Arrays](/docs/v4/fields/array.md), [Blocks](/docs/v4/fields/blocks.md), or `hasMany` [Select](/docs/v4/fields/select.md) fields ### Relational Databases Many projects might call for more rigid database architecture where the shape of your data is strongly enforced at the database level. For example, if you know the shape of your data and it's relatively "flat", and you don't anticipate it to change often, your workload might suit relational databases like Postgres very well. You should prefer a relational DB like Postgres or SQLite if: - You are comfortable with [Migrations](/docs/v4/database/migrations.md) - You require enforced data consistency at the database level - You have a lot of relationships between collections and require relationships to be enforced ## Processing database operations in batches Use `payload.db.batchProcessing` to run ordered `create`, `updateOne`, and `deleteOne` database operations in bounded batches. Each result includes the source operation index and a `succeeded`, `failed`, `noMatch`, or `unattempted` status. ```ts const results = await payload.db.batchProcessing({ batchSize: 100, operations: [ { args: { collection: 'posts', data: { title: 'Created in a batch' }, }, operation: 'create', }, ], req, }) ``` Processing stops after the first failed operation by default. Set `shouldContinueOnError: true` to attempt later operations. The method does not start, commit, or roll back a transaction. When you supply a request with a transaction, the caller remains responsible for that transaction. This is a low-level database method. Its operations do not run Local API access control, hooks, field validation, or upload handling. The default implementation uses existing adapter methods. Custom adapters created with `createDatabaseAdapter` receive this implementation automatically. ## Payload Differences It's important to note that nearly every Payload feature is available in all of our officially supported Database Adapters, including [Localization](/docs/v4/configuration/localization.md), [Arrays](/docs/v4/fields/array.md), [Blocks](/docs/v4/fields/blocks.md), etc. The only thing that is not supported in SQLite yet is the [Point Field](/docs/v4/fields/point.md), but that should be added soon. It's up to you to choose which database you would like to use based on the requirements of your project. Payload has no opinion on which database you should ultimately choose. ## Copying a stored document Use `payload.db.copy` to copy one stored collection document. The method selects the source with a `where` query and creates a new database record. Use `data` to replace top-level source values. Payload also assigns new IDs to copied array and block rows. ```ts const copiedPost = await payload.db.copy({ collection: 'posts', data: { title: 'Copied post', }, req, where: { id: { equals: sourcePostID, }, }, }) ``` ### Key differences - `payload.db.copy` makes a one-to-one storage copy: one current source document becomes one new database record. It does not copy version history. - It copies stored values as-is, except document, array, and block row IDs. Unique fields and custom database indexes can reject the new record. Use `data` overrides or handle these conflicts yourself. - Custom-ID collections require `data.id` with the configured ID type. ID hooks do not run, so the caller must supply a unique destination ID. - It does not run access control, hooks, field validation, or upload handling. Use [`payload.duplicate`](/docs/v4/local-api/overview.md#collection-create) for normal application duplication. The default implementation uses existing adapter methods. Custom adapters created with `createDatabaseAdapter` receive this implementation automatically. Direct implementations of `BaseDatabaseAdapter` must provide `copy` after this change. # Migrations Source: https://payloadcms.com/docs/beta/database/migrations Payload exposes a full suite of migration controls available for your use. Migration commands are accessible via the `npm run payload` command in your project directory. Ensure you have an npm script called "payload" in your `package.json` file. ```json { "scripts": { "payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload" } } ``` > **Note** > > Note that you need to run Payload migrations through the package manager that > you are using, because Payload should not be globally installed on your > system. ## Migration file contents Payload stores all created migrations in a folder that you can specify. By default, migrations are stored in `./src/migrations`. A migration file has two exports - an `up` function, which is called when a migration is executed, and a `down` function that will be called if for some reason the migration fails to complete successfully. The `up` function should contain all changes that you attempt to make within the migration, and the `down` should ideally revert any changes you make. Here is an example migration file: ```ts import { MigrateUpArgs, MigrateDownArgs } from '@payloadcms/your-db-adapter' export async function up({ payload, req }: MigrateUpArgs): Promise { // Perform changes to your database here. // You have access to `payload` as an argument, and // everything is done in TypeScript. } export async function down({ payload, req }: MigrateDownArgs): Promise { // Do whatever you need to revert changes if the `up` function fails } ``` ## Using Transactions When migrations are run, each migration is performed in a new [transaction](/docs/v4/database/transactions.md) for you. All you need to do is pass the `req` object to any [Local API](/docs/v4/local-api/overview.md) or direct database calls, such as `payload.db.updateMany()`, to make database changes inside the transaction. Assuming no errors were thrown, the transaction is committed after your `up` or `down` function runs. If the migration errors at any point or fails to commit, it is caught and the transaction gets aborted. This way no change is made to the database if the migration fails. ### Using database directly with the transaction Additionally, you can bypass Payload's layer entirely and perform operations directly on your underlying database within the active transaction: ### MongoDB: ```ts import { type MigrateUpArgs } from '@payloadcms/db-mongodb' export async function up({ session, payload, req, }: MigrateUpArgs): Promise { const posts = await payload.db.collections.posts.collection .find({ session }) .toArray() } ``` ### Postgres: ```ts import { type MigrateUpArgs, sql } from '@payloadcms/db-postgres' export async function up({ db, payload, req }: MigrateUpArgs): Promise { const { rows: posts } = await db.execute(sql`SELECT * from posts`) } ``` ### SQLite: In SQLite, transactions are disabled by default. [More](/docs/v4/database/transactions.md). ```ts import { type MigrateUpArgs, sql } from '@payloadcms/db-sqlite' export async function up({ db, payload, req }: MigrateUpArgs): Promise { const { rows: posts } = await db.run(sql`SELECT * from posts`) } ``` ## Migrations Directory Each DB adapter has an optional property `migrationDir` where you can override where you want your migrations to be stored/read. If this is not specified, Payload will check the default and possibly make a best effort to find your migrations directory by searching in common locations ie. `./src/migrations`, `./dist/migrations`, `./migrations`, etc. All database adapters should implement similar migration patterns, but there will be small differences based on the adapter and its specific needs. Below is a list of all migration commands that should be supported by your database adapter. ## Commands ### Migrate The `migrate` command will run any migrations that have not yet been run. ```text npm run payload migrate ``` ### Create Create a new migration file in the migrations directory. You can optionally name the migration that will be created. By default, migrations will be named using a timestamp. ```text npm run payload migrate:create optional-name-here ``` Flags: - `--skip-empty`: with Postgres, it skips the "no schema changes detected. Would you like to create a blank migration file?" prompt which can be useful for generating migration in CI. - `--force-accept-warning`: accepts any command prompts, creates a blank migration even if there weren't any changes to the schema. ### Status The `migrate:status` command will check the status of migrations and output a table of which migrations have been run, and which migrations have not yet run. `payload migrate:status` ```text npm run payload migrate:status ``` ### Down Roll back the last batch of migrations. ```text npm run payload migrate:down ``` ### Refresh Roll back all migrations that have been run, and run them again. ```text npm run payload migrate:refresh ``` ### Reset Roll back all migrations. ```text npm run payload migrate:reset ``` ### Fresh Drops all entities from the database and re-runs all migrations from scratch. ```text npm run payload migrate:fresh ``` ## When to run migrations Depending on which Database Adapter you use, your migration workflow might differ subtly. In relational databases, migrations will be **required** for non-development database environments. But with MongoDB, you might only need to run migrations once in a while (or never even need them). #### MongoDB#mongodb-migrations In MongoDB, you'll only ever really need to run migrations for times where you change your database shape, and you have lots of existing data that you'd like to transform from Shape A to Shape B. In this case, you can create a migration by running `pnpm payload migrate:create`, and then write the logic that you need to perform to migrate your documents to their new shape. You can then either run your migrations in CI before you build / deploy, or you can run them locally, against your production database, by using your production database connection string on your local computer and running the `pnpm payload migrate` command. #### Postgres#postgres-migrations In relational databases like Postgres, migrations are a bit more important, because each time you add a new field or a new collection, you'll need to update the shape of your database to match your Payload Config (otherwise you'll see errors upon trying to read / write your data). That means that Postgres users of Payload should become familiar with the entire migration workflow from top to bottom. Here is an overview of a common workflow for working locally against a development database, creating migrations, and then running migrations against your production database before deploying. **1 - work locally using push mode** Payload uses Drizzle ORM's powerful `push` mode to automatically sync data changes to your database for you while in development mode. By default, this is enabled and is the suggested workflow to using Postgres and Payload while doing local development. You can disable this setting and solely use migrations to manage your local development database (pass `push: false` to your Postgres adapter), but if you do disable it, you may see frequent errors while running development mode. This is because Payload will have updated to your new data shape, but your local database will not have updated. For this reason, we suggest that you leave `push` as its default setting and treat your local dev database as a sandbox. For more information about push mode and prototyping in development, [click here](/docs/v4/database/postgres.md#prototyping-in-development-mode). The typical workflow in Payload is to build out your Payload configs, install plugins, and make progress in development mode - allowing Drizzle to push your changes to your local database for you. Once you're finished, you can create a migration. But importantly, you do not need to run migrations against your development database, because Drizzle will have already pushed your changes to your database for you. > **Warning** > > Warning: do not mix "push" and migrations with your local development > database. If you use "push" locally, and then try to migrate, Payload will > throw a warning, telling you that these two methods are not meant to be used > interchangeably. **2 - create a migration** Once you're done with working in your Payload Config, you can create a migration. It's best practice to try and complete a specific task or fully build out a feature before you create a migration. But once you're ready, you can run `pnpm payload migrate:create`, which will perform the following steps for you: - We will look for any existing migrations, and automatically generate SQL changes necessary to convert your schema from its prior state to the new state of your Payload Config - We will then create a new migration file in your `/migrations` folder that contains all the SQL necessary to be run We won't immediately run this migration for you, however. > **Success** > > Tip: migrations created by Payload are relatively programmatic in nature, so > there should not be any surprises, but before you check in the created > migration it's a good idea to always double-check the contents of the > migration files. **3 - set up your build process to run migrations** Generally, you want to run migrations before you build Payload for production. This typically happens in your CI pipeline and can usually be configured on platforms like Payload Cloud, Vercel, or Netlify by specifying your build script. A common set of scripts in a `package.json`, set up to run migrations in CI, might look like this: ```js "scripts": { // For running in dev mode "dev": "next dev", // To build your Next + Payload app for production "build": "next build", // A "tie-in" to Payload's CLI for convenience // this helps you run `pnpm payload migrate:create` and similar "payload": "cross-env NODE_OPTIONS=--no-deprecation payload", // This command is what you'd set your `build script` to. // Notice how it runs `payload migrate` and then `pnpm build`? // This will run all migrations for you before building, in your CI, // against your production database "ci": "payload migrate && pnpm build", }, ``` In the example above, we've specified a `ci` script which we can use as our "build script" in the platform that we are deploying to production with. This will require that your build pipeline can connect to your database, and it will simply run the `payload migrate` command prior to starting the build process. By calling `payload migrate`, Payload will automatically execute any migrations in your `/migrations` folder that have not yet been executed against your production database, in the order that they were created. If it fails, the deployment will be rejected. But now, with your build script set up to run your migrations, you will be all set! Next time you deploy, your CI will execute the required migrations for you, and your database will be caught up with the shape that your Payload Config requires. ## Running migrations in production In certain cases, you might want to run migrations at runtime when the server starts. Running them during build time may be impossible due to not having access to your database connection while building or similar reasoning. If you're using a long-running server or container where your Node server starts up one time and then stays initialized, you might prefer to run migrations on server startup instead of within your CI. In order to run migrations at runtime, on initialization, you can pass your migrations to your database adapter under the `prodMigrations` key as follows: ```ts // Import your migrations from the `index.ts` file // that Payload generates for you import { migrations } from './migrations' import { buildConfig } from 'payload' export default buildConfig({ // your config here db: postgresAdapter({ // your adapter config here prodMigrations: migrations, }), }) ``` Passing your migrations as shown above will tell Payload, in production only, to execute any migrations that need to be run prior to completing the initialization of Payload. This is ideal for long-running services where Payload will only be initialized at startup. > **Warning:** if Payload is instructed to run migrations in production, this > may slow down serverless cold starts on platforms such as Vercel. Generally, > this option should only be used for long-running servers / containers. ## Environment-Specific Configurations and Migrations Your configuration may include environment-specific settings (e.g., enabling a plugin only in production). If you generate migrations without considering the environment, it can lead to discrepancies and issues. When running migrations locally, Payload uses the development environment, which might miss production-specific configurations. Similarly, running migrations in production could miss development-specific entities. This is an easy oversight, so be mindful of any environment-specific logic in your config when handling migrations. **Ways to address this:** - Manually update your migration file after it is generated to include any environment-specific configurations. - Temporarily enable any required production environment variables in your local setup when generating the migration to capture the necessary updates. - Use separate migration files for each environment to ensure the correct migration is executed in the corresponding environment. # Transactions Source: https://payloadcms.com/docs/beta/database/transactions Database transactions allow your application to make a series of database changes in an all-or-nothing commit. Consider an HTTP request that creates a new **Order** and has an `afterChange` hook to update the stock count of related **Items**. If an error occurs when updating an **Item** and an HTTP error is returned to the user, you would not want the new **Order** to be persisted or any other items to be changed either. This kind of interaction with the database is handled seamlessly with transactions. By default, Payload will use transactions for all data changing operations, as long as it is supported by the configured database. Database changes are contained within all Payload operations and any errors thrown will result in all changes being rolled back without being committed. When transactions are not supported by the database, Payload will continue to operate as expected without them. > **Note:** > > MongoDB requires a connection to a replicaset in order to make use of transactions. > **Note:** > > Transactions in SQLite are disabled by default. You need to pass `transactionOptions: {}` to enable them. The initial request made to Payload will begin a new transaction and attach it to the `req.transactionID`. If you have a `hook` that interacts with the database, you can opt in to using the same transaction by passing the `req` in the arguments. For example: ```ts const afterChange: CollectionAfterChangeHook = async ({ req }) => { // because req.transactionID is assigned from Payload and passed through, // my-slug will only persist if the entire request is successful await req.payload.create({ req, collection: 'my-slug', overrideAccess: true, data: { some: 'data', }, }) } ``` ## Async Hooks with Transactions Since Payload hooks can be async and be written to not await the result, it is possible to have an incorrect success response returned on a request that is rolled back. If you have a hook where you do not `await` the result, then you should **not** pass the `req.transactionID`. ```ts const afterChange: CollectionAfterChangeHook = async ({ req }) => { // WARNING: an async call made with the same req, but NOT awaited, // may fail resulting in an OK response being returned with response data that is not committed const dangerouslyIgnoreAsync = req.payload.create({ req, collection: 'my-slug', overrideAccess: true, data: { some: 'other data', }, }) // Should this call fail, it will not rollback other changes // because the req (and its transactionID) is not passed through const safelyIgnoredAsync = req.payload.create({ collection: 'my-slug', overrideAccess: true, data: { some: 'other data', }, }) } ``` ## Direct Transaction Access When writing your own scripts or custom endpoints, you may wish to have direct control over transactions. This is useful for interacting with your database outside of Payload's Local API. The following functions can be used for managing transactions: - `payload.db.beginTransaction` - Starts a new session and returns a transaction ID for use in other Payload Local API calls. - `payload.db.commitTransaction` - Takes the identifier for the transaction, finalizes any changes. - `payload.db.rollbackTransaction` - Takes the identifier for the transaction, discards any changes. Payload uses the `req` object to pass the transaction ID through to the database adapter. If you are not using the `req` object, you can make a new object to pass the transaction ID directly to database adapter methods and Local API calls. Example: ```ts import payload from 'payload' import config from './payload.config' const standalonePayloadScript = async () => { // initialize Payload await payload.init({ config }) const transactionID = await payload.db.beginTransaction() try { // Make an update using the Local API await payload.update({ collection: 'posts', overrideAccess: true, data: { some: 'data', }, where: { slug: { equals: 'my-slug' }, }, req: { transactionID }, }) /* You can make additional db changes or run other functions that need to be committed on an all or nothing basis */ // Commit the transaction await payload.db.commitTransaction(transactionID) } catch (error) { // Rollback the transaction await payload.db.rollbackTransaction(transactionID) } } standalonePayloadScript() ``` ## Disabling Transactions If you wish to disable transactions entirely, you can do so by passing `false` as the `transactionOptions` in your database adapter configuration. All the official Payload database adapters support this option. In addition to allowing database transactions to be disabled at the adapter level. You can prevent Payload from using a transaction in direct calls to the Local API by adding `disableTransaction: true` to the args. For example: ```ts await payload.update({ collection: 'posts', overrideAccess: true, data: { some: 'data', }, where: { slug: { equals: 'my-slug' }, }, disableTransaction: true, }) ``` # Indexes Source: https://payloadcms.com/docs/beta/database/indexes Database indexes are a way to optimize the performance of your database by allowing it to quickly locate and retrieve data. If you have a field that you frequently query or sort by, adding an index to that field can significantly improve the speed of those operations. When your query runs, the database will not scan the entire document to find that one field, but will instead use the index to quickly locate the data. To index a field, set the `index` option to `true` in your field's config: ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... fields: [ // ... { name: 'title', type: 'text', // highlight-start index: true, // highlight-end }, ], } ``` > **Note:** The `id`, `createdAt`, and `updatedAt` fields are indexed by > default. > **Tip:** If you're using MongoDB, you can use [MongoDB > Compass](https://www.mongodb.com/products/compass) to visualize and manage > your indexes. ## Compound Indexes In addition to indexing single fields, you can also create compound indexes that index multiple fields together. This can be useful for optimizing queries that filter or sort by multiple fields. To create a compound index, use the `indexes` option in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... fields: [ // ... ], indexes: [ { fields: ['title', 'createdAt'], unique: true, // Optional, if you want the combination of fields to be unique }, ], } ``` ## Unique Fields Setting `unique: true` on a field creates a **collection-wide** database unique index on that field's path. This means no two documents in the collection can share the same value for that field. > **Warning** > > **Using `unique` on fields nested inside `array` or `blocks`** creates a > collection-wide unique index on the dotted path (e.g. `items.key`). This is > **not** the same as enforcing uniqueness within a single document's array rows > — it prevents *any two documents* from having the same value at that path. > > Additionally, on **MongoDB**, if the nested field also has `required: true`, > Payload creates a non-sparse unique index. Documents that do not contain the > array will share a `null` value at that path, causing a duplicate key error > when saving a second document without the array. Removing `required: true` > restores the sparse index and avoids this collision. > > If you need to enforce uniqueness _within_ a document's array rows (e.g. no > duplicate keys in the same document), use a custom > [`validate`](/docs/v4/fields/overview.md#validation) function on the array field > instead. ## Localized fields and MongoDB indexes When you set `index: true` or `unique: true` on a localized field, MongoDB creates one index **per locale path** (e.g., `slug.en`, `slug.da-dk`, etc.). With many locales and indexed fields, this can quickly approach MongoDB's per-collection index limit. If you know you'll query specifically by a locale, you can insert a custom MongoDB index for the locale path manually or with a migration script. # MongoDB Source: https://payloadcms.com/docs/beta/database/mongodb To use Payload with MongoDB, install the package `@payloadcms/db-mongodb`. It will come with everything you need to store your Payload data in MongoDB. Then from there, pass it to your Payload Config as follows: ```ts import { mongooseAdapter } from '@payloadcms/db-mongodb' export default buildConfig({ // Your config goes here collections: [ // Collections go here ], // Configure the Mongoose adapter here db: mongooseAdapter({ // Mongoose-specific arguments go here. // URL is required. url: process.env.DATABASE_URL, }), }) ``` ## Options | Option | Description | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. | | `bulkOperationsSingleTransaction` | When `true`, bulk update and delete operations will process documents one at a time in separate transactions instead of all at once in a single transaction. Useful for avoiding transaction limitations with large datasets in DocumentDB and Cosmos DB. Defaults to `false`. | | `connectOptions` | Customize MongoDB connection options. Payload will connect to your MongoDB database using default options which you can override and extend to include all the [options](https://mongoosejs.com/docs/connections.html#options) available to mongoose. | | `collectionsSchemaOptions` | Customize Mongoose schema options for collections. | | `disableIndexHints` | Set to true to disable hinting to MongoDB to use 'id' as index. This is currently done when counting documents for pagination, as it increases the speed of the count function used in that query. Disabling this optimization might fix some problems with AWS DocumentDB. Defaults to false | | `migrationDir` | Customize the directory that migrations are stored. | | `transactionOptions` | An object with configuration properties used in [transactions](https://www.mongodb.com/docs/manual/core/transactions/) or `false` which will disable the use of transactions. | | `collation` | Enable language-specific string comparison with customizable options. Available on MongoDB 3.4+. Defaults locale to "en". Example: `{ strength: 3 }`. For a full list of collation options and their definitions, see the [MongoDB documentation](https://www.mongodb.com/docs/manual/reference/collation/). | | `allowAdditionalKeys` | By default, Payload strips all additional keys from MongoDB data that don't exist in the Payload schema. If you have some data that you want to include to the result but it doesn't exist in Payload, you can set this to `true`. Be careful as Payload access control _won't_ work for this data. | | `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. | | `disableFallbackSort` | Set to `true` to disable the adapter adding a fallback sort when sorting by non-unique fields, this can affect performance in some cases but it ensures a consistent order of results. | | `useAlternativeDropDatabase` | Set to `true` to use an alternative `dropDatabase` implementation that calls `collection.deleteMany({})` on every collection instead of sending a raw `dropDatabase` command. Payload only uses `dropDatabase` for testing purposes. Defaults to `false`. | | `useBigIntForNumberIDs` | Set to `true` to use `BigInt` for custom ID fields of type `'number'`. Useful for databases that don't support `double` or `int32` IDs. Defaults to `false`. | | `useJoinAggregations` | Set to `false` to disable join aggregations (which use correlated subqueries) and instead populate join fields via multiple `find` queries. Defaults to `true`. | | `usePipelineInSortLookup` | Set to `false` to disable the use of `pipeline` in the `$lookup` aggregation in sorting. Defaults to `true`. | ## Access to Mongoose models After Payload is initialized, this adapter exposes all of your Mongoose models and they are available for you to work with directly. You can access Mongoose models as follows: - Collection models - `payload.db.collections[myCollectionSlug]` - Globals model - `payload.db.globals` - Versions model (both collections and globals) - `payload.db.versions[myEntitySlug]` ## Using other MongoDB implementations You can import the `compatibilityOptions` object to get the recommended settings for other MongoDB implementations. Since these databases aren't officially supported by payload, you may still encounter issues even with these settings (please create an issue or PR if you believe these options should be updated): ```ts import { mongooseAdapter, compatibilityOptions } from '@payloadcms/db-mongodb' export default buildConfig({ db: mongooseAdapter({ url: process.env.DATABASE_URL, // For example, if you're using firestore: ...compatibilityOptions.firestore, }), }) ``` We export compatibility options for [DocumentDB](https://aws.amazon.com/documentdb/), [Azure Cosmos DB](https://azure.microsoft.com/en-us/products/cosmos-db) and [Firestore](https://cloud.google.com/firestore/mongodb-compatibility/docs/overview). Known limitations: - Azure Cosmos DB does not support transactions that update two or more documents in different collections, which is a common case when using Payload (via hooks). - Azure Cosmos DB requires the root config property `indexSortableFields` to be set to `true`. ## Using collation There are situations where you may want to use language-specific string comparison, for example when sorting strings with accents or in different languages. MongoDB supports this via [collation](https://www.mongodb.com/docs/manual/reference/collation/). We thread your locale automatically through to the MongoDB queries when collation is enabled in the adapter, so that when you sort by a field, it uses the correct language rules for that locale. To enable collation, set the `collation` option in the adapter configuration: ```ts import { mongooseAdapter } from '@payloadcms/db-mongodb' export default buildConfig({ // Your config goes here collections: [ // Collections go here ], // Configure the Mongoose adapter here db: mongooseAdapter({ // Mongoose-specific arguments go here. // URL is required. url: process.env.DATABASE_URL, collation: { // Set any MongoDB collation options here. // The locale will be set automatically based on the request locale. strength: 1, // Example option }, }), }) ``` > **Warning** > > Collation will affect things like sort based on the locale being used. Please > test thoroughly before using in production as it can affect your queries. # Postgres Source: https://payloadcms.com/docs/beta/database/postgres To use Payload with Postgres, install the package `@payloadcms/db-postgres`. It leverages Drizzle ORM and `node-postgres` to interact with a Postgres database that you provide. Alternatively, the `@payloadcms/db-vercel-postgres` package is also available and is optimized for use with Vercel. It automatically manages changes to your database for you in development mode, and exposes a full suite of migration controls for you to leverage in order to keep other database environments in sync with your schema. DDL transformations are automatically generated. To configure Payload to use Postgres, pass the `postgresAdapter` to your Payload Config as follows: ### Usage `@payloadcms/db-postgres`: ```ts import { postgresAdapter } from '@payloadcms/db-postgres' export default buildConfig({ // Configure the Postgres adapter here db: postgresAdapter({ // Postgres-specific arguments go here. // `pool` is required. pool: { connectionString: process.env.DATABASE_URL, }, }), }) ``` `@payloadcms/db-vercel-postgres`: ```ts import { vercelPostgresAdapter } from '@payloadcms/db-vercel-postgres' export default buildConfig({ // Automatically uses process.env.POSTGRES_URL if no options are provided. db: vercelPostgresAdapter(), // Optionally, can accept the same options as the @vercel/postgres package. db: vercelPostgresAdapter({ pool: { connectionString: process.env.DATABASE_URL, }, }), }) ``` > **Note:** If you're using `vercelPostgresAdapter` and your > `process.env.POSTGRES_URL` or `pool.connectionString` points to a local > database (e.g hostname has `localhost` or `127.0.0.1`) we use the `pg` module > for pooling instead of `@vercel/postgres`. This is because `@vercel/postgres` > doesn't work with local databases, if you want to disable that behavior, you > can pass `forceUseVercelPostgres: true` to the adapter's args and follow > [Vercel > guide](https://vercel.com/docs/storage/vercel-postgres/local-development#option-2:-local-postgres-instance-with-docker) > for a Docker Neon DB setup. ## Options | Option | Description | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `pool` \* | [Pool connection options](https://orm.drizzle.team/docs/quick-postgresql/node-postgres) that will be passed to Drizzle and `node-postgres` or to `@vercel/postgres` | | `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. | | `extensions` | An array of Postgres extension names to install (for example `['unaccent']`). Installing an extension only makes its functions available in the database - it does not, by itself, change how Payload builds queries. To use an extension's function in query generation, pair it with `query.operatorHandlers`. [More details](#custom-query-operators). | | `query.operatorHandlers` | Customize how Payload's `contains`, `like`, `not_like`, and `not_equals` query operators are built, for example to make text matching accent-insensitive. [More details](#custom-query-operators). | | `migrationDir` | Customize the directory that migrations are stored. | | `schemaName` (experimental) | A string for the postgres schema to use, defaults to 'public'. | | `idType` | A string of 'serial', or 'uuid' that is used for the data type given to id columns. | | `transactionOptions` | A PgTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) | | `disableCreateDatabase` | Pass `true` to disable auto database creation if it doesn't exist. Defaults to `false`. | | `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '\_locales'. | | `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '\_rels'. | | `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '\_v'. | | `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) | | `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) | | `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` | | `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. | | `readReplicas` | An array of DB read replicas connection strings, can be used to offload read-heavy traffic. | | `readReplicasAfterWriteInterval` | How long (ms) after a write to keep routing reads to the primary instead of a replica. Prevents stale reads caused by replication lag. Only relevant when `readReplicas` is set. Default `2000`. Set to `0` to disable. | | `blocksAsJSON` | Store blocks as a JSON column instead of using the relational structure which can improve performance with a large amount of blocks | ## Custom query operators Payload maps the `contains` and `like` query operators to Postgres's `ILIKE`, and `not_like` to `NOT ILIKE`. This is case-insensitive, but not accent-insensitive: `contains: 'acido'` does not match a stored value of `Ácido`. Installing a Postgres extension with `extensions` makes its functions available in the database, but does not by itself change how Payload builds queries - `extensions: ['unaccent']` alone does not make any query accent-insensitive. To use an installed extension's function while building a query, configure `query.operatorHandlers`. ### `postgresUnaccent()` The `postgresUnaccent()` preset makes `contains`, `like`, and `not_like` accent-insensitive for `text` and `textarea` fields by wrapping both sides of the comparison in Postgres's `unaccent()` function. It requires the `unaccent` extension: ```ts import { postgresAdapter, postgresUnaccent } from '@payloadcms/db-postgres' export default buildConfig({ db: postgresAdapter({ extensions: ['unaccent'], pool: { connectionString: process.env.DATABASE_URL, }, query: { operatorHandlers: [postgresUnaccent()], }, }), }) ``` Configuring `postgresUnaccent()` without the `unaccent` extension installed in the database will fail at initialization with a clear error. Listing `extensions: ['unaccent']` is the easiest way to have Payload install it for you; an extension installed by hand also satisfies the check. The same import is available from `@payloadcms/db-vercel-postgres` for the Vercel Postgres adapter. A default or custom `id` field stored as a native `uuid` column is left unaffected, even though Payload reports its field type as `text`. A genuine custom `text` or `varchar` `id` field still receives accent-insensitive matching like any other text field. Relationship, upload, and `number` fields are never affected, regardless of the target collection's ID type. ### Custom operand transform To use a different SQL function, provide your own operand-transform handler. Handler callbacks receive SQL as [Drizzle's tagged `sql` template](https://orm.drizzle.team/docs/sql), which keeps values parameterized: ```ts import { postgresAdapter, sql } from '@payloadcms/db-postgres' postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL, }, query: { operatorHandlers: [ { name: 'custom-text-normalization', operators: ['contains', 'like', 'not_like'], transformOperands: ({ column, value }) => ({ column: sql`custom_normalize(${column})`, value: sql`custom_normalize(${value})`, }), }, ], }, }) ``` ### Custom operator replacement A handler can replace the comparison entirely instead of transforming its operands, for example to use a similarity function instead of pattern matching. For the `contains` and `like` operators, `value` arrives already wrapped in `%...%` for the default `ILIKE` pattern match. Strip the `%` before passing the term to a function that does not do pattern matching, such as `similarity()`: ```ts postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL, }, query: { operatorHandlers: [ { name: 'custom-similarity-match', operators: ['contains'], build: ({ column, value }) => { const term = typeof value === 'string' ? value.slice(1, -1) : value return sql`similarity(${column}, ${term}) > 0.4` }, }, ], }, }) ``` An operator replacement changes matching semantics only - it does not add relevance sorting or expose a new Payload query operator. Only one replacement handler may match a given resolved operator and field type; a conflicting configuration fails at initialization, naming both handlers and the operator. ### Limitations - JSON, rich text, and `blocksAsJSON` queries do not go through `query.operatorHandlers` in the current release. A handler like `postgresUnaccent()` does not apply to those fields. - `near`, `within`, and `intersects` queries on `point` fields build PostGIS-specific SQL directly and do not go through `query.operatorHandlers`. - A `not_equals` handler customizes only the non-null comparison. Payload always additionally matches documents where the field is `null`, regardless of any handler. - `hasMany` `contains` queries on `text` or `textarea` fields, localized text columns, and text stored in relational array/block tables are all supported. ### Indexing Wrapping a column in a function, as `postgresUnaccent()` and custom operand transforms do, prevents a plain index on the original column from being used for that comparison. Unanchored `%term%` searches already need specialized indexing (such as a trigram index) at scale, independent of this feature. If you need an indexed accent-insensitive lookup, consider a functional index or an adapter-managed normalized column. Postgres's built-in `unaccent()` function is marked `STABLE`, not `IMMUTABLE`, so it cannot be used directly in a standard expression index. Building one requires wrapping `unaccent()` in your own `IMMUTABLE` function first. ### Security Handlers run server-side SQL construction code. Always build SQL with Drizzle's tagged `sql` template, as shown above, so values stay parameterized. Never concatenate request data into `sql.raw`. ## Access to Drizzle After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it. To ensure type-safety, you need to generate Drizzle schema first with: ```sh npx payload generate:db-schema ``` Then, you can access Drizzle as follows: ```ts import { posts } from './payload-generated-schema' // To avoid installing Drizzle, you can import everything that drizzle has from our re-export path. import { eq, sql, and } from '@payloadcms/db-postgres/drizzle' // Drizzle's Querying API: https://orm.drizzle.team/docs/rqb const posts = await payload.db.drizzle.query.posts.findMany() // Drizzle's Select API https://orm.drizzle.team/docs/select const result = await payload.db.drizzle .select() .from(posts) .where( and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example post title'`), ) ``` ## Tables, relations, and enums In addition to exposing Drizzle directly, all of the tables, Drizzle relations, and enum configs are exposed for you via the `payload.db` property as well. - Tables - `payload.db.tables` - Enums - `payload.db.enums` - Relations - `payload.db.relations` ## Prototyping in development mode Drizzle exposes two ways to work locally in development mode. The first is [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push), which automatically pushes changes you make to your Payload Config (and therefore, Drizzle schema) to your database so you don't have to manually migrate every time you change your Payload Config. This only works in development mode, and should not be mixed with manually running [`migrate`](/docs/v4/database/migrations.md) commands. You will be warned if any changes that you make will entail data loss while in development mode. Push is enabled by default, but you can opt out if you'd like. Alternatively, you can disable `push` and rely solely on migrations to keep your local database in sync with your Payload Config. ## Migration workflows In Postgres, migrations are a fundamental aspect of working with Payload and you should become familiar with how they work. For more information about migrations, [click here](/docs/v4/database/migrations.md#when-to-run-migrations). ## Drizzle schema hooks ### beforeSchemaInit Runs before the schema is built. You can use this hook to extend your database structure with tables that won't be managed by Payload. ```ts import { postgresAdapter } from '@payloadcms/db-postgres' import { integer, pgTable, serial, } from '@payloadcms/db-postgres/drizzle/pg-core' postgresAdapter({ beforeSchemaInit: [ ({ schema, adapter }) => { return { ...schema, tables: { ...schema.tables, addedTable: pgTable('added_table', { id: serial('id').notNull(), }), }, } }, ], }) ``` One use case is preserving your existing database structure when migrating to Payload. By default, Payload drops the current database schema, which may not be desirable in this scenario. To quickly generate the Drizzle schema from your database you can use [Drizzle Introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull) You should get the `schema.ts` file which may look like this: ```ts import { pgTable, uniqueIndex, serial, varchar, text, } from 'drizzle-orm/pg-core' export const users = pgTable('users', { id: serial('id').primaryKey(), fullName: text('full_name'), phone: varchar('phone', { length: 256 }), }) export const countries = pgTable( 'countries', { id: serial('id').primaryKey(), name: varchar('name', { length: 256 }), }, (countries) => { return { nameIndex: uniqueIndex('name_idx').on(countries.name), } }, ) ``` You can import them into your config and append to the schema with the `beforeSchemaInit` hook like this: ```ts import { postgresAdapter } from '@payloadcms/db-postgres' import { users, countries } from '../drizzle/schema' postgresAdapter({ beforeSchemaInit: [ ({ schema, adapter }) => { return { ...schema, tables: { ...schema.tables, users, countries, }, } }, ], }) ``` Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection. ### afterSchemaInit Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config. To extend a table, Payload exposes `extendTable` utility to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration). The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns. ```ts import { postgresAdapter } from '@payloadcms/db-postgres' import { index, integer } from '@payloadcms/db-postgres/drizzle/pg-core' import { buildConfig } from 'payload' export default buildConfig({ collections: [ { slug: 'places', fields: [ { name: 'country', type: 'text', }, { name: 'city', type: 'text', }, ], }, ], db: postgresAdapter({ afterSchemaInit: [ ({ schema, extendTable, adapter }) => { extendTable({ table: schema.tables.places, columns: { extraIntegerColumn: integer('extra_integer_column'), }, extraConfig: (table) => ({ country_city_composite_index: index( 'country_city_composite_index', ).on(table.country, table.city), }), }) return schema }, ], }), }) ``` ### Note for generated schema: Columns and tables, added in schema hooks won't be added to the generated via `payload generate:db-schema` Drizzle schema. If you want them to be there, you either have to edit this file manually or mutate the internal Payload "raw" SQL schema in the `beforeSchemaInit`: ```ts import { postgresAdapter } from '@payloadcms/db-postgres' postgresAdapter({ beforeSchemaInit: [ ({ schema, adapter }) => { // Add a new table adapter.rawTables.myTable = { name: 'my_table', columns: { my_id: { name: 'my_id', type: 'serial', primaryKey: true, }, }, } // Add a new column to generated by Payload table: adapter.rawTables.posts.columns.customColumn = { name: 'custom_column', // Note that Payload SQL doesn't support everything that Drizzle does. type: 'integer', notNull: true, } // Add a new index to generated by Payload table: adapter.rawTables.posts.indexes.customColumnIdx = { name: 'custom_column_idx', unique: true, on: ['custom_column'], } return schema }, ], }) ``` # SQLite Source: https://payloadcms.com/docs/beta/database/sqlite To use Payload with SQLite, install the package `@payloadcms/db-sqlite`. It leverages Drizzle ORM and `libSQL` to interact with a SQLite database that you provide. It automatically manages changes to your database for you in development mode, and exposes a full suite of migration controls for you to leverage in order to keep other database environments in sync with your schema. DDL transformations are automatically generated. To configure Payload to use SQLite, pass the `sqliteAdapter` to your Payload Config as follows: ```ts import { sqliteAdapter } from '@payloadcms/db-sqlite' export default buildConfig({ // Your config goes here collections: [ // Collections go here ], // Configure the SQLite adapter here db: sqliteAdapter({ // SQLite-specific arguments go here. // `client.url` is required. client: { url: process.env.DATABASE_URL, authToken: process.env.DATABASE_AUTH_TOKEN, }, }), }) ``` ## Options | Option | Description | | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `client` \* | [Client connection options](https://orm.drizzle.team/docs/get-started-sqlite#turso) that will be passed to `createClient` from `@libsql/client`. | | `push` | Disable Drizzle's [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push) in development mode. By default, `push` is enabled for development mode only. | | `migrationDir` | Customize the directory that migrations are stored. | | `logger` | The instance of the logger to be passed to drizzle. By default Payload's will be used. | | `idType` | A string of 'number', or 'uuid' that is used for the data type given to id columns. | | `transactionOptions` | A SQLiteTransactionConfig object for transactions, or set to `false` to disable using transactions. [More details](https://orm.drizzle.team/docs/transactions) | | `localesSuffix` | A string appended to the end of table names for storing localized fields. Default is '\_locales'. | | `relationshipsSuffix` | A string appended to the end of table names for storing relationships. Default is '\_rels'. | | `versionsSuffix` | A string appended to the end of table names for storing versions. Defaults to '\_v'. | | `beforeSchemaInit` | Drizzle schema hook. Runs before the schema is built. [More Details](#beforeschemainit) | | `afterSchemaInit` | Drizzle schema hook. Runs after the schema is built. [More Details](#afterschemainit) | | `generateSchemaOutputFile` | Override generated schema from `payload generate:db-schema` file path. Defaults to `{CWD}/src/payload-generated.schema.ts` | | `autoIncrement` | Pass `true` to enable SQLite [AUTOINCREMENT](https://www.sqlite.org/autoinc.html) for primary keys to ensure the same ID cannot be reused from deleted rows | | `allowIDOnCreate` | Set to `true` to use the `id` passed in data on the create API operations without using a custom ID field. | | `blocksAsJSON` | Store blocks as a JSON column instead of using the relational structure which can improve performance with a large amount of blocks | | `busyTimeout` | Maximum time in milliseconds to wait when the database is locked. Default is `0`. | | `wal` | Enable Write-Ahead Logging (WAL) mode for improved performance and concurrency. [More Details](#wal-mode) | ## Access to Drizzle After Payload is initialized, this adapter will expose the full power of Drizzle to you for use if you need it. To ensure type-safety, you need to generate Drizzle schema first with: ```sh npx payload generate:db-schema ``` Then, you can access Drizzle as follows: ```ts // Import table from the generated file import { posts } from './payload-generated-schema' // To avoid installing Drizzle, you can import everything that drizzle has from our re-export path. import { eq, sql, and } from '@payloadcms/db-sqlite/drizzle' // Drizzle's Querying API: https://orm.drizzle.team/docs/rqb const posts = await payload.db.drizzle.query.posts.findMany() // Drizzle's Select API https://orm.drizzle.team/docs/select const result = await payload.db.drizzle .select() .from(posts) .where( and(eq(posts.id, 50), sql`lower(${posts.title}) = 'example post title'`), ) ``` ## Tables and relations In addition to exposing Drizzle directly, all of the tables and Drizzle relations are exposed for you via the `payload.db` property as well. - Tables - `payload.db.tables` - Relations - `payload.db.relations` ## Prototyping in development mode Drizzle exposes two ways to work locally in development mode. The first is [`db push`](https://orm.drizzle.team/kit-docs/overview#prototyping-with-db-push), which automatically pushes changes you make to your Payload Config (and therefore, Drizzle schema) to your database so you don't have to manually migrate every time you change your Payload Config. This only works in development mode, and should not be mixed with manually running [`migrate`](/docs/v4/database/migrations.md) commands. You will be warned if any changes that you make will entail data loss while in development mode. Push is enabled by default, but you can opt out if you'd like. Alternatively, you can disable `push` and rely solely on migrations to keep your local database in sync with your Payload Config. ## Migration workflows In SQLite, migrations are a fundamental aspect of working with Payload and you should become familiar with how they work. For more information about migrations, [click here](/docs/v4/database/migrations.md#when-to-run-migrations). ## Drizzle schema hooks ### beforeSchemaInit Runs before the schema is built. You can use this hook to extend your database structure with tables that won't be managed by Payload. ```ts import { sqliteAdapter } from '@payloadcms/db-sqlite' import { integer, sqliteTable } from '@payloadcms/db-sqlite/drizzle/sqlite-core' sqliteAdapter({ beforeSchemaInit: [ ({ schema, adapter }) => { return { ...schema, tables: { ...schema.tables, addedTable: sqliteTable('added_table', { id: integer('id').primaryKey({ autoIncrement: true }), }), }, } }, ], }) ``` One use case is preserving your existing database structure when migrating to Payload. By default, Payload drops the current database schema, which may not be desirable in this scenario. To quickly generate the Drizzle schema from your database you can use [Drizzle Introspection](https://orm.drizzle.team/kit-docs/commands#introspect--pull) You should get the `schema.ts` file which may look like this: ```ts import { sqliteTable, text, uniqueIndex, integer, } from 'drizzle-orm/sqlite-core' export const users = sqliteTable('users', { id: integer('id').primaryKey({ autoIncrement: true }), fullName: text('full_name'), phone: text('phone', { length: 256 }), }) export const countries = sqliteTable( 'countries', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name', { length: 256 }), }, (countries) => { return { nameIndex: uniqueIndex('name_idx').on(countries.name), } }, ) ``` You can import them into your config and append to the schema with the `beforeSchemaInit` hook like this: ```ts import { sqliteAdapter } from '@payloadcms/db-sqlite' import { users, countries } from '../drizzle/schema' sqliteAdapter({ beforeSchemaInit: [ ({ schema, adapter }) => { return { ...schema, tables: { ...schema.tables, users, countries, }, } }, ], }) ``` Make sure Payload doesn't overlap table names with its collections. For example, if you already have a collection with slug "users", you should either change the slug or `dbName` to change the table name for this collection. ### afterSchemaInit Runs after the Drizzle schema is built. You can use this hook to modify the schema with features that aren't supported by Payload, or if you want to add a column that you don't want to be in the Payload config. To extend a table, Payload exposes `extendTable` utility to the args. You can refer to the [Drizzle documentation](https://orm.drizzle.team/docs/sql-schema-declaration). The following example adds the `extra_integer_column` column and a composite index on `country` and `city` columns. ```ts import { sqliteAdapter } from '@payloadcms/db-sqlite' import { index, integer } from '@payloadcms/db-sqlite/drizzle/sqlite-core' import { buildConfig } from 'payload' export default buildConfig({ collections: [ { slug: 'places', fields: [ { name: 'country', type: 'text', }, { name: 'city', type: 'text', }, ], }, ], db: sqliteAdapter({ afterSchemaInit: [ ({ schema, extendTable, adapter }) => { extendTable({ table: schema.tables.places, columns: { extraIntegerColumn: integer('extra_integer_column'), }, extraConfig: (table) => ({ country_city_composite_index: index( 'country_city_composite_index', ).on(table.country, table.city), }), }) return schema }, ], }), }) ``` ### Note for generated schema: Columns and tables, added in schema hooks won't be added to the generated via `payload generate:db-schema` Drizzle schema. If you want them to be there, you either have to edit this file manually or mutate the internal Payload "raw" SQL schema in the `beforeSchemaInit`: ```ts import { sqliteAdapter } from '@payloadcms/db-sqlite' sqliteAdapter({ beforeSchemaInit: [ ({ schema, adapter }) => { // Add a new table adapter.rawTables.myTable = { name: 'my_table', columns: { my_id: { name: 'my_id', type: 'integer', primaryKey: true, }, }, } // Add a new column to generated by Payload table: adapter.rawTables.posts.columns.customColumn = { name: 'custom_column', // Note that Payload SQL doesn't support everything that Drizzle does. type: 'integer', notNull: true, } // Add a new index to generated by Payload table: adapter.rawTables.posts.indexes.customColumnIdx = { name: 'custom_column_idx', unique: true, on: ['custom_column'], } return schema }, ], }) ``` ## D1 Database > **Warning** > > This adapter is currently in beta as it is new and could be subject to changes > which may be considered breaking We also provide a separate adapter to connect to [Cloudflare D1](https://developers.cloudflare.com/d1/), which is a serverless SQLite database. To use it, install the package `@payloadcms/db-d1-sqlite` and configure it as follows: ```ts import { sqliteD1Adapter } from '@payloadcms/db-d1-sqlite' export default buildConfig({ // Your config goes here collections: [ // Collections go here ], // Configure the D1 adapter here db: sqliteD1Adapter({ // D1-specific arguments go here. // `binding` is required and should match the D1 database binding name in your Cloudflare Worker environment. binding: cloudflare.env.D1, }), }) ``` It inherits the options from the SQLite adapter above with the exception of the connection options in favour of the `binding`. You can see our [Cloudflare D1 template](https://github.com/payloadcms/payload/tree/main/templates/with-cloudflare-d1) for a full example of how to set this up. ### D1 Read Replicas You can enable read replicas support with the `first-primary` strategy. This is experimental. You must also enable it on your D1 database in the Cloudflare dashboard. Read more about it in the [Cloudflare documentation](https://developers.cloudflare.com/d1/best-practices/read-replication/). > **Note** > > All write queries are still forwarded to the primary database instance. Read > replication only improves the response time for read query requests. ```ts import { sqliteD1Adapter } from '@payloadcms/db-d1-sqlite' export default buildConfig({ collections: [], db: sqliteD1Adapter({ binding: cloudflare.env.D1, // You can also enable read replicas support with the `first-primary` strategy. readReplicas: 'first-primary', }), }) ``` You can then verify that they're being used by checking the logs in your Cloudflare dashboard. You should see logs indicating whether a read or write operation was performed, and on which database instance. ### WAL Mode You can enable Write-Ahead Logging (WAL) mode for improved performance and concurrency by passing the `wal` option to the SQLite adapter. ```ts title="Example SQLite Adapter with WAL" import { sqliteAdapter } from '@payloadcms/db-sqlite' export default buildConfig({ // Your config goes here collections: [ // Collections go here ], // Configure the SQLite adapter here db: sqliteAdapter({ wal: true, // Enable WAL mode with default settings client: { url: process.env.DATABASE_URL, }, }), }) ``` Settings for the `wal` option: | Setting | Description | Default | | ------------------ | ------------------------------------------------------------------------------------------------- | ----------------- | | `journalSizeLimit` | Max size of the WAL file before checkpointing to the main DB. | `67108864` (64MB) | | `synchronous` | WAL sync mode: `EXTRA`, `FULL`, `NORMAL`, `OFF`. Controls balance of performance vs. data safety. | `'FULL'` | # Fields Overview Source: https://payloadcms.com/docs/beta/fields/overview Fields are the building blocks of Payload. They define the schema of the Documents that will be stored in the [Database](/docs/v4/database/overview.md), as well as automatically generate the corresponding UI within the [Admin Panel](/docs/v4/admin/overview.md). There are many [Field Types](#field-types) to choose from, ranging anywhere from simple text strings to nested arrays and blocks. Most fields save data to the database, while others are strictly presentational. Fields can have [Custom Validations](#validation), [Conditional Logic](/docs/v4/fields/overview.md#conditional-logic), [Access Control](#field-level-access-control), [Hooks](#field-level-hooks), and so much more. Fields can be endlessly customized in their appearance and behavior without affecting their underlying data structure. Fields are designed to withstand heavy modification or even complete replacement through the use of [Custom Field Components](#custom-components). > **Note** > > Payload also generates fields based on your collection and global > configuration. See [Fields Generated by Payload](/docs/v4/fields/default-fields.md) to learn > which fields you can override and how their configurations are merged. To configure fields, use the `fields` property in your [Collection](/docs/v4/configuration/collections.md) or [Global](/docs/v4/configuration/globals.md) config: ```ts import type { CollectionConfig } from 'payload' export const Page: CollectionConfig = { // ... fields: [ // highlight-line // ... ], } ``` ## Field Types Payload provides a wide variety of built-in Field Types, each with its own unique properties and behaviors that determine which values it can accept, how it is presented in the API, and how it will be rendered in the [Admin Panel](/docs/v4/admin/overview.md). To configure fields, use the `fields` property in your [Collection](/docs/v4/configuration/collections.md) or [Global](/docs/v4/configuration/globals.md) config: ```ts import type { CollectionConfig } from 'payload' export const Page: CollectionConfig = { slug: 'pages', // highlight-start fields: [ { name: 'field', type: 'text', }, ], // highlight-end } ``` > **Reminder:** Each field is an object with at least the `type` property. This > matches the field to its corresponding Field Type. [More > details](#field-options). There are three main categories of fields in Payload: - [Data Fields](#data-fields) - [Presentational Fields](#presentational-fields) - [Virtual Fields](#virtual-fields) To begin writing fields, first determine which [Field Type](#field-types) best supports your application. Then author your field accordingly using the [Field Options](#field-options) for your chosen field type. ### Data Fields Data Fields are used to store data in the [Database](/docs/v4/database/overview.md). All Data Fields have a `name` property. This is the key that will be used to store the field's value. Here are the available Data Fields: - [Array](/docs/v4/fields/array.md) - for repeating content, supports nested fields - [Blocks](/docs/v4/fields/blocks.md) - for block-based content, supports nested fields - [Checkbox](/docs/v4/fields/checkbox.md) - saves boolean true / false values - [Code](/docs/v4/fields/code.md) - renders a code editor interface that saves a string - [Date](/docs/v4/fields/date.md) - renders a date picker and saves a timestamp - [Email](/docs/v4/fields/email.md) - ensures the value is a properly formatted email address - [Group](/docs/v4/fields/group.md) - nests fields within a keyed object - [JSON](/docs/v4/fields/json.md) - renders a JSON editor interface that saves a JSON object - [Number](/docs/v4/fields/number.md) - saves numeric values - [Point](/docs/v4/fields/point.md) - for location data, saves geometric coordinates - [Radio](/docs/v4/fields/radio.md) - renders a radio button group that allows only one value to be selected - [Relationship](/docs/v4/fields/relationship.md) - assign relationships to other collections - [Rich Text](/docs/v4/fields/rich-text.md) - renders a fully extensible rich text editor - [Select](/docs/v4/fields/select.md) - renders a dropdown / picklist style value selector - [Tabs (Named)](/docs/v4/fields/tabs.md) - similar to group, but renders nested fields within a tabbed layout - [Text](/docs/v4/fields/text.md) - simple text input that saves a string - [Textarea](/docs/v4/fields/textarea.md) - similar to text, but allows for multi-line input - [Upload](/docs/v4/fields/upload.md) - allows local file and image upload ### Presentational Fields Presentational Fields do not store data in the database. Instead, they are used to organize and present other fields in the [Admin Panel](/docs/v4/admin/overview.md), or to add custom UI components. Here are the available Presentational Fields: - [Collapsible](/docs/v4/fields/collapsible.md) - nests fields within a collapsible component - [Row](/docs/v4/fields/row.md) - aligns fields horizontally - [Tabs (Unnamed)](/docs/v4/fields/tabs.md) - nests fields within a tabbed layout. It is not presentational if the tab has a name. - [Group (Unnamed)](/docs/v4/fields/group.md) - nests fields within a keyed object. It is not presentational if the group has a name. - [UI](/docs/v4/fields/ui.md) - blank field for custom UI components ### Virtual Fields Virtual fields display data that is not stored in the database, but is computed or derived from other fields. Here are the available Virtual Fields: - [Join](/docs/v4/fields/join.md) - achieves two-way data binding between fields > **Tip:** Don't see a built-in field type that you need? Build it! Using a > combination of [Field Validations](#validation) and [Custom > Components](/docs/v4/custom-components/overview.md), you can override the entirety of > how a component functions within the [Admin Panel](/docs/v4/admin/overview.md) to > effectively create your own field type. ## Virtual Field Configuration While [Join fields](#virtual-fields) are purpose-built virtual field types, **any field type can be made virtual** by adding the `virtual` property to its configuration. This allows you to create computed or relationship-derived fields that appear in API responses without being stored in the database. Virtual fields are populated during API responses and can be used in the Admin Panel, but their values are not persisted to the database. This makes them ideal for displaying read-only computed data, relationship summaries, or formatted versions of existing field data. ### Configuring Virtual Fields Any field type can be made virtual by adding the `virtual` property to the field configuration. The `virtual` property can be configured in two ways: #### Boolean Virtual Fields When `virtual` is set to `true`, the field becomes virtual but doesn't automatically populate any data. You'll typically use [Field-level Hooks](#field-level-hooks) to compute and populate the field's value: ```ts { name: 'fullName', type: 'text', virtual: true, hooks: { afterRead: [ ({ siblingData }) => { return `${siblingData.firstName} ${siblingData.lastName}` } ] } } ``` #### String Path Virtual Fields When `virtual` is set to a string path, it creates a "virtual relationship field" that automatically resolves to data from another field in the document. This is particularly useful for displaying relationship data: ```ts { name: 'authorName', type: 'text', virtual: 'author.name' // Resolves to the 'name' field of the 'author' relationship } ``` ### Virtual Path Syntax Virtual paths use dot notation to traverse relationships and nested data: - `author.name` - Gets the `name` field from the `author` relationship - `author.profile.bio` - Gets the `bio` field from a nested `profile` object within the `author` relationship - `categories.title` - For hasMany relationships, returns an array of `title` values - `request.additionalStakeholders.email` - Traverses multiple relationship levels **Important Requirements for Virtual Path Fields:** 1. **Source Relationship Required**: The document must have a relationship field that corresponds to the first part of the virtual path. For example, if using `virtual: 'author.name'`, there must be an `author` relationship field defined in the same collection. 2. **Path Resolution**: Virtual paths resolve at query time by following the relationships and extracting the specified field values. 3. **Array Handling**: When the virtual path traverses a `hasMany` relationship, the result will be an array of values. ### Common Use Cases #### Displaying Relationship Names Instead of just showing relationship IDs, display the actual names or titles: ```ts // Original relationship field { name: 'author', type: 'relationship', relationTo: 'users' }, // Virtual field to display author's name { name: 'authorName', type: 'text', virtual: 'author.name' } ``` #### Multiple Relationship Values For `hasMany` relationships, virtual fields return arrays: ```ts // Original relationship field { name: 'categories', type: 'relationship', relationTo: 'categories', hasMany: true }, // Virtual field to display category titles { name: 'categoryTitles', type: 'text', virtual: 'categories.title' // Returns ['Tech', 'News', 'Updates'] } ``` #### Computed Values Use hooks to create computed virtual fields: ```ts { name: 'wordCount', type: 'number', virtual: true, hooks: { afterRead: [ ({ siblingData }) => { const content = siblingData.content || '' return content.split(/\s+/).length } ] } } ``` ### Virtual Fields in API Responses Virtual fields appear in API responses alongside regular fields: ```json { "id": "123", "title": "My Post", "author": "64f1234567890abcdef12345", "authorName": "John Doe", // Virtual field "categories": ["64f9876543210fedcba67890", "64f5432109876543210abcdef"], "categoryTitles": ["Tech", "News"], // Virtual field "wordCount": 450 // Virtual field } ``` > **Important:** When using virtual path fields, ensure that the referenced > relationship field exists in your schema. Virtual paths like `author.name` > require an `author` relationship field to be defined, otherwise the virtual > field will not resolve properly. ## Field Options All fields require at least the `type` property. This matches the field to its corresponding [Field Type](#field-types) to determine its appearance and behavior within the [Admin Panel](/docs/v4/admin/overview.md). Each Field Type has its own unique set of options based on its own type. To set a field's type, use the `type` property in your Field Config: ```ts import type { Field } from 'payload' export const MyField: Field = { type: 'text', // highlight-line name: 'myField', } ``` > **Warning** > > For a full list of configuration options, see the documentation for each > [Field Type](#field-types). ### Field Names All [Data Fields](#data-fields) require a `name` property. This is the key that will be used to store and retrieve the field's value in the database. This property must be unique amongst this field's siblings. To set a field's name, use the `name` property in your Field Config: ```ts import type { Field } from 'payload' export const MyField: Field = { type: 'text', name: 'myField', // highlight-line } ``` Since field names map directly to keys in API response objects and to column names in the database, they should follow JavaScript identifier conventions: start with a letter or underscore, and contain only letters, numbers, and underscores. Just like all Payload field names are strings, all Payload response objects use strings as their keys — so treat field names as you would a JavaScript variable name. > **Warning** > > Avoid field names that start with a digit (e.g. `'1'`, `'2field'`) or contain > hyphens (e.g. `'field-name'`). While these are valid strings in TypeScript, > they can cause unexpected behavior when Payload accesses data using > dot-notation paths, generates GraphQL schemas, or maps fields to database > columns. GraphQL identifiers do not allow hyphens, so hyphenated field names > will cause schema generation to fail. Payload reserves various field names for internal use. Using reserved field names will result in your field being sanitized from the config. The following field names are forbidden and cannot be used: - `__v` - `salt` - `hash` - `file` - `status` - with Postgres Adapter and when drafts are enabled ### Field-level Hooks In addition to being able to define [Hooks](/docs/v4/hooks/overview.md) on a document-level, you can define extremely granular logic field-by-field. To define Field-level Hooks, use the `hooks` property in your Field Config: ```ts import type { Field } from 'payload' export const MyField: Field = { type: 'text', name: 'myField', // highlight-start hooks: { // ... }, // highlight-end } ``` For full details on Field-level Hooks, see the [Field Hooks](/docs/v4/hooks/fields.md) documentation. ### Field-level Access Control In addition to being able to define [Access Control](/docs/v4/access-control/overview.md) on a document-level, you can define extremely granular permissions field-by-field. To define Field-level Access Control, use the `access` property in your Field Config: ```ts import type { Field } from 'payload' export const MyField: Field = { type: 'text', name: 'myField', // highlight-start access: { // ... }, // highlight-end } ``` For full details on Field-level Access Control, see the [Field Access Control](/docs/v4/access-control/fields.md) documentation. ### Disabling Field Duplication Fields are copied from the source document when a document is duplicated. To prevent a field's source value from being copied, set `disableDuplicate` to `true`: ```ts import type { Field } from 'payload' export const MyField: Field = { name: 'secret', type: 'text', disableDuplicate: true, // highlight-line defaultValue: 'new-secret', } ``` This option is available on fields that store their own value, including named groups and tabs. For layout-only rows, collapsibles, unnamed groups, and unnamed tabs, set it on the nested fields that own the values you do not want copied. When the field is omitted from the duplicate request, Payload applies its normal create defaults. For groups, nested field defaults are applied. Arrays and blocks use their field-level default value, if configured, and then apply nested defaults to the default rows or blocks. Setting `disableDuplicate` on a nested field preserves its parent row or block and applies that field's default. Request data may still provide a value for the field, subject to its normal `create` access control. The option is not bypassed by `overrideAccess`, and `beforeDuplicate` hooks do not run for a disabled field or its descendants. ### Default Values Fields can be optionally prefilled with initial values. This is used in both the [Admin Panel](/docs/v4/admin/overview.md) as well as API requests to populate missing or undefined field values during the `create` or `update` operations. To set a field's default value, use the `defaultValue` property in your Field Config: ```ts import type { Field } from 'payload' export const MyField: Field = { type: 'text', name: 'myField', defaultValue: 'Hello, World!', // highlight-line } ``` Default values can be defined as a static value or a function that returns a value. When a `defaultValue` is defined statically, Payload's [Database Adapters](/docs/v4/database/overview.md) will apply it to the database schema or models. Functions can be written to make use of the following argument properties: - `user` - the authenticated user object - `locale` - the currently selected locale string - `req` - the `PayloadRequest` object Here is an example of a `defaultValue` function: ```ts import type { Field } from 'payload' const translation: { en: 'Written by' es: 'Escrito por' } export const myField: Field = { name: 'attribution', type: 'text', // highlight-start defaultValue: ({ user, locale, req }) => `${translation[locale]} ${user.name}`, // highlight-end } ``` > **Tip:** You can use async `defaultValue` functions to fill fields with data > from API requests or Local API using `req.payload`. ### Validation Fields are automatically validated based on their [Field Type](#field-types) and other [Field Options](#field-options) such as `required` or `min` and `max` value constraints. If needed, however, field validations can be customized or entirely replaced by providing your own custom validation functions. To set a custom field validation function, use the `validate` property in your Field Config: ```ts import type { Field } from 'payload' export const MyField: Field = { type: 'text', name: 'myField', validate: (value) => Boolean(value) || 'This field is required', // highlight-line } ``` Custom validation functions should return either `true` or a `string` representing the error message to display in API responses. The following arguments are provided to the `validate` function: | Argument | Description | | -------- | ------------------------------------------------------------------------------- | | `value` | The value of the field being validated. | | `ctx` | An object with additional data and context. [More details](#validation-context) | #### Validation Context The `ctx` argument contains full document data, sibling field data, the current operation, and other useful information such as currently authenticated user: ```ts import type { Field } from 'payload' export const MyField: Field = { type: 'text', name: 'myField', // highlight-start validate: (val, { user }) => Boolean(user) || 'You must be logged in to save this field', // highlight-end } ``` The following additional properties are provided in the `ctx` object: | Property | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `data` | An object containing the full collection or global document currently being edited. | | `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. | | `operation` | Will be `create` or `update` depending on the UI action or API call. | | `path` | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. | | `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation. | | `req` | The current HTTP request object. Contains `payload`, `user`, etc. | | `event` | Either `onChange` or `submit` depending on the current action. Used as a performance opt-in. [More details](#validation-performance). | #### Localized and Built-in Error Messages You can return localized error messages by utilizing the translation function provided in the `req` object: ```ts import type { Field } from 'payload' export const MyField: Field = { type: 'text', name: 'myField', validate: (value, { req: { t } }) => Boolean(value) || t('validation:required'), // highlight-line } ``` This way you can use [Custom Translations](/docs/v4/configuration/i18n.md#custom-translations) as well as Payload's built in error messages (like `validation:required` used in the example above). For a full list of available translation strings, see the [english translation file](https://github.com/payloadcms/payload/blob/main/packages/translations/src/languages/en.ts) of Payload. #### Reusing Default Field Validations When using custom validation functions, Payload will use yours in place of the default. However, you might want to simply augment the default validation with your own custom logic. To reuse default field validations, call them from within your custom validation function: ```ts import { text } from 'payload/shared' const field: Field = { name: 'notBad', type: 'text', validate: (val, args) => { if (val === 'bad') return 'This cannot be "bad"' return text(val, args) // highlight-line }, } ``` Here is a list of all default field validation functions: ```ts import { array, blocks, checkbox, code, date, email, json, number, point, radio, relationship, richText, select, tabs, text, textarea, upload, } from 'payload/shared' ``` #### Validation Performance When writing async or computationally heavy validation functions, it is important to consider the performance implications. Within the Admin Panel, validations are executed on every change to the field, so they should be as lightweight as possible and only run when necessary. If you need to perform expensive validations, such as querying the database, consider using the `event` property in the `ctx` object to only run that particular validation on form submission. To write asynchronous validation functions, use the `async` keyword to define your function: ```ts import type { CollectionConfig } from 'payload' export const Orders: CollectionConfig = { slug: 'orders', fields: [ { name: 'customerNumber', type: 'text', // highlight-start validate: async (val, { event }) => { if (event === 'onChange') { return true } // only perform expensive validation when the form is submitted const response = await fetch(`https://your-api.com/customers/${val}`) if (response.ok) { return true } return 'The customer number provided does not match any customers within our records.' }, // highlight-end }, ], } ``` > **Success** > > For more performance tips, see the [Performance > documentation](/docs/v4/performance/overview.md). ## Admin Options You can customize the appearance and behavior of fields within the [Admin Panel](/docs/v4/admin/overview.md) through the `admin` property of any Field Config: ```ts import type { CollectionConfig } from 'payload' export const CollectionConfig: CollectionConfig = { // ... fields: [ // ... { name: 'myField', type: 'text', admin: { // highlight-line // ... }, }, ], } ``` The following options are available: | Option | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`condition`** | Programmatically show / hide fields based on other fields. [More details](#conditional-logic). | | **`components`** | All Field Components can be swapped out for [Custom Components](/docs/v4/custom-components/overview.md) that you define. | | **`description`** | Helper text to display alongside the field to provide more information for the editor. [More details](#description). | | **`position`** | Use `'sidebar'` to render the field in the sidebar or `'main'` (default) to keep it in the main area. | | **`width`** | Restrict the width of a field. You can pass any string-based value here, be it pixels, percentages, etc. This property is especially useful when fields are nested within a `Row` type where they can be organized horizontally. | | **`style`** | [CSS Properties](https://developer.mozilla.org/en-US/docs/Web/CSS) to inject into the root element of the field. | | **`className`** | Attach a [CSS class attribute](https://developer.mozilla.org/en-US/docs/Web/CSS/Class_selectors) to the root DOM element of a field. | | **`readOnly`** | Setting a field to `readOnly` has no effect on the API whatsoever but disables the admin component's editability to prevent editors from modifying the field's value. | | **`disabled`** | Controls where the field is disabled in the [Admin Panel](/docs/v4/admin/overview.md). Pass `true` to disable everywhere, or an object `{ field?, column?, filter?, groupBy?, bulkEdit? }` for granular per-area control. [More details](#disabled). | | **`hidden`** | Will transform the field into a `hidden` input type. Its value will still submit with requests in the Admin Panel, but the field itself will not be visible to editors. | ### Disabled The `admin.disabled` property controls where a field is disabled in the Admin Panel. It accepts either a boolean or an object. #### Disable everywhere (boolean) Pass `true` to disable the field across **all** Admin Panel surfaces — the edit form, list column selector, list filter dropdown, groupBy selector, and bulk edit modal: ```ts fields: [ { name: 'internalRef', type: 'text', admin: { disabled: true }, // hidden from every admin surface }, ] ``` Pass `false` (or omit the property) to enable the field everywhere. #### Disable per area (object) Pass an object to disable the field in specific areas only. Any key not set defaults to enabled: | Key | Disables | | ---------- | --------------------------- | | `field` | Edit view form rendering. | | `column` | List view column selector. | | `filter` | List view filter dropdown. | | `groupBy` | List view groupBy selector. | | `bulkEdit` | Bulk edit modal. | ```ts fields: [ { name: 'internalRef', type: 'text', admin: { disabled: { column: true, filter: true } }, }, ] ``` UI fields default to `disabled: { bulkEdit: true }` so they are excluded from bulk edit unless explicitly enabled. ### Field Descriptions Field Descriptions are used to provide additional information to the editor about a field, such as special instructions. Their placement varies from field to field, but typically are displayed with subtle style differences beneath the field inputs. A description can be configured in three ways: - As a string. - As a function which returns a string. [More details](#description-functions). - As a React component. [More details](#description). To add a Custom Description to a field, use the `admin.description` property in your Field Config: ```ts import type { CollectionConfig } from 'payload' export const MyCollectionConfig: CollectionConfig = { // ... fields: [ // ... { name: 'myField', type: 'text', admin: { description: 'Hello, world!', // highlight-line }, }, ], } ``` > **Reminder:** To replace the Field Description with a [Custom > Component](/docs/v4/custom-components/overview.md), use the > `admin.components.Description` property. [More details](#description). #### Description Functions Custom Descriptions can also be defined as a function. Description Functions are executed on the server and can be used to format simple descriptions based on the user's current [Locale](/docs/v4/configuration/localization.md). To add a Description Function to a field, set the `admin.description` property to a _function_ in your Field Config: ```ts import type { CollectionConfig } from 'payload' export const MyCollectionConfig: CollectionConfig = { // ... fields: [ // ... { name: 'myField', type: 'text', admin: { description: ({ t }) => `${t('Hello, world!')}`, // highlight-line }, }, ], } ``` All Description Functions receive the following arguments: | Argument | Description | | -------- | ------------------------------------------------------------------------------------------------ | | **`t`** | The `t` function used to internationalize the Admin Panel. [More details](/docs/v4/configuration/i18n.md) | > **Note:** If you need to subscribe to live updates within your form, use a > Description Component instead. [More details](#description). ### Conditional Logic You can show and hide fields based on what other fields are doing by utilizing conditional logic on a field by field basis. The `condition` property on a field's admin config accepts a function which takes the following arguments: | Argument | Description | | ----------------- | -------------------------------------------------------------------------------- | | **`data`** | The entire document's data that is currently being edited. | | **`siblingData`** | Only the fields that are direct siblings to the field with the condition. | | **`ctx`** | An object containing additional information about the field’s location and user. | The `ctx` object: | Property | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`blockData`** | The nearest parent block's data. If the field is not inside a block, this will be `undefined`. | | **`operation`** | A string relating to which operation the field type is currently executing within. | | **`path`** | The full path to the field in the schema, represented as an array of string segments, including array indexes. I.e `['group', 'myArray', '1', 'textField']`. | | **`user`** | The currently authenticated user object. | The `condition` function should return a boolean that will control if the field should be displayed or not. **Example:** ```ts { fields: [ { name: 'enableGreeting', type: 'checkbox', defaultValue: false, }, { name: 'greeting', type: 'text', admin: { // highlight-start condition: (data, siblingData, { blockData, path, user }) => { if (data.enableGreeting) { return true } else { return false } }, // highlight-end }, }, ] } ``` ### Custom Components Within the [Admin Panel](/docs/v4/admin/overview.md), fields are represented in three distinct places: - [Field](#field) - The actual form field rendered in the Edit View. - [Cell](#cell) - The table cell component rendered in the List View. - [Filter](#filter) - The filter component rendered in the List View. - [Diff](#diff) - The Diff component rendered in the Version Diff View To swap in Field Components with your own, use the `admin.components` property in your Field Config: ```ts import type { CollectionConfig } from 'payload' export const CollectionConfig: CollectionConfig = { // ... fields: [ // ... { // ... admin: { components: { // highlight-line // ... }, }, }, ], } ``` The following options are available: | Component | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`Field`** | The form field rendered of the Edit View. [More details](#field). | | **`Cell`** | The table cell rendered of the List View. [More details](#cell). | | **`Filter`** | The filter component rendered in the List View. [More details](#filter). | | **`Label`** | Override the default Label of the Field Component. [More details](#label). | | **`Error`** | Override the default Error of the Field Component. [More details](#error). | | **`Diff`** | Override the default Diff component rendered in the Version Diff View. [More details](#diff). | | **`Description`** | Override the default Description of the Field Component. [More details](#description). | | **`beforeInput`** | An array of elements that will be added before the input of the Field Component. [More details](#afterinput-and-beforeinput). | | **`afterInput`** | An array of elements that will be added after the input of the Field Component. [More details](#afterinput-and-beforeinput). | #### Field The Field Component is the actual form field rendered in the Edit View. This is the input that user's will interact with when editing a document. To swap in your own Field Component, use the `admin.components.Field` property in your Field Config: ```ts import type { CollectionConfig } from 'payload' export const CollectionConfig: CollectionConfig = { // ... fields: [ // ... { // ... admin: { components: { Field: '/path/to/MyFieldComponent', // highlight-line }, }, }, ], } ``` _For details on how to build Custom Components, see [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components)._ > **Warning** > > Instead of replacing the entire Field Component, you can alternately replace > or slot-in only specific parts by using the [`Label`](#label), > [`Error`](#error), [`beforeInput`](#afterinput-and-beforeinput), and > [`afterInput`](#afterinput-and-beforeinput) properties. ##### Default Props All Field Components receive the following props by default: | Property | Description | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`docPreferences`** | An object that contains the [Preferences](/docs/v4/admin/preferences.md) for the document. | | **`field`** | In Client Components, this is the sanitized Client Field Config. In Server Components, this is the original Field Config. Server Components will also receive the sanitized field config through the`clientField` prop (see below). | | **`locale`** | The locale of the field. [More details](/docs/v4/configuration/localization.md). | | **`readOnly`** | A boolean value that represents if the field is read-only or not. | | **`user`** | The currently authenticated user. [More details](/docs/v4/authentication/overview.md). | | **`validate`** | A function that can be used to validate the field. | | **`path`** | A string representing the direct, dynamic path to the field at runtime, i.e. `myGroup.myArray.0.myField`. | | **`schemaPath`** | A string representing the direct, static path to the Field Config, i.e. `posts.myGroup.myArray.myField`. | | **`indexPath`** | A hyphen-notated string representing the path to the field _within the nearest named ancestor field_, i.e. `0-0` | In addition to the above props, all Server Components will also receive the following props: | Property | Description | | ----------------- | ----------------------------------------------------------------------------- | | **`clientField`** | The serializable Client Field Config. | | **`field`** | The Field Config. | | **`data`** | The current document being edited. | | **`i18n`** | The [i18n](/docs/v4/configuration/i18n.md) object. | | **`payload`** | The [Payload](/docs/v4/local-api/overview.md) class. | | **`permissions`** | The field permissions based on the currently authenticated user. | | **`siblingData`** | The data of the field's siblings. | | **`user`** | The currently authenticated user. [More details](/docs/v4/authentication/overview.md). | | **`value`** | The value of the field at render-time. | ##### Sending and receiving values from the form When swapping out the `Field` component, you are responsible for sending and receiving the field's `value` from the form itself. To do so, import the [`useField`](/docs/v4/admin/react-hooks.md#usefield) hook from `@payloadcms/ui` and use it to manage the field's value: ```tsx 'use client' import { useField } from '@payloadcms/ui' export const CustomTextField: React.FC = () => { const { value, setValue } = useField() // highlight-line return setValue(e.target.value)} value={value} /> } ``` > **Success** > > For a complete list of all available React hooks, see the [Payload React > Hooks](/docs/v4/admin/react-hooks.md) documentation. For additional help, see [Building > Custom Components](/docs/v4/custom-components/overview.md#building-custom-components). ##### TypeScript#field-component-types When building Custom Field Components, import the field props to ensure type safety in your component. There is an explicit props type for every Field Type and server/client environment. The convention is to prepend the field type onto the target type, i.e. `TextFieldClientProps`: ```tsx import type { TextFieldClientProps, TextFieldServerProps, // ...and so on for each Field Type } from 'payload' ``` See each individual Field Type for exact type imports. #### Cell The Cell Component is rendered in the table of the List View. It represents the value of the field when displayed in a table cell. To swap in your own Cell Component, use the `admin.components.Cell` property in your Field Config: ```ts import type { Field } from 'payload' export const myField: Field = { name: 'myField', type: 'text', admin: { components: { Cell: '/path/to/MyCustomCellComponent', // highlight-line }, }, } ``` #### Example: Custom Cell Component ```ts 'use client' import type { DefaultCellComponentProps } from 'payload' export const MyCustomCellComponent: React.FC = ({ cellData, field, rowData, }) => { const price = cellData as number const currency = rowData.currency || 'USD' return ( {new Intl.NumberFormat('en-US', { style: 'currency', currency, }).format(price)} ) } ``` > **Important:** Use `DefaultCellComponentProps` for client components (with > `'use client'` directive) and `DefaultServerCellComponentProps` for server > components. Omitting `'use client'` will change the expected prop types. All Cell Components receive the following props: | Property | Description | | --------------------- | ---------------------------------------------------------------------------- | | **`cellData`** | The data value for this specific cell. | | **`collectionSlug`** | The slug of the collection being rendered. | | **`field`** | The field configuration object. | | **`rowData`** | An object containing all data for the current row. | | **`className`** | Optional CSS class name for styling the cell. | | **`columnIndex`** | Optional index number of the column. | | **`customCellProps`** | Optional additional custom props passed to the cell. | | **`link`** | Optional boolean representing whether this cell should be wrapped in a link. | | **`linkURL`** | Optional URL string for the link. | | **`onClick`** | Optional function called when the cell is clicked. | | **`viewType`** | Optional string representing the type of view being rendered. | > **Note:** Cell components receive a different set of props than Field > components, optimized specifically for rendering data in the List View. They > do not receive the standard Field component props like `docPreferences`, > `validate`, or `path`. For details on how to build Custom Components themselves, see [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components). #### Filter The Filter Component is the actual input element rendered within the "Filter By" dropdown of the List View used to represent this field when building filters. All Custom Filter Components receive the following props: | Property | Description | | -------------- | -------------------------------------------------- | | **`disabled`** | Boolean indicating whether the filter is disabled. | | **`onChange`** | Function to call when the filter value changes. | | **`operator`** | The comparison operator being used for the filter. | | **`value`** | The current value of the filter. | To swap in your own Filter Component, use the `admin.components.Filter` property in your Field Config: ```ts import type { Field } from 'payload' export const myField: Field = { name: 'myField', type: 'text', admin: { components: { Filter: '/path/to/MyCustomFilterComponent', // highlight-line }, }, } ``` All Custom Filter Components receive the same [Default Field Component Props](#field). For details on how to build Custom Components themselves, see [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components). #### Label The Label Component is rendered anywhere a field needs to be represented by a label. This is typically used in the Edit View, but can also be used in the List View and elsewhere. To swap in your own Label Component, use the `admin.components.Label` property in your Field Config: ```ts import type { Field } from 'payload' export const myField: Field = { name: 'myField', type: 'text', admin: { components: { Label: '/path/to/MyCustomLabelComponent', // highlight-line }, }, } ``` All Custom Label Components receive the same [Default Field Component Props](#field). For details on how to build Custom Components themselves, see [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components). ##### TypeScript#label-component-types When building Custom Label Components, import the matching props type for the field and environment. The convention is to append `LabelServerProps` or `LabelClientProps` to the field type, i.e. `TextFieldLabelClientProps`. ```tsx import type { TextFieldLabelServerProps, TextFieldLabelClientProps, // ...and so on for each Field Type } from 'payload' ``` #### Description Alternatively to the [Description Property](#field-descriptions), you can also use a [Custom Component](/docs/v4/custom-components/overview.md) as the Field Description. This can be useful when you need to provide more complex feedback to the user, such as rendering dynamic field values or other interactive elements. To add a Description Component to a field, use the `admin.components.Description` property in your Field Config: ```ts import type { CollectionConfig } from 'payload' export const MyCollectionConfig: CollectionConfig = { // ... fields: [ // ... { name: 'myField', type: 'text', admin: { components: { Description: '/path/to/MyCustomDescriptionComponent', // highlight-line }, }, }, ], } ``` All Custom Description Components receive the same [Default Field Component Props](#field). For details on how to build a Custom Components themselves, see [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components). ##### TypeScript#description-component-types When building Custom Description Components, import the matching props type for the field and environment. The convention is to append `DescriptionServerProps` or `DescriptionClientProps` to the field type, i.e. `TextFieldDescriptionClientProps`. ```tsx import type { TextFieldDescriptionServerProps, TextFieldDescriptionClientProps, // And so on for each Field Type } from 'payload' ``` #### Error The Error Component is rendered when a field fails validation. It is typically displayed beneath the field input in a visually-compelling style. To swap in your own Error Component, use the `admin.components.Error` property in your Field Config: ```ts import type { Field } from 'payload' export const myField: Field = { name: 'myField', type: 'text', admin: { components: { Error: '/path/to/MyCustomErrorComponent', // highlight-line }, }, } ``` All Error Components receive the [Default Field Component Props](#field). For details on how to build Custom Components themselves, see [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components). ##### TypeScript#error-component-types When building Custom Error Components, import the matching props type for the field and environment. The convention is to append `ErrorServerProps` or `ErrorClientProps` to the field type, i.e. `TextFieldErrorClientProps`. ```tsx import type { TextFieldErrorServerProps, TextFieldErrorClientProps, // And so on for each Field Type } from 'payload' ``` #### Diff The Diff Component is rendered in the Version Diff view. It will only be visible in entities with versioning enabled, To swap in your own Diff Component, use the `admin.components.Diff` property in your Field Config: ```ts import type { Field } from 'payload' export const myField: Field = { name: 'myField', type: 'text', admin: { components: { Diff: '/path/to/MyCustomDiffComponent', // highlight-line }, }, } ``` All Error Components receive the [Default Field Component Props](#field). For details on how to build Custom Components themselves, see [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components). ##### TypeScript#diff-component-types When building Custom Diff Components, import the matching props type for the field and environment. The convention is to append `DiffServerProps` or `DiffClientProps` to the field type, i.e. `TextFieldDiffClientProps`. ```tsx import type { TextFieldDiffServerProps, TextFieldDiffClientProps, // And so on for each Field Type } from 'payload' ``` #### afterInput and beforeInput With these properties you can add multiple components _before_ and _after_ the input element, as their name suggests. This is useful when you need to render additional elements alongside the field without replacing the entire field component. To add components before and after the input element, use the `admin.components.beforeInput` and `admin.components.afterInput` properties in your Field Config: ```ts import type { CollectionConfig } from 'payload' export const MyCollectionConfig: CollectionConfig = { // ... fields: [ // ... { name: 'myField', type: 'text', admin: { components: { // highlight-start beforeInput: ['/path/to/MyCustomComponent'], afterInput: ['/path/to/MyOtherCustomComponent'], // highlight-end }, }, }, ], } ``` All `afterInput` and `beforeInput` Components receive the same [Default Field Component Props](#field). For details on how to build Custom Components, see [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components). ## TypeScript You can import the Payload `Field` type as well as other common types from the `payload` package. [More details](/docs/v4/typescript/overview.md). ```ts import type { Field } from 'payload' ``` # Fields Generated by Payload Source: https://payloadcms.com/docs/beta/fields/default-fields Payload automatically adds fields to collections and globals based on their configuration. For example, collections receive an `id` field, auth-enabled collections receive fields such as `email`, and upload-enabled collections receive fields such as `filename`. You can override the configuration of supported generated fields by defining a top-level field with the same `name` in your collection or global `fields` array. Payload merges auth, upload, and version field overrides into its default field configuration, preserving properties that you do not override, including hooks. Custom ID and timestamp fields replace the generated field instead. > **Warning** > > Only the fields listed on this page support overrides. Other internal fields > are reserved for Payload and should not be added to your fields array. ## Example The following auth collection further restricts writes to Payload's default `apiKey` field. Field overrides replace matching access functions, so the custom functions retain Payload's default admin access boundary before applying an application role: ```ts import type { CollectionConfig, FieldAccess } from 'payload' const canCreateOrUpdateAPIKeys: FieldAccess = async ({ req }) => { const adminSlug = req.payload.config.admin.user if (req.user?.collection !== adminSlug) { return false } const adminAccess = req.payload.collections[adminSlug]?.config.access.admin if (adminAccess && !(await adminAccess({ req, slug: adminSlug }))) { return false } return req.user.roles?.includes('api-key-admin') === true } export const Users: CollectionConfig = { slug: 'users', auth: { useAPIKey: true, }, fields: [ { name: 'apiKey', // highlight-line type: 'text', access: { create: canCreateOrUpdateAPIKeys, update: canCreateOrUpdateAPIKeys, }, }, ], } ``` ## Document fields The following fields are available on collections and globals without enabling authentication, uploads, or versions: | Field | Used by | Override behavior | | ----------- | ------------------------------------------- | --------------------------------------------------------------------------- | | `id` | Collections | Replaces the generated ID field. See [Custom ID fields](#custom-id-fields). | | `createdAt` | Collections with timestamps and all globals | Replaces the generated field. Keep this as a `date` field. | | `updatedAt` | Collections with timestamps and all globals | Replaces the generated field. Keep this as a `date` field. | | `deletedAt` | Collections with `trash` enabled | Replaces the generated field. Keep this as a `date` field. | ### Custom ID fields All [Collections](/docs/v4/configuration/collections.md) automatically generate their own ID field. If needed, you can override this behavior by providing an explicit ID field to your config. This field should either be required or have a hook to generate the ID dynamically. To define a custom ID field, add a top-level field with the `name` property set to `id`: ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... fields: [ { name: 'id', // highlight-line required: true, type: 'number', }, ], } ``` > **Warning** > > Custom ID fields can only be [`Number`](/docs/v4/fields/number.md) or [`Text`](/docs/v4/fields/text.md) fields. > Custom text IDs must not contain `/` or `.` characters. ## Authentication fields Authentication fields are added to a collection according to its [auth configuration](/docs/v4/authentication/overview.md). Payload deep-merges matching fields from your collection's `fields` array into these defaults, except for the special `password` override described below. | Fields | Added when | | -------------------------------------------------------------------- | --------------------------------------------- | | `email`, `password`, `resetPasswordToken`, `resetPasswordExpiration` | The local authentication strategy is enabled. | | `username` | `auth.loginWithUsername` is enabled. | | `apiKey`, `apiKeyLast4`, `apiKeyIndex` | `auth.useAPIKey` is enabled. | | `_verified`, `_verificationToken` | `auth.verify` is enabled. | | `loginAttempts`, `lockUntil` | `auth.maxLoginAttempts` is greater than `0`. | | `sessions` | `auth.useSessions` is enabled. | The `password` override is also applied to the generated new-password and confirm-password fields in the Admin Panel. You may need to set `hidden: true` on the override to prevent an additional field from being displayed. The `salt` and `hash` fields are reserved and cannot be overridden. ## Upload fields The following fields are added to [upload-enabled collections](/docs/v4/upload/overview.md). Payload deep-merges matching fields from your collection's `fields` array into these defaults. | Fields | Added when | | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `filename`, `mimeType`, `filesize`, `width`, `height`, `url`, `thumbnailURL` | Uploads are enabled. | | `focalX`, `focalY` | Uploads are enabled, unless `focalPoint` is `false` and no Sharp `variants` or image adjustment options (`resizeOptions`, `formatOptions`, and so on) are configured. | | `variants` | Sharp `variants` are configured. Matching fields nested under `variants` are also merged recursively. | The `file` field is reserved and cannot be overridden. ## Version fields The `_status` field is added to collections and globals when [drafts](/docs/v4/versions/drafts.md) are enabled. Define a top-level `_status` field to merge custom configuration into Payload's default field. # Array Field Source: https://payloadcms.com/docs/beta/fields/array The Array Field is used when you need to have a set of "repeating" [Fields](/docs/v4/fields/overview.md). It stores an array of objects containing fields that you define. These fields can be of any type, including other arrays, to achieve infinitely nested data structures. Arrays are useful for many different types of content from simple to complex, such as: - A "slider" with an image ([upload field](/docs/v4/fields/upload.md)) and a caption ([text field](/docs/v4/fields/text.md)) - Navigational structures where editors can specify nav items containing pages ([relationship field](/docs/v4/fields/relationship.md)), an "open in new tab" [checkbox field](/docs/v4/fields/checkbox.md) - Event agenda "timeslots" where you need to specify start & end time ([date field](/docs/v4/fields/date.md)), label ([text field](/docs/v4/fields/text.md)), and Learn More page [relationship](/docs/v4/fields/relationship.md) ![Array field with two Rows in Payload Admin Panel](https://payloadcms.com/images/docs/fields/array.png) _Admin Panel screenshot of an Array field with two Rows_ To create an Array Field, set the `type` to `array` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyArrayField: Field = { // ... // highlight-start type: 'array', fields: [ // ... ], // highlight-end } ``` ## Config Options | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as the heading in the [Admin Panel](/docs/v4/admin/overview.md) or an object with keys for each language. Auto-generated from name if not defined. | | **`fields`** \* | Array of field types to correspond to each row of the Array. | | **`validate`** | Provide a custom validation function that will be executed on both the [Admin Panel](/docs/v4/admin/overview.md) and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`minRows`** | A number for the fewest allowed items during validation when a value is present. | | **`maxRows`** | A number for the most allowed items during validation when a value is present. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide an array of row data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. If enabled, a separate, localized set of all data within this Array will be kept, so there is no need to specify each nested field as `localized`. | | **`required`** | Require this field to have a value. | | **`labels`** | Customize the row labels appearing in the Admin dashboard. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/v4/typescript/generating-types.md#custom-field-interfaces) & [GraphQL type](/docs/v4/graphql/graphql-schema.md#custom-field-schemas). | | **`dbName`** | Custom table name for the field when using SQL Database Adapter ([Postgres](/docs/v4/database/postgres.md)). Auto-generated from name if not defined. | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ > **Warning** > > Setting `unique: true` on a field **inside** an Array creates a > collection-wide database unique index — not a per-document one. No two > documents in the collection can share the same value at that nested path, and > on MongoDB, documents without the array will collide on `null`. To enforce > uniqueness within a single document's rows, use a custom > [`validate`](/docs/v4/fields/overview.md#validation) function on this Array field. > [More details](/docs/v4/database/indexes.md#unique-fields). ## Admin Options To customize the appearance and behavior of the Array Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyArrayField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Array Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Option | Description | | ------------------------- | ----------------------------------------------------------------------------------- | | **`initCollapsed`** | Set the initial collapsed state | | **`components.RowLabel`** | React component to be rendered as the label on the array row. [Example](#row-label) | | **`isSortable`** | Disable order sorting by setting this value to `false` | ## Example In this example, we have an Array Field called `slider` that contains a set of fields for a simple image slider. Each row in the array has a `title`, `image`, and `caption`. We also customize the row label to display the title if it exists, or a default label if it doesn't. ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'slider', // required type: 'array', // required label: 'Image Slider', minRows: 2, maxRows: 10, interfaceName: 'CardSlider', // optional labels: { singular: 'Slide', plural: 'Slides', }, fields: [ // required { name: 'title', type: 'text', }, { name: 'image', type: 'upload', relationTo: 'media', required: true, }, { name: 'caption', type: 'text', }, ], }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { ArrayField } from '@payloadcms/ui' import type { ArrayFieldServerProps } from 'payload' export const CustomArrayFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { ArrayField } from '@payloadcms/ui' import type { ArrayFieldClientProps } from 'payload' export const CustomArrayFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { ArrayFieldLabelServerProps } from 'payload' export const CustomArrayFieldLabelServer: React.FC< ArrayFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import type { ArrayFieldLabelClientProps } from 'payload' import { FieldLabel } from '@payloadcms/ui' import React from 'react' export const CustomArrayFieldLabelClient: React.FC< ArrayFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` ### Row Label ```tsx 'use client' import { useRowLabel } from '@payloadcms/ui' export const ArrayRowLabel = () => { const { data, rowNumber } = useRowLabel<{ title?: string }>() const customLabel = `${data.title || 'Slide'} ${String(rowNumber).padStart(2, '0')} ` return
Custom Label: {customLabel}
} ``` # Blocks Field Source: https://payloadcms.com/docs/beta/fields/blocks The Blocks Field is one of the most flexible tools in Payload. It stores an array of objects, where each object is a “block” with its own schema. Unlike a simple array (where every item looks the same), blocks let you mix and match different content types in any order. This makes Blocks perfect for building dynamic, editor-friendly experiences, such as: - A page builder with blocks like `Quote`, `CallToAction`, `Slider`, or `Gallery`. - A form builder with block types like `Text`, `Select`, or `Checkbox`. - An event agenda where each timeslot could be a `Break`, `Presentation`, or `BreakoutSession`. ![Admin Panel screenshot of add Blocks drawer view](https://payloadcms.com/images/docs/fields/blocks.png) _Admin Panel screenshot of add Blocks drawer view_ To add a Blocks Field, set the `type` to `blocks` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyBlocksField: Field = { // ... // highlight-start type: 'blocks', blocks: [ // ... ], // highlight-end } ``` This page is divided into two parts: first, the settings of the Blocks Field, and then the settings of the blocks inside it. ## Block Field ### Block Config Options | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as the heading in the Admin Panel or an object with keys for each language. Auto-generated from name if not defined. | | **`blocks`** \* | Array of [block configs](/docs/v4/fields/blocks.md#config-options) to be made available to this field. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`minRows`** | A number for the fewest allowed items during validation when a value is present. | | **`maxRows`** | A number for the most allowed items during validation when a value is present. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API response or the Admin Panel. | | **`defaultValue`** | Provide an array of block data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. If enabled, a separate, localized set of all data within this field will be kept, so there is no need to specify each nested field as `localized`. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`labels`** | Customize the block row labels appearing in the Admin dashboard. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ > **Warning** > > Setting `unique: true` on a field **inside** a Block creates a collection-wide > database unique index — not a per-document one. No two documents in the > collection can share the same value at that nested path, and on MongoDB, > documents without the block will collide on `null`. To enforce uniqueness > within a single document's blocks, use a custom > [`validate`](/docs/v4/fields/overview.md#validation) function on this Blocks field. > [More details](/docs/v4/database/indexes.md#unique-fields). ### Block Admin Options To customize the appearance and behavior of the Blocks Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyBlocksField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Blocks Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Option | Description | | ------------------- | ------------------------------------------------------ | | **`initCollapsed`** | Set the initial collapsed state | | **`isSortable`** | Disable order sorting by setting this value to `false` | #### Customizing the way your block is rendered in Lexical If you're using this block within the [Lexical editor](/docs/v4/rich-text/overview.md), you can also customize how the block is rendered in the Lexical editor itself by specifying custom components. - `admin.components.Label` - pass a custom React component here to customize the way that the label is rendered for this block - `admin.components.Block` - pass a component here to completely override the way the block is rendered in Lexical with your own component This is super handy if you'd like to present your editors with a very deliberate and nicely designed block "preview" right in your rich text. For example, if you have a `gallery` block, you might want to actually render the gallery of images directly in your Lexical block. With the `admin.components.Block` property, you can do exactly that! > **Tip:** If you customize the way your block is rendered in Lexical, you can > import utility components to easily edit / remove your block - so that you > don't have to build all of this yourself. To import these utility components for one of your custom blocks, you can import the following: ```ts import { // Edit block buttons (choose the one that corresponds to your usage) // When clicked, this will open a drawer with your block's fields // so your editors can edit them InlineBlockEditButton, BlockEditButton, // Buttons that will remove this block from Lexical // (choose the one that corresponds to your usage) InlineBlockRemoveButton, BlockRemoveButton, // The label that should be rendered for an inline block InlineBlockLabel, // The default "container" that is rendered for an inline block // if you want to re-use it InlineBlockContainer, // The default "collapsible" UI that is rendered for a regular block // if you want to re-use it BlockCollapsible, } from '@payloadcms/richtext-lexical/client' ``` ## Copying and Pasting Blocks The Admin Panel includes built-in copy and paste support for both individual block rows and entire Blocks fields. This is available through the row action menu (the `...` button on each block) and from the field-level action menu in the field header. ### Row-level vs. Field-level - **Row copy/paste** — copies a single block and lets you paste it into any position within any compatible Blocks field - **Field copy/paste** — copies all blocks in the field at once and pastes them as the full contents of a target field, replacing whatever was there ### How it differs from Duplicate The **Duplicate** action creates an immediate in-place copy of a block, inserted directly below the original — always within the same field on the same document. **Copy/paste** is designed for moving data across fields or documents: - Copy a block on `Page A`, open `Page B`, and paste it into that page's blocks field - Copy a block from one Blocks field and paste it into a different Blocks field on the same document ### How it works Copied data is written to `localStorage` under the key `_payloadClipboard`. This means: - The clipboard persists across browser tabs (same origin) - It is not shared between different Payload origins When pasting, Payload validates that the copied block type exists in the target field's `blocks` config. If the schemas are incompatible, the paste action will not be available. All block and nested array IDs are automatically regenerated on paste to prevent duplicate key errors. > **Warning** > > **"Paste Fields" replaces all blocks, even when a single row is on the clipboard.** > > If you have copied a single block row and then use **Paste Fields** (the field-level paste action in the field header), all existing blocks in the target field will be replaced with just that one copied row. > > To add a copied row to a field that already has content, use **Paste Row** instead: add a new empty block first, then use the row-level **Paste Row** action on that new row to replace it with the copied data. ## Blocks Items ### Config Options Blocks are defined as separate configs of their own. > **Tip:** Best practice is to define each block config in its own file, and > then import them into your Blocks field as necessary. This way each block > config can be easily shared between fields. For instance, using the "layout > builder" example, you might want to feature a few of the same blocks in a Post > collection as well as a Page collection. Abstracting into their own files > trivializes their reusability. | Option | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`slug`** \* | Identifier for this block type. Will be saved on each block as the `blockType` property. | | **`fields`** \* | Array of fields to be stored in this block. | | **`labels`** | Customize the block labels that appear in the Admin dashboard. Auto-generated from slug if not defined. Alternatively you can use `admin.components.Label` for greater control. | | **`interfaceName`** | Override the name of the auto-generated top-level [Typescript interface](/docs/v4/typescript/generating-types.md#custom-field-interfaces) & [GraphQL type](/docs/v4/graphql/graphql-schema.md#custom-field-schemas). Blocks always get an interface — defaults to a PascalCase form of the slug. | | **`graphQL.singularName`** | Text to use for the GraphQL schema name. Auto-generated from slug if not defined. NOTE: this is set for deprecation, prefer `interfaceName`. | | **`dbName`** | Custom table name for this block type when using SQL Database Adapter ([Postgres](/docs/v4/database/postgres.md)). Auto-generated from slug if not defined. | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | _\* An asterisk denotes that a property is required._ ### Admin Options Blocks are not fields, so they don’t inherit the base properties shared by all fields (not to be confused with the Blocks Field, documented above, which does). Here are their available admin options: | Option | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | | **`components.Block`** | Custom component for replacing the Block, including the header. | | **`components.Label`** | Custom component for replacing the Block Label. | | **`disableBlockName`** | Hide the blockName field by setting this value to `true`. | | **`group`** | Text or localization object used to group this Block in the Blocks Drawer. | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`images`** | Custom images for the block in different UI contexts. Supports `icon` (20x20px for Lexical menus/toolbars) and `thumbnail` (3:2 ratio for block selection drawer). Each can be a URL string or `{ url, alt }` object. [More details](#block-image-guidelines). | | ### Block Image Guidelines The `images` property lets you provide custom images for each display context: - **`images.icon`** - Small icon for Lexical editor menus and toolbars (displayed at 20x20px) - **`images.thumbnail`** - Larger thumbnail for block selection drawer (3:2 aspect ratio) Each can be either a plain URL string or an object `{ url: string, alt?: string }`. #### Icon (`images.icon`) Used in Lexical editor slash menus, toolbar dropdowns, and inline block menus. **Requirements**: - Displayed at **20x20 pixels** - **Square images** (1:1 aspect ratio) work best - Falls back to `images.thumbnail`, then to default block icon **Recommendations**: - Use SVG for crisp rendering at small sizes - Keep designs simple with bold shapes - Provide web-optimized images (SVG, PNG with transparency) #### Thumbnail (`images.thumbnail`) Used in the block selection drawer when editors add blocks to a blocks field. **Requirements**: - **3:2 aspect ratio** (e.g., 480x320, 600x400, 900x600) - Images are scaled using `object-fit: cover` - Non-matching aspect ratios will be **cropped** to fill the container **Recommendations**: - Use images with **3:2 aspect ratio** to avoid unwanted cropping - Keep important visual content **centered** in your image - Provide web-optimized images (JPEG, PNG, WebP) for faster loading #### Usage Examples **Example 1: Using both icon and thumbnail with alt text** ```ts const QuoteBlock: Block = { slug: 'quote', admin: { images: { icon: { url: 'https://example.com/icons/quote-icon-20x20.svg', alt: 'Quote icon', }, thumbnail: { url: 'https://example.com/thumbnails/quote-block-480x320.jpg', alt: 'Quote block', }, }, }, fields: [ { name: 'quoteText', type: 'text', required: true, }, ], } ``` **Example 2: Using URL strings (no alt text)** ```ts const CallToActionBlock: Block = { slug: 'cta', admin: { images: { icon: 'https://example.com/icons/cta-20x20.svg', thumbnail: 'https://example.com/thumbnails/cta-block-480x320.jpg', }, }, fields: [ { name: 'buttonText', type: 'text', }, ], } ``` **Example 3: Using only icon** ```ts const DividerBlock: Block = { slug: 'divider', admin: { images: { icon: 'https://example.com/icons/divider-20x20.svg', }, }, fields: [], } ``` If no `images` is provided, default graphics are displayed automatically in both contexts. ### blockType, blockName, and block.label Each block stores two pieces of data alongside your fields. The `blockType` identifies which schema to use and it is exactly the block’s `slug`. The `blockName` is an optional label you can give to a block to make editing and scanning easier. The **label** is shared by all blocks of the same type and is defined in the block config via `label` with a fallback to `slug`. On the other hand, the **blockName** is specific to each block individually. You can hide the editable name with `admin.disableBlockName`. If you provide `admin.components.Label`, that component replaces both the name and the label in the Admin UI. | Property | Scope | Source | Visible in UI | Notes | | ------------- | ---------- | ------------------------------------------ | ------------- | -------------------------------------------------------------------------------------------------- | | `blockType` | Each block | The block’s `slug` | Not a header | Used to resolve which block schema to render | | `blockName` | Each block | Editor input in the Admin | Yes | Optional label; hide with `admin.disableBlockName` or replace with custom `admin.components.Label` | | `block.label` | Block type | `label` in block config or `slug` fallback | Yes | Shared by all blocks of that type. Can be replaced with custom `admin.components.Label` | ### Custom Components #### Field ##### Server Component ```tsx import type React from 'react' import { BlocksField } from '@payloadcms/ui' import type { BlocksFieldServerProps } from 'payload' export const CustomBlocksFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` ##### Client Component ```tsx 'use client' import React from 'react' import { BlocksField } from '@payloadcms/ui' import type { BlocksFieldClientProps } from 'payload' export const CustomBlocksFieldClient: React.FC = ( props, ) => { return } ``` #### Label ##### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { BlocksFieldLabelServerProps } from 'payload' export const CustomBlocksFieldLabelServer: React.FC< BlocksFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` ##### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { BlocksFieldLabelClientProps } from 'payload' export const CustomBlocksFieldLabelClient: React.FC< BlocksFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` ## Example `collections/ExampleCollection.js` ```ts import { Block, CollectionConfig } from 'payload' const QuoteBlock: Block = { slug: 'Quote', // required imageURL: 'https://google.com/path/to/image.jpg', imageAltText: 'A nice thumbnail image to show what this block looks like', interfaceName: 'QuoteBlock', // optional fields: [ // required { name: 'quoteHeader', type: 'text', required: true, }, { name: 'quoteText', type: 'text', }, ], } export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'layout', // required type: 'blocks', // required minRows: 1, maxRows: 20, blocks: [ // required QuoteBlock, ], }, ], } ``` ## Block References If you have multiple blocks used in multiple places, your Payload Config can grow in size, potentially sending more data to the client and requiring more processing on the server. However, you can optimize performance by defining each block **once** in your Payload Config and then referencing its slug wherever it's used instead of passing the entire block config. To do this, define the block in the `blocks` array of the Payload Config. Then, in the Blocks Field, pass the block slug to the field's `blocks` array. ```ts import { buildConfig } from 'payload' import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical' // Payload Config const config = buildConfig({ // Define the block once blocks: [ { slug: 'TextBlock', fields: [ { name: 'text', type: 'text', }, ], }, ], collections: [ { slug: 'collection1', fields: [ { name: 'content', type: 'blocks', // Reference the block by slug blocks: ['TextBlock'], }, ], }, { slug: 'collection2', fields: [ { name: 'editor', type: 'richText', editor: lexicalEditor({ features: [ BlocksFeature({ // Same reference can be reused anywhere, even in the lexical editor, without incurred performance hit blocks: ['TextBlock'], }), ], }), }, ], }, ], }) ``` > **Reminder:** > Blocks referenced by slug are treated as isolated from the collection / global config. This has the following implications: > > 1. The block config cannot be modified or extended in the collection config. It will be identical everywhere it's referenced. > 2. Access control for blocks referenced by slug is run only once - data from the collection will not be available in the block's access control. ## TypeScript As you build your own Block configs, you might want to store them in separate files but retain typing accordingly. To do so, you can import and use Payload's `Block` type: ```ts import type { Block } from 'payload' ``` ## Conditional Blocks Blocks can be conditionally enabled using the `filterOptions` property on the blocks field. It allows you to provide a function that returns which block slugs should be available based on the given context. ### Behavior - `filterOptions` is re-evaluated as part of the form state request, whenever the document data changes. - If a block is present in the field but no longer allowed by `filterOptions`, a validation error will occur when saving. ### filterOptions Example ```ts { name: 'blocksWithDynamicFilterOptions', type: 'blocks', filterOptions: ({ siblingData }) => { return siblingData?.enabledBlocks?.length ? [siblingData.enabledBlocks] // allow only the matching block : true // allow all blocks if no value is set }, blocks: [ { slug: 'block1', fields: [{ type: 'text', name: 'block1Text' }] }, { slug: 'block2', fields: [{ type: 'text', name: 'block2Text' }] }, { slug: 'block3', fields: [{ type: 'text', name: 'block3Text' }] }, // ... ], } ``` In this example, the list of available blocks is determined by the enabledBlocks sibling field. If no value is set, all blocks remain available. # Checkbox Field Source: https://payloadcms.com/docs/beta/fields/checkbox The Checkbox Field saves a boolean in the database. ![Checkbox field with text field in Payload Admin Panel](https://payloadcms.com/images/docs/fields/checkbox.png) _Admin Panel screenshot of Checkbox field with Text field below_ To add a Checkbox Field, set the `type` to `checkbox` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyCheckboxField: Field = { // ... type: 'checkbox', // highlight-line } ``` ## Config Options | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value, will default to false if field is also `required`. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](/docs/v4/fields/overview.md#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Example Here is an example of a Checkbox Field in a Collection: ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'enableCoolStuff', // required type: 'checkbox', // required label: 'Click me to see fanciness', defaultValue: false, }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { CheckboxField } from '@payloadcms/ui' import type { CheckboxFieldServerProps } from 'payload' export const CustomCheckboxFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { CheckboxField } from '@payloadcms/ui' import type { CheckboxFieldClientProps } from 'payload' export const CustomCheckboxFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { CheckboxFieldLabelServerProps } from 'payload' export const CustomCheckboxFieldLabelServer: React.FC< CheckboxFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { CheckboxFieldLabelClientProps } from 'payload' export const CustomCheckboxFieldLabelClient: React.FC< CheckboxFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # Code Field Source: https://payloadcms.com/docs/beta/fields/code The Code Field saves a string in the database, but provides the [Admin Panel](/docs/v4/admin/overview.md) with a code editor styled interface. ![Shows a Code field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/code.png) _This field is using the `monaco-react` editor syntax highlighting._ To add a Code Field, set the `type` to `code` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyBlocksField: Field = { // ... type: 'code', // highlight-line } ``` ## Config Options | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. | | **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. See below for [more detail](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Code Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyCodeField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Code Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`language`** | This property can be set to any language listed [here](https://github.com/microsoft/monaco-editor/tree/main/src/basic-languages). | | **`editorOptions`** | Options that can be passed to the monaco editor, [view the full list](https://microsoft.github.io/monaco-editor/typedoc/interfaces/editor.IDiffEditorConstructionOptions.html). | ## Example `collections/ExampleCollection.ts ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'trackingCode', // required type: 'code', // required required: true, admin: { language: 'javascript', }, }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { CodeField } from '@payloadcms/ui' import type { CodeFieldServerProps } from 'payload' export const CustomCodeFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { CodeField } from '@payloadcms/ui' import type { CodeFieldClientProps } from 'payload' export const CustomCodeFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { CodeFieldLabelServerProps } from 'payload' export const CustomCodeFieldLabelServer: React.FC< CodeFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { CodeFieldLabelClientProps } from 'payload' export const CustomCodeFieldLabelClient: React.FC< CodeFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # JSON Field Source: https://payloadcms.com/docs/beta/fields/json The JSON Field saves raw JSON to the database and provides the [Admin Panel](/docs/v4/admin/overview.md) with a code editor styled interface. This is different from the [Code Field](/docs/v4/fields/code.md) which saves the value as a string in the database. ![Shows a JSON field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/json.png) _This field is using the `monaco-react` editor syntax highlighting._ To add a JSON Field, set the `type` to `json` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyJSONField: Field = { // ... type: 'json', // highlight-line } ``` ## Config Options | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`jsonSchema`** | Provide a JSON schema that will be used for validation. [JSON schemas](https://json-schema.org/learn/getting-started-step-by-step) | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the JSON Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyJSONField: Field = { // ... admin: { // highlight-line // ... }, } ``` The JSON Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Option | Description | | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`editorOptions`** | Options that can be passed to the monaco editor, [view the full list](https://microsoft.github.io/monaco-editor/docs.html#variables/editor_editor_api.editor.EditorOptions.html). | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'customerJSON', // required type: 'json', // required required: true, }, ], } ``` ## JSON Schema Validation Payload JSON fields fully support the [JSON schema](https://json-schema.org/) standard. By providing a schema in your field config, the editor will be guided in the admin UI, getting typeahead for properties and their formats automatically. When the document is saved, the default validation will prevent saving any invalid data in the field according to the schema in your config. If you only provide a URL to a schema, Payload will fetch the desired schema if it is publicly available. If not, it is recommended to add the schema directly to your config or import it from another file so that it can be implemented consistently in your project. ### Local JSON Schema `collections/ExampleCollection.ts` ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'customerJSON', // required type: 'json', // required jsonSchema: { uri: 'a://b/foo.json', // required fileMatch: ['a://b/foo.json'], // required schema: { type: 'object', properties: { foo: { enum: ['bar', 'foobar'], }, }, }, }, }, ], } // {"foo": "bar"} or {"foo": "foobar"} - ok // Attempting to create {"foo": "not-bar"} will throw an error ``` ### Remote JSON Schema `collections/ExampleCollection.ts` ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'customerJSON', // required type: 'json', // required jsonSchema: { uri: 'https://example.com/customer.schema.json', // required fileMatch: ['https://example.com/customer.schema.json'], // required }, }, ], } // If 'https://example.com/customer.schema.json' has a JSON schema // {"foo": "bar"} or {"foo": "foobar"} - ok // Attempting to create {"foo": "not-bar"} will throw an error ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { JSONField } from '@payloadcms/ui' import type { JSONFieldServerProps } from 'payload' export const CustomJSONFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { JSONField } from '@payloadcms/ui' import type { JSONFieldClientProps } from 'payload' export const CustomJSONFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { JSONFieldLabelServerProps } from 'payload' export const CustomJSONFieldLabelServer: React.FC< JSONFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { JSONFieldLabelClientProps } from 'payload' export const CustomJSONFieldLabelClient: React.FC< JSONFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # Collapsible Field Source: https://payloadcms.com/docs/beta/fields/collapsible The Collapsible Field is presentational-only and only affects the Admin Panel. By using it, you can place fields within a nice layout component that can be collapsed / expanded. ![Shows a Collapsible field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/collapsible.png) _Admin Panel screenshot of a Collapsible field_ To add a Collapsible Field, set the `type` to `collapsible` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyCollapsibleField: Field = { // ... // highlight-start type: 'collapsible', fields: [ // ... ], // highlight-end } ``` ## Config Options | Option | Description | | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`label`** \* | A label to render within the header of the collapsible component. This can be a string, function or react component. Function/components receive `({ data, path })` as args. | | **`fields`** \* | Array of field types to nest within this Collapsible. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Collapsible Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyCollapsibleField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Collapsible Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Option | Description | | ------------------- | ------------------------------- | | **`initCollapsed`** | Set the initial collapsed state | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { label: ({ data }) => data?.title || 'Untitled', type: 'collapsible', // required fields: [ // required { name: 'title', type: 'text', required: true, }, { name: 'someTextField', type: 'text', required: true, }, ], }, ], } ``` # Date Field Source: https://payloadcms.com/docs/beta/fields/date The Date Field saves a Date in the database and provides the [Admin Panel](/docs/v4/admin/overview.md) with a customizable time picker interface. ![Shows a Date field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/date.png) _This field is using the `react-datepicker` component for UI._ To add a Date Field, set the `type` to `date` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyDateField: Field = { // ... type: 'date', // highlight-line } ``` ## Config Options | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`timezone`** \* | Set to `true` to enable timezone selection on this field. [More details](#timezones). | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Date Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyDateField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Date Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Property | Description | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | | **`placeholder`** | Placeholder text for the field. | | **`date`** | Pass options to customize date field appearance. | | **`date.displayFormat`** | Format date to be shown in field **cell**. | | **`date.pickerAppearance`** \* | Determines the appearance of the datepicker: `dayAndTime` `timeOnly` `dayOnly` `monthOnly`. | | **`date.monthsToShow`** \* | Number of months to display max is 2. Defaults to 1. | | **`date.minDate`** \* | Min date value to allow. | | **`date.maxDate`** \* | Max date value to allow. | | **`date.minTime`** \* | Min time value to allow. | | **`date.maxTime`** \* | Max date value to allow. | | **`date.overrides`** \* | Pass any valid props directly to the [react-datepicker](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md) | | **`date.timeIntervals`** \* | Time intervals to display. Defaults to 30 minutes. | | **`date.timeFormat`** \* | Determines time format. Defaults to `'h:mm aa'`. | _\* This property is passed directly to [react-datepicker](https://github.com/Hacker0x01/react-datepicker/blob/master/docs/datepicker.md)._ ### Display Format and Picker Appearance These properties only affect how the date is displayed in the UI. The full date is always stored in the format `YYYY-MM-DDTHH:mm:ss.SSSZ` (e.g. `1999-01-01T8:00:00.000+05:00`). `displayFormat` determines how the date is presented in the field **cell**, you can pass any valid [unicode date format](https://date-fns.org/v4.1.0/docs/format). `pickerAppearance` sets the appearance of the **react datepicker**, the options available are `dayAndTime`, `dayOnly`, `timeOnly`, and `monthOnly`. By default, the datepicker will display `dayOnly`. When only `pickerAppearance` is set, an equivalent format will be rendered in the date field cell. To overwrite this format, set `displayFormat`. ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'dateOnly', type: 'date', admin: { date: { pickerAppearance: 'dayOnly', displayFormat: 'd MMM yyy', }, }, }, { name: 'timeOnly', type: 'date', admin: { date: { pickerAppearance: 'timeOnly', displayFormat: 'h:mm:ss a', }, }, }, { name: 'monthOnly', type: 'date', admin: { date: { pickerAppearance: 'monthOnly', displayFormat: 'MMMM yyyy', }, }, }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { DateTimeField } from '@payloadcms/ui' import type { DateFieldServerProps } from 'payload' export const CustomDateFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { DateTimeField } from '@payloadcms/ui' import type { DateFieldClientProps } from 'payload' export const CustomDateFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { DateFieldLabelServerProps } from 'payload' export const CustomDateFieldLabelServer: React.FC< DateFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { DateFieldLabelClientProps } from 'payload' export const CustomDateFieldLabelClient: React.FC< DateFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` ## Timezones To enable timezone selection on a Date field, set the `timezone` property to `true`: ```ts { name: 'date', type: 'date', timezone: true, } ``` This will add a dropdown to the date picker that allows users to select a timezone. The selected timezone will be saved in the database along with the date in a new column named `date_tz`. You can customise the available list of timezones in the [global admin config](/docs/v4/admin/overview.md#timezones) or on the field config itself which accepts the following config as well: | Property | Description | | -------------------- | ---------------------------------------------------------------------------------------- | | `defaultTimezone` | A value for the default timezone to be set. | | `supportedTimezones` | An array of supported timezones with label and value object. | | `required` | If true, the timezone selection will be required even if the date is not. | | `override` | A function to customize the generated timezone field. [More details](#timezone-override) | ```ts { name: 'date', type: 'date', timezone: { defaultTimezone: 'America/New_York', supportedTimezones: [ { label: 'New York', value: 'America/New_York' }, { label: 'Los Angeles', value: 'America/Los_Angeles' }, { label: 'London', value: 'Europe/London' }, ], }, } ``` > **Good to know:** > The date itself will be stored in UTC so it's up to you to handle the conversion to the user's timezone when displaying the date in your frontend. > > Dates without a specific time are normalised to 12:00 in the selected timezone. ### Timezone Override The `override` function allows you to customize the auto-generated timezone select field at a granular level. This is useful when you need to modify admin options like visibility, descriptions, or other field properties. ```ts { name: 'publishedAt', type: 'date', label: 'Published At', timezone: { override: ({ baseField }) => ({ ...baseField, admin: { ...baseField.admin, disabled: { column: true }, // Hide from list view columns }, }), }, } ``` The `override` function receives an object with `baseField` (the default timezone select field) and must return a valid field configuration. The base field includes: - `name`: The timezone field name (e.g., `publishedAt_tz`) - `type`: Always `'select'` - `options`: The available timezone options - `defaultValue`: The default timezone value - `required`: Whether the timezone is required - `label`: Auto-generated from the parent field's label (e.g., "Published At Tz") - `admin.hidden`: `true` by default > **Note** > > We recommend changing the available options only via the supportedTimezones > config so that the right validations are run against your timezones. ### Custom UTC Offsets In addition to IANA timezone names (like `America/New_York`), you can also use fixed UTC offsets in the `±HH:mm` format: ```ts { name: 'eventTime', type: 'date', timezone: { supportedTimezones: [ { label: 'UTC+5:30 (India)', value: '+05:30' }, { label: 'UTC-8 (Pacific)', value: '-08:00' }, { label: 'UTC+0', value: '+00:00' }, ], }, } ``` You can also mix IANA timezones with custom UTC offsets: ```ts { name: 'scheduledAt', type: 'date', timezone: { supportedTimezones: [ { label: 'New York', value: 'America/New_York' }, { label: 'UTC+5:30', value: '+05:30' }, { label: 'UTC', value: 'UTC' }, ], }, } ``` > **Note** > > Custom UTC offsets are fixed and do not account for daylight saving time (DST) > adjustments. If you need automatic DST handling, use IANA timezone names > instead (e.g., `America/New_York` rather than `-05:00`). #### GraphQL Enum Names When using offset timezones with GraphQL, the offset values are transformed to valid GraphQL enum names using the `_TZOFFSET_` prefix: | Offset Value | GraphQL Enum Name | | ------------ | ----------------------- | | `+05:30` | `_TZOFFSET_PLUS_05_30` | | `-08:00` | `_TZOFFSET_MINUS_08_00` | | `+00:00` | `_TZOFFSET_PLUS_00_00` | Similarly, IANA timezone names are also transformed (e.g., `America/New_York` becomes `America_New_York`). ```graphql # Query returns the enum name query { Event { scheduledAt_tz # Returns "_TZOFFSET_PLUS_05_30" } } # Mutations use the enum name mutation { createEvent(data: { scheduledAt_tz: _TZOFFSET_PLUS_05_30 }) { scheduledAt_tz } } ``` The actual value (`+05:30`) is stored in the database and returned by the REST API. # Email Field Source: https://payloadcms.com/docs/beta/fields/email The Email Field enforces that the value provided is a valid email address. ![Shows an Email field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/email.png) _Admin Panel screenshot of an Email field_ To create an Email Field, set the `type` to `email` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyEmailField: Field = { // ... type: 'email', // highlight-line } ``` ## Config Options | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Email Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyEmailField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Email Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Property | Description | | ------------------ | ------------------------------------------------------------------------- | | **`placeholder`** | Set this property to define a placeholder string for the field. | | **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'contact', // required type: 'email', // required label: 'Contact Email Address', required: true, }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { EmailField } from '@payloadcms/ui' import type { EmailFieldServerProps } from 'payload' export const CustomEmailFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { EmailField } from '@payloadcms/ui' import type { EmailFieldClientProps } from 'payload' export const CustomEmailFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { EmailFieldLabelServerProps } from 'payload' export const CustomEmailFieldLabelServer: React.FC< EmailFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { EmailFieldLabelClientProps } from 'payload' export const CustomEmailFieldLabelClient: React.FC< EmailFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # Group Field Source: https://payloadcms.com/docs/beta/fields/group The Group Field allows [Fields](/docs/v4/fields/overview.md) to be nested under a common property name. It also groups fields together visually in the [Admin Panel](/docs/v4/admin/overview.md). ![Shows a Group field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/group.png) _Admin Panel screenshot of a Group field_ To add a Group Field, set the `type` to `group` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyGroupField: Field = { // ... // highlight-start type: 'group', fields: [ // ... ], // highlight-end } ``` ## Config Options | Option | Description | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`fields`** \* | Array of field types to nest within this Group. | | **`label`** | Used as a heading in the Admin Panel and to name the generated GraphQL type. Defaults to the field name, if defined. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide an object of data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. If enabled, a separate, localized set of all data within this Group will be kept, so there is no need to specify each nested field as `localized`. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/v4/typescript/generating-types.md#custom-field-interfaces) & [GraphQL type](/docs/v4/graphql/graphql-schema.md#custom-field-schemas). | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database. See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Group Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyGroupField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Group Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Option | Description | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`hideGutter`** | Set this property to `true` to hide this field's gutter within the Admin Panel. The field gutter is rendered as a vertical line and padding, but often if this field is nested within a Group, Block, or Array, you may want to hide the gutter. | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'pageMeta', type: 'group', // required interfaceName: 'Meta', // optional fields: [ // required { name: 'title', type: 'text', required: true, minLength: 20, maxLength: 100, }, { name: 'description', type: 'textarea', required: true, minLength: 40, maxLength: 160, }, ], }, ], } ``` ## Presentational group fields You can also use the Group field to only visually group fields without affecting the data structure. Not defining a `name` will render just the grouped fields (no nested object is created). If you want the group to appear as a titled section in the Admin UI, set a `label`. ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { label: 'Page meta', // label only → presentational type: 'group', // required fields: [ { name: 'title', type: 'text', required: true, minLength: 20, maxLength: 100, }, { name: 'description', type: 'textarea', required: true, minLength: 40, maxLength: 160, }, ], }, ], } ``` ## Named group ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'pageMeta', // name → nested object in data label: 'Page meta', type: 'group', // required fields: [ { name: 'title', type: 'text', required: true, minLength: 20, maxLength: 100, }, { name: 'description', type: 'textarea', required: true, minLength: 40, maxLength: 160, }, ], }, ], } ``` # Number Field Source: https://payloadcms.com/docs/beta/fields/number The Number Field stores and validates numeric entry and supports additional numerical validation and formatting features. ![Shows a Number field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/number.png) _Admin Panel screenshot of a Number field_ To add a Number Field, set the `type` to `number` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyNumberField: Field = { // ... type: 'number', // highlight-line } ``` ## Config Options | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`min`** | Minimum value accepted. Used in the default `validation` function. | | **`max`** | Maximum value accepted. Used in the default `validation` function. | | **`hasMany`** | Makes this field an ordered array of numbers instead of just a single number. | | **`minRows`** | Minimum number of numbers in the numbers array, if `hasMany` is set to true. | | **`maxRows`** | Maximum number of numbers in the numbers array, if `hasMany` is set to true. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Number Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyNumberField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Number Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Property | Description | | ------------------ | --------------------------------------------------------------------------------- | | **`step`** | Set a value for the number field to increment / decrement using browser controls. | | **`placeholder`** | Set this property to define a placeholder string for the field. | | **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'age', // required type: 'number', // required required: true, admin: { step: 1, }, }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { NumberField } from '@payloadcms/ui' import type { NumberFieldServerProps } from 'payload' export const CustomNumberFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { NumberField } from '@payloadcms/ui' import type { NumberFieldClientProps } from 'payload' export const CustomNumberFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { NumberFieldLabelServerProps } from 'payload' export const CustomNumberFieldLabelServer: React.FC< NumberFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { NumberFieldLabelClientProps } from 'payload' export const CustomNumberFieldLabelClient: React.FC< NumberFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # Point Field Source: https://payloadcms.com/docs/beta/fields/point The Point Field saves a pair of coordinates in the database and assigns an index for location related queries. The data structure in the database matches the GeoJSON structure to represent point. The Payload API simplifies the object data to only the [longitude, latitude] location. ![Shows a Point field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/point.png) _Admin Panel screenshot of a Point field_ To add a Point Field, set the `type` to `point` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyPointField: Field = { // ... type: 'point', // highlight-line } ``` > **Important:** The Point Field currently is not supported in SQLite. ## Config | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Used as a field label in the Admin Panel and to name the generated GraphQL type. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. To support location queries, point index defaults to `2dsphere`, to disable the index set to `false`. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](/docs/v4/fields/overview.md#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'location', type: 'point', label: 'Location', }, ], } ``` ## Querying - near In order to do query based on the distance to another point, you can use the `near` operator. When querying using the near operator, the returned documents will be sorted by nearest first. ## Querying - within In order to do query based on whether points are within a specific area defined in GeoJSON, you can use the `within` operator. Example: ```ts const polygon: Point[] = [ [9.0, 19.0], // bottom-left [9.0, 21.0], // top-left [11.0, 21.0], // top-right [11.0, 19.0], // bottom-right [9.0, 19.0], // back to starting point to close the polygon ] payload.find({ collection: 'points', overrideAccess: true, where: { point: { within: { type: 'Polygon', coordinates: [polygon], }, }, }, }) ``` ## Querying - intersects In order to do query based on whether points intersect a specific area defined in GeoJSON, you can use the `intersects` operator. Example: ```ts const polygon: Point[] = [ [9.0, 19.0], // bottom-left [9.0, 21.0], // top-left [11.0, 21.0], // top-right [11.0, 19.0], // bottom-right [9.0, 19.0], // back to starting point to close the polygon ] payload.find({ collection: 'points', overrideAccess: true, where: { point: { intersects: { type: 'Polygon', coordinates: [polygon], }, }, }, }) ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { PointField } from '@payloadcms/ui' import type { PointFieldServerProps } from 'payload' export const CustomPointFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { PointField } from '@payloadcms/ui' import type { PointFieldClientProps } from 'payload' export const CustomPointFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { PointFieldLabelServerProps } from 'payload' export const CustomPointFieldLabelServer: React.FC< PointFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { PointFieldLabelClientProps } from 'payload' export const CustomPointFieldLabelClient: React.FC< PointFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # Radio Group Field Source: https://payloadcms.com/docs/beta/fields/radio The Radio Field allows for the selection of one value from a predefined set of possible values and presents a radio group-style set of inputs to the [Admin Panel](/docs/v4/admin/overview.md). ![Shows a Radio field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/radio.png) _Admin Panel screenshot of a Radio field_ To add a Radio Field, set the `type` to `radio` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyRadioField: Field = { // ... // highlight-start type: 'radio', options: [ // ... ], // highlight-end } ``` ## Config Options | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. The default value must exist within provided values in `options`. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/v4/database/postgres.md)). Auto-generated from name if not defined. | | **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/v4/typescript/generating-types.md#custom-field-interfaces) & [GraphQL type](/docs/v4/graphql/graphql-schema.md#custom-field-schemas). | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ > **Important:** > > Option values should be strings that do not contain hyphens or special characters due to GraphQL > enumeration naming constraints. Underscores are allowed. If you determine you need your option > values to be non-strings or contain special characters, they will be formatted accordingly before > being used as a GraphQL enum. ## Admin Options To customize the appearance and behavior of the Radio Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyRadioField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Radio Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Property | Description | | ------------ | ---------------------------------------------------------------------------------------------------------------------------- | | **`layout`** | Allows for the radio group to be styled as a horizontally or vertically distributed list. The default value is `horizontal`. | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'color', // required type: 'radio', // required options: [ // required { label: 'Mint', value: 'mint', }, { label: 'Dark Gray', value: 'dark_gray', }, ], defaultValue: 'mint', // The first value in options. admin: { layout: 'horizontal', }, }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { RadioGroupField } from '@payloadcms/ui' import type { RadioFieldServerProps } from 'payload' export const CustomRadioFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { RadioGroupField } from '@payloadcms/ui' import type { RadioFieldClientProps } from 'payload' export const CustomRadioFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { RadioFieldLabelServerProps } from 'payload' export const CustomRadioFieldLabelServer: React.FC< RadioFieldLabelServerProps > = ({ clientField, path, required }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { RadioFieldLabelClientProps } from 'payload' export const CustomRadioFieldLabelClient: React.FC< RadioFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # Relationship Field Source: https://payloadcms.com/docs/beta/fields/relationship The Relationship Field is one of the most powerful fields Payload features. It provides the ability to easily relate documents together. ![Shows a relationship field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/relationship.png) _Admin Panel screenshot of a Relationship field_ The Relationship field is used in a variety of ways, including: - To add `Product` documents to an `Order` document - To allow for an `Order` to feature a `placedBy` relationship to either an `Organization` or `User` collection - To assign `Category` documents to `Post` documents To add a Relationship Field, set the `type` to `relationship` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyRelationshipField: Field = { // ... // highlight-start type: 'relationship', relationTo: 'products', // highlight-end } ``` ## Config Options | Option | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`relationTo`** \* | Provide one or many collection `slug`s to be able to assign relationships to. | | **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-relationship-options). | | **`hasMany`** | Boolean when, if set to `true`, allows this field to have many relations instead of only one. | | **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with `hasMany`. | | **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with `hasMany`. | | **`maxDepth`** | Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/v4/queries/depth.md#max-depth) | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to link the field with a relationship. See [Virtual Field Configuration](/docs/v4/fields/overview.md#virtual-field-configuration) | | **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/v4/graphql/overview.md#field-complexity) | _\* An asterisk denotes that a property is required._ > **Tip:** The [Depth](/docs/v4/queries/depth.md) parameter can be used to automatically > populate related documents that are returned by the API. ## Admin Options To the appearance and behavior of the Relationship Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyRelationshipField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Relationship Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Property | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop (only works when `hasMany` is set to `true`). | | **`allowCreate`** | Set to `false` if you'd like to disable the ability to create new documents from within the relationship field. | | **`allowEdit`** | Set to `false` if you'd like to disable the ability to edit documents from within the relationship field. | | **`sortOptions`** | Define a default sorting order for the options within a Relationship field's dropdown. [More details](#sort-options) | | **`placeholder`** | Define a custom text or function to replace the generic default placeholder | | **`appearance`** | Set to `drawer` or `select` to change the behavior of the field. Defaults to `select`. | ### Sort Options You can specify `sortOptions` in two ways: **As a string:** Provide a string to define a global default sort field for all relationship field dropdowns across different collections. You can prefix the field name with a minus symbol ("-") to sort in descending order. Example: ```ts sortOptions: 'fieldName', ``` This configuration will sort all relationship field dropdowns by `"fieldName"` in ascending order. **As an object :** Specify an object where keys are collection slugs and values are strings representing the field names to sort by. This allows for different sorting fields for each collection's relationship dropdown. Example: ```ts sortOptions: { "pages": "fieldName1", "posts": "-fieldName2", "categories": "fieldName3" } ``` In this configuration: - Dropdowns related to `pages` will be sorted by `"fieldName1"` in ascending order. - Dropdowns for `posts` will use `"fieldName2"` for sorting in descending order (noted by the "-" prefix). - Dropdowns associated with `categories` will sort based on `"fieldName3"` in ascending order. Note: If `sortOptions` is not defined, the default sorting behavior of the Relationship field dropdown will be used. ## Filtering relationship options Options can be dynamically limited by supplying a [query constraint](/docs/v4/queries/overview.md), which will be used both for validating input and filtering available relationships in the UI. The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to prevent all, or a `Where` query. When using a function, it will be called with an argument object with the following properties: | Property | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `blockData` | The data of the nearest parent block. Will be `undefined` if the field is not within a block or when called on a `Filter` component within the list view. | | `data` | An object containing the full collection or global document currently being edited. Will be an empty object when called on a `Filter` component within the list view. | | `id` | The `id` of the current document being edited. Will be `undefined` during the `create` operation or when called on a `Filter` component within the list view. | | `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property. | | `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. | | `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field. Will be an empty object when called on a `Filter` component within the list view. | | `user` | An object containing the currently authenticated user. | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'purchase', type: 'relationship', relationTo: ['products', 'services'], filterOptions: ({ relationTo, siblingData }) => { // returns a Where query dynamically by the type of relationship if (relationTo === 'products') { return { stock: { greater_than: siblingData.quantity }, } } if (relationTo === 'services') { return { isAvailable: { equals: true }, } } }, }, ], } ``` You can learn more about writing queries [here](/docs/v4/queries/overview.md). > **Note:** > > When a relationship field has both **filterOptions** and a custom > **validate** function, the api will not validate **filterOptions** > unless you call the default relationship field validation function imported from > **payload/shared** in your validate function. ## Bi-directional relationships The `relationship` field on its own is used to define relationships for the document that contains the relationship field, and this can be considered as a "one-way" relationship. For example, if you have a Post that has a `category` relationship field on it, the related `category` itself will not surface any information about the posts that have the category set. However, the `relationship` field can be used in conjunction with the `Join` field to produce powerful bi-directional relationship authoring capabilities. If you're interested in bi-directional relationships, check out the [documentation for the Join field](/docs/v4/fields/join.md). ## How the data is saved Given the variety of options possible within the `relationship` field type, the shape of the data needed for creating and updating these fields can vary. The following sections will describe the variety of data shapes that can arise from this field. ### Has One The most simple pattern of a relationship is to use `hasMany: false` with a `relationTo` that allows for only one type of collection. ```ts { slug: 'example-collection', fields: [ { name: 'owner', // required type: 'relationship', // required relationTo: 'users', // required hasMany: false, } ] } ``` The shape of the data to save for a document with the field configured this way would be: ```json { // ObjectID of the related user "owner": "6031ac9e1289176380734024" } ``` When querying documents in this collection via REST API, you could query as follows: `?where[owner][equals]=6031ac9e1289176380734024`. ### Has One - Polymorphic Also known as **dynamic references**, in this configuration, the `relationTo` field is an array of Collection slugs that tells Payload which Collections are valid to reference. ```ts { slug: 'example-collection', fields: [ { name: 'owner', // required type: 'relationship', // required relationTo: ['users', 'organizations'], // required hasMany: false, } ] } ``` The shape of the data to save for a document with more than one relationship type would be: ```json { "owner": { "relationTo": "organizations", "value": "6031ac9e1289176380734024" } } ``` Here is an example for how to query documents by this data (note the difference in referencing the `owner.value`): `?where[owner.value][equals]=6031ac9e1289176380734024`. You can also query for documents where a field has a relationship to a specific Collection: `?where[owners.relationTo][equals]=organizations`. This query would return only documents that have an owner relationship to organizations. ### Has Many The `hasMany` tells Payload that there may be more than one collection saved to the field. ```ts { slug: 'example-collection', fields: [ { name: 'owners', // required type: 'relationship', // required relationTo: 'users', // required hasMany: true, } ] } ``` To save to the `hasMany` relationship field we need to send an array of IDs: ```json { "owners": ["6031ac9e1289176380734024", "602c3c327b811235943ee12b"] } ``` When querying documents, the format does not change for arrays: `?where[owners][equals]=6031ac9e1289176380734024`. ### Has Many - Polymorphic ```ts { slug: 'example-collection', fields: [ { name: 'owners', // required type: 'relationship', // required relationTo: ['users', 'organizations'], // required hasMany: true, required: true, } ] } ``` Relationship fields with `hasMany` set to more than one kind of collections save their data as an array of objects—each containing the Collection `slug` as the `relationTo` value, and the related document `id` for the `value`: ```json { "owners": [ { "relationTo": "users", "value": "6031ac9e1289176380734024" }, { "relationTo": "organizations", "value": "602c3c327b811235943ee12b" } ] } ``` Querying is done in the same way as the earlier Polymorphic example: `?where[owners.value][equals]=6031ac9e1289176380734024`. ### Querying and Filtering Polymorphic Relationships Polymorphic and non-polymorphic relationships must be queried differently because of how the related data is stored and may be inconsistent across different collections. Because of this, filtering polymorphic relationship fields from the Collection List admin UI is limited to the `id` value. For a polymorphic relationship, the response will always be an array of objects. Each object will contain the `relationTo` and `value` properties. The data can be queried by the related document ID: `?where[field.value][equals]=6031ac9e1289176380734024`. Or by the related document Collection slug: `?where[field.relationTo][equals]=your-collection-slug`. However, you **cannot** query on any field values within the related document. Since we are referencing multiple collections, the field you are querying on may not exist and break the query. > **Note:** > > You **cannot** query on a field within a polymorphic relationship as you would with a > non-polymorphic relationship. ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { RelationshipField } from '@payloadcms/ui' import type { RelationshipFieldServerProps } from 'payload' export const CustomRelationshipFieldServer: React.FC< RelationshipFieldServerProps > = ({ clientField, path, schemaPath, permissions }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { RelationshipField } from '@payloadcms/ui' import type { RelationshipFieldClientProps } from 'payload' export const CustomRelationshipFieldClient = ( props: RelationshipFieldClientProps, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { RelationshipFieldLabelServerProps } from 'payload' export const CustomRelationshipFieldLabelServer: React.FC< RelationshipFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { RelationshipFieldLabelClientProps } from 'payload' export const CustomRelationshipFieldLabelClient: React.FC< RelationshipFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # Join Field Source: https://payloadcms.com/docs/beta/fields/join The Join Field is used to make Relationship and Upload fields available in the opposite direction. With a Join you can edit and view collections having reference to a specific collection document. The field itself acts as a virtual field, in that no new data is stored on the collection with a Join field. Instead, the Admin UI surfaces the related documents for a better editing experience and is surfaced by Payload's APIs. The Join field is useful in scenarios including: - To surface `Orders` for a given `Product` - To view and edit `Posts` belonging to a `Category` - To work with any bi-directional relationship data - Displaying where a document or upload is used in other documents ![Shows Join field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/join.png) _Admin Panel screenshot of Join field_ For the Join field to work, you must have an existing [relationship](/docs/v4/fields/relationship.md) or [upload](/docs/v4/fields/upload.md) field in the collection you are joining. This will reference the collection and path of the field of the related documents. To add a Relationship Field, set the `type` to `join` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyJoinField: Field = { // highlight-start name: 'relatedPosts', type: 'join', collection: 'posts', on: 'category', // highlight-end } // relationship field in another collection: export const MyRelationshipField: Field = { name: 'category', type: 'relationship', relationTo: 'categories', } ``` In this example, the field is defined to show the related `posts` when added to a `category` collection. The `on` property is used to specify the relationship field name of the field that relates to the collection document. With this example, if you navigate to a Category in the Admin UI or an API response, you'll now see that the Posts which are related to the Category are populated for you. This is extremely powerful and can be used to define a wide variety of relationship types in an easy manner. > **Success** > > The Join field is extremely performant and does not add additional query > overhead to your API responses until you add depth of 1 or above. It works in > all database adapters. In MongoDB, we use **aggregations** to automatically > join in related documents, and in relational databases, we use joins. > **Warning** > > The Join Field is not supported in > [DocumentDB](https://aws.amazon.com/documentdb/) and [Azure Cosmos > DB](https://azure.microsoft.com/en-us/products/cosmos-db), as we internally > use MongoDB aggregations to query data for that field, which are limited > there. This can be changed in the future. ### Schema advice When modeling your database, you might come across many places where you'd like to feature bi-directional relationships. But here's an important consideration—you generally only want to store information about a given relationship in _one_ place. Let's take the Posts and Categories example. It makes sense to define which category a post belongs to while editing the post. It would generally not be necessary to have a list of post IDs stored directly on the category as well, for a few reasons: - You want to have a "single source of truth" for relationships, and not worry about keeping two sources in sync with one another - If you have hundreds, thousands, or even millions of posts, you would not want to store all of those post IDs on a given category - Etc. This is where the `join` field is especially powerful. With it, you only need to store the `category_id` on the `post`, and Payload will automatically join in related posts for you when you query for categories. The related category is only stored on the post itself - and is not duplicated on both sides. However, the `join` field is what enables bi-directional APIs and UI for you. ### Using the Join field to have full control of your database schema For typical polymorphic / many relationships, if you're using Postgres or SQLite, Payload will automatically create a `posts_rels` table, which acts as a junction table to store all of a given document's relationships. However, this might not be appropriate for your use case if you'd like to have more control over your database architecture. You might not want to have that `_rels` table, and would prefer to maintain / control your own junction table design. > **Success** > > With the Join field, you can control your own junction table design, and avoid > Payload's automatic _rels table creation. The `join` field can be used in conjunction with _any_ collection - and if you wanted to define your own "junction" collection, which, say, is called `categories_posts` and has a `post_id` and a `category_id` column, you can achieve complete control over the shape of that junction table. You could go a step further and leverage the `admin.hidden` property of the `categories_posts` collection to hide the collection from appearing in the Admin UI navigation. #### Specifying additional fields on relationships Another very powerful use case of the `join` field is to be able to define "context" fields on your relationships. Let's say that you have Posts and Categories, and use join fields on both your Posts and Categories collection to join in related docs from a new pseudo-junction collection called `categories_posts`. Now, the relations are stored in this third junction collection, and can be surfaced on both Posts and Categories. But, importantly, you could add additional "context" fields to this shared junction collection. For example, on this `categories_posts` collection, in addition to having the `category` and `post` fields, we could add custom "context" fields like `featured` or `spotlight`, which would allow you to store additional information directly on relationships. The `join` field gives you complete control over any type of relational architecture in Payload, all wrapped up in a powerful Admin UI. ## Config Options | Option | Description | | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** \* | To be used as the property name when retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`collection`** \* | The `slug`s having the relationship field or an array of collection slugs. | | **`on`** \* | The name of the relationship or upload field that relates to the collection document. Use dot notation for nested paths, like 'myGroup.relationName'. If `collection` is an array, this field must exist for all specified collections | | **`orderable`** | If true, enables custom ordering and joined documents can be reordered via drag and drop. Uses [fractional indexing](/docs/v4/configuration/collections.md#fractional-indexing) for efficient reordering. | | **`where`** | A `Where` query to hide related documents from appearing. Will be merged with any `where` specified in the request. | | **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/v4/queries/depth.md#max-depth). | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. | | **`defaultSort`** | The field name used to specify the order the joined documents are returned. | | **`admin`** | Admin-specific configuration. [More details](#admin-config-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins). | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation. | | **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/v4/graphql/overview.md#field-complexity) | _\* An asterisk denotes that a property is required._ ## Admin Config Options You can control the user experience of the join field using the `admin` config properties. The following options are supported: | Option | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`defaultColumns`** | Array of field names that correspond to which columns to show in the relationship table. Default is the collection config. | | **`allowCreate`** | Set to `false` to remove the controls for making new related documents from this field. | | **`components.Label`** | Override the default Label of the Field Component. [More details](/docs/v4/fields/overview.md#label) | | **`disableRowTypes`** | Set to `false` to render row types, and `true` to hide them. Defaults to `false` for join fields with a singular `relationTo`, and `true` for join fields where `relationTo` is an array. | ## Join Field Data When a document is returned that for a Join field is populated with related documents. The structure returned is an object with: - `docs` an array of related documents or only IDs if the depth is reached - `hasNextPage` a boolean indicating if there are additional documents - `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query ```json { "id": "66e3431a3f23e684075aae9c", "relatedPosts": { "docs": [ { "id": "66e3431a3f23e684075aaeb9", // other fields... "category": "66e3431a3f23e684075aae9c" } // { ... } ], "hasNextPage": false, "totalDocs": 10 // if count: true is passed } // other fields... } ``` ## Join Field Data (polymorphic) When a document is returned that for a polymorphic Join field (with `collection` as an array) is populated with related documents. The structure returned is an object with: - `docs` an array of `relationTo` - the collection slug of the document and `value` - the document itself or the ID if the depth is reached - `hasNextPage` a boolean indicating if there are additional documents - `totalDocs` a total number of documents, exists only if `count: true` is passed to the join query ```json { "id": "66e3431a3f23e684075aae9c", "relatedPosts": { "docs": [ { "relationTo": "posts", "value": { "id": "66e3431a3f23e684075aaeb9", // other fields... "category": "66e3431a3f23e684075aae9c" } } // { ... } ], "hasNextPage": false, "totalDocs": 10 // if count: true is passed } // other fields... } ``` ## Query Options The Join Field supports custom queries to filter, sort, and limit the related documents that will be returned. In addition to the specific query options for each Join Field, you can pass `joins: false` to disable all Join Field from returning. This is useful for performance reasons when you don't need the related documents. The following query options are supported: | Property | Description | | ----------- | --------------------------------------------------------------------------------------------------- | | **`limit`** | The maximum related documents to be returned, default is 10. | | **`where`** | An optional `Where` query to filter joined documents. Will be merged with the field `where` object. | | **`sort`** | A string used to order related results | | **`count`** | Whether include the count of related documents or not. Not included by default | These can be applied to the Local API, GraphQL, and REST API. ### Local API By adding `joins` to the Local API you can customize the request for each join field by the `name` of the field. ```js const result = await payload.find({ collection: 'categories', overrideAccess: true, where: { title: { equals: 'My Category', }, }, joins: { relatedPosts: { limit: 5, where: { title: { equals: 'My Post', }, }, sort: 'title', }, }, }) ``` > **Warning** > > Currently, `Where` query support on joined documents for join fields with an > array of `collection` is limited and not supported for fields inside arrays > and blocks. ### Rest API The REST API supports the same query options as the Local API. You can use the `joins` query parameter to customize the request for each join field by the `name` of the field. For example, an API call to get a document with the related posts limited to 5 and sorted by title: `/api/categories/${id}?joins[relatedPosts][limit]=5&joins[relatedPosts][sort]=title` You can specify as many `joins` parameters as needed for the same or different join fields for a single request. ### GraphQL The GraphQL API supports the same query options as the local and REST APIs. You can specify the query options for each join field in your query. Example: ```graphql query { Categories { docs { relatedPosts( sort: "createdAt" limit: 5 where: { author: { equals: "66e3431a3f23e684075aaeb9" } } """ Optionally pass count: true if you want to retrieve totalDocs """ count: true -- s ) { docs { title } hasNextPage totalDocs } } } } ``` # Rich Text Field Source: https://payloadcms.com/docs/beta/fields/rich-text The Rich Text Field lets editors write and format dynamic content in a familiar interface. The content is saved as JSON in the database and can be converted to HTML or any other format needed. Consistent with Payload's goal of making you learn as little of Payload as possible, customizing and using the Rich Text Editor does not involve learning how to develop for a Payload rich text editor. Instead, you can invest your time and effort into learning the underlying open-source tools that will allow you to apply your learnings elsewhere as well. Admin Panel screenshot of a Rich Text field ## Config Options | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`editor`** | Customize or override the rich text editor. [More details](/docs/v4/rich-text/overview.md). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | \* _An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Rich Text Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option. The Rich Text Field inherits all the default options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options). ```ts import type { Field } from 'payload' export const MyRichTextField: Field = { // ... admin: { // highlight-line // ... }, } ``` Further customization can be done with editor-specific options. ## Editor-specific Options For a ton more editor-specific options, including how to build custom rich text elements directly into your editor, take a look at the [rich text editor documentation](/docs/v4/rich-text/overview.md). # Row Field Source: https://payloadcms.com/docs/beta/fields/row The Row Field is presentational-only and only affects the [Admin Panel](/docs/v4/admin/overview.md). By using it, you can arrange [Fields](/docs/v4/fields/overview.md) next to each other horizontally. ![Shows a row field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/row.png) _Admin Panel screenshot of a Row field_ To add a Row Field, set the `type` to `row` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyRowField: Field = { // ... // highlight-start type: 'row', fields: [ // ... ], // highlight-end } ``` ## Config Options | Option | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------- | | **`fields`** \* | Array of field types to nest within this Row. | | **`admin`** | Admin-specific configuration excluding `description`, `readOnly`, and `hidden`. [More details](/docs/v4/fields/overview.md#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | _\* An asterisk denotes that a property is required._ ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { type: 'row', // required fields: [ // required { name: 'label', type: 'text', required: true, admin: { width: '50%', }, }, { name: 'value', type: 'text', required: true, admin: { width: '50%', }, }, ], }, ], } ``` # Select Field Source: https://payloadcms.com/docs/beta/fields/select The Select Field provides a dropdown-style interface for choosing options from a predefined list as an enumeration. ![Shows a Select field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/select.png) _Admin Panel screenshot of a Select field_ To add a Select Field, set the `type` to `select` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MySelectField: Field = { // ... // highlight-start type: 'select', options: [ // ... ], // highlight-end } ``` ## Config Options | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`options`** \* | Array of options to allow the field to store. Can either be an array of strings, or an array of objects containing a `label` string and a `value` string. | | **`hasMany`** | Boolean when, if set to `true`, allows this field to have many selections instead of only one. | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. See the [default field admin config](/docs/v4/fields/overview.md#admin-options) for more details. | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`enumName`** | Custom enum name for this field when using SQL Database Adapter ([Postgres](/docs/v4/database/postgres.md)). Auto-generated from name if not defined. | | **`dbName`** | Custom table name (if `hasMany` set to `true`) for this field when using SQL Database Adapter ([Postgres](/docs/v4/database/postgres.md)). Auto-generated from name if not defined. | | **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/v4/typescript/generating-types.md#custom-field-interfaces) & [GraphQL type](/docs/v4/graphql/graphql-schema.md#custom-field-schemas). | | **`filterOptions`** | Dynamically filter which options are available based on the user, data, etc. [More details](#filteroptions) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ > **Important:** Option values should be strings that do not contain hyphens or > special characters due to GraphQL enumeration naming constraints. Underscores > are allowed. If you determine you need your option values to be non-strings or > contain special characters, they will be formatted accordingly before being > used as a GraphQL enum. ### filterOptions Used to dynamically filter which options are available based on the current user, document data, or other criteria. Some examples of this might include: - Restricting options based on a user's role, e.g. admin-only options - Displaying different options based on the value of another field, e.g. a city/state selector The result of `filterOptions` will determine: - Which options are displayed in the Admin Panel - Which options can be saved to the database This function can be synchronous or asynchronous, i.e. it can return `Option[]` or `Promise`, allowing you to perform an async lookup (e.g. querying another collection) before resolving the filtered options. To do this, use the `filterOptions` property in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MySelectField: Field = { // ... // highlight-start type: 'select', options: [ { label: 'One', value: 'one', }, { label: 'Two', value: 'two', }, { label: 'Three', value: 'three', }, ], filterOptions: ({ options, data }) => data.disallowOption1 ? options.filter( (option) => (typeof option === 'string' ? options : option.value) !== 'one', ) : options, // highlight-end } ``` > **Note:** This property is similar to `filterOptions` in > [Relationship](/docs/v4/fields/relationship.md) or [Upload](/docs/v4/fields/upload.md) fields, except that the > return value of this function is simply an array of options, not a query > constraint. ## Admin Options To customize the appearance and behavior of the Select Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MySelectField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Select Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Property | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | **`isClearable`** | Set to `true` if you'd like this field to be clearable within the Admin UI. | | **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop. (Only works when `hasMany` is set to `true`) | | **`placeholder`** | Define a custom text or function to replace the generic default placeholder | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'selectedFeatures', // required type: 'select', // required hasMany: true, admin: { isClearable: true, isSortable: true, // use mouse to drag and drop different values, and sort them according to your choice }, options: [ { label: 'Metallic Paint', value: 'metallic_paint', }, { label: 'Alloy Wheels', value: 'alloy_wheels', }, { label: 'Carbon Fiber Dashboard', value: 'carbon_fiber_dashboard', }, ], }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type { SelectFieldServerProps } from 'payload' import type React from 'react' import { SelectField } from '@payloadcms/ui' export const CustomSelectFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import type { SelectFieldClientProps } from 'payload' import { SelectField } from '@payloadcms/ui' import React from 'react' export const CustomSelectFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { SelectFieldLabelServerProps } from 'payload' export const CustomSelectFieldLabelServer: React.FC< SelectFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { SelectFieldLabelClientProps } from 'payload' export const CustomSelectFieldLabelClient: React.FC< SelectFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # Slug Field Source: https://payloadcms.com/docs/beta/fields/slug > **Warning** > > The slug field is experimental and may change, or even be removed, in future > releases. Use at your own risk. A slug is a unique, indexed, URL-friendly string that identifies a particular document, often used to construct the URL of a webpage. The Slug Field auto-generates its value from another field, such as a title or name field, and provides UI to lock and unlock the field to protect its value, as well as to re-generate the slug on-demand. To create a Slug Field, set the `type` to `slug` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { // ... fields: [ // ... // highlight-line { name: 'slug', type: 'slug', useAsSlug: 'title' }, // highlight-line ], } ``` ## Config Options The Slug Field defaults `required`, `unique`, and `index` to `true`, and renders in the `sidebar`. In addition to the standard [field options](/docs/v4/fields/overview.md#field-options), it exposes: | Option | Description | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `useAsSlug` | Optional. The name of the top-level field to generate the slug from (e.g. `'title'`). This field must exist in the same collection. | | `slugify` | Optional. Override the default slugify function. [More details](#custom-slugify-function). | Because `slug` is a standard named field, customize it with the usual field properties directly — `label`, `localized`, `required`, `unique` (set `false` to drop the unique index, e.g. for a per-tenant compound index), `index`, `defaultValue`, and `admin` (including `admin.position`). ## Generation The slug is static: it fills once while empty and is preserved thereafter, so editing the source field later never rewrites it — this protects live URLs from changing unexpectedly. - On create, an empty slug is derived from the `useAsSlug` source field, if any. With no source value (or no `useAsSlug` configured), it falls back to `-`, where `N` is the first available integer (e.g. `posts-1`). The slug is therefore always present the moment the document is created. - An explicit value always wins and is normalized through [`slugify`](#custom-slugify-function) (e.g. `Hello World` → `hello-world`). It must be unique — a collision is rejected rather than silently changed. - Once set, the slug is never regenerated automatically. Editors can unlock the field to edit it directly, or re-generate it on-demand from the Admin Panel. - Generated values are deduped against existing documents so two never claim the same slug. Because the slug is guaranteed to exist on create, [autosave-enabled](/docs/v4/versions/autosave.md) documents get a stable slug on the initial draft — before a title is typed — which live preview needs to load the document by its slug. ### Localization A localized slug is unique per locale: the same value can be reused across a document's own locales, but not across documents within one locale. Every locale is seeded on create, so switching locales never lands on a blank slug, and generation is locale-aware when the source is localized. ## Custom Slugify Function You can override the default slugify function of the Slug Field. This is necessary if the slug requires special treatment, such as character encoding, additional language support, etc. This function receives the value of the `useAsSlug` field as `valueToSlugify` and must return a string. For example, if you wanted to use the [`slugify`](https://www.npmjs.com/package/slugify) package, you could do something like this: ```ts import type { CollectionConfig } from 'payload' import slugify from 'slugify' export const MyCollection: CollectionConfig = { // ... fields: [ // ... { name: 'slug', type: 'slug', useAsSlug: 'title', slugify: ({ valueToSlugify }) => slugify(valueToSlugify, { // ...additional `slugify` options here }), }, ], } ``` The following args are provided to the custom `slugify` function: | Argument | Type | Description | | ---------------- | ---------------- | ------------------------------------------------ | | `valueToSlugify` | `string` | The value of the field specified in `useAsSlug`. | | `data` | `object` | The full document data being saved. | | `req` | `PayloadRequest` | The Payload request object. | # Tabs Field Source: https://payloadcms.com/docs/beta/fields/tabs The Tabs Field is presentational-only and only affects the [Admin Panel](/docs/v4/admin/overview.md) (unless a tab is named). By using it, you can place fields within a nice layout component that separates certain sub-fields by a tabbed interface. ![Shows a tabs field used to separate Hero and Page layout in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/tabs.png) _Tabs field type used to separate Hero fields from Page Layout_ To add a Tabs Field, set the `type` to `tabs` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyTabsField: Field = { // ... // highlight-start type: 'tabs', tabs: [ // ... ], // highlight-end } ``` ## Config Options | Option | Description | | ------------- | ----------------------------------------------------------------------- | | **`tabs`** \* | Array of tabs to render within this Tabs field. | | **`admin`** | Admin-specific configuration. [More details](/docs/v4/fields/overview.md#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | ### Tab-specific Config Each tab must have either a `name` or `label` and the required `fields` array. You can also optionally pass a `description` to render within each individual tab. | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** | Groups field data into an object when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | The label to render on the tab itself. Required when name is undefined, defaults to name converted to words. | | **`fields`** \* | The fields to render within this tab. | | **`description`** | Optionally render a description within this tab to describe the contents of the tab itself. | | **`interfaceName`** | Create a top level, reusable [Typescript interface](/docs/v4/typescript/generating-types.md#custom-field-interfaces) & [GraphQL type](/docs/v4/graphql/graphql-schema.md#custom-field-schemas). (`name` must be present) | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | | **`admin`** | Admin-specific configuration. [More details](/docs/v4/fields/overview.md#admin-options). Currently supports `condition` and `description`. | | **`id`** | An optional identifier for the tab. If `admin.condition` is used on an unnamed tab, an `id` is required (and will be auto-generated if not provided). | _\* An asterisk denotes that a property is required._ ## Conditional Tabs Just like fields, individual tabs can be conditionally shown or hidden based on the value of other fields. This is done by providing a `condition` function to the `admin` property of the tab config. When a tab's condition returns `false`, the tab and all of its fields will be hidden from the Admin Panel. If the currently active tab becomes hidden due to a condition change, Payload will automatically switch to the next available visible tab. ### Example ```ts { type: 'tabs', tabs: [ { label: 'Always Visible', fields: [ { name: 'showExtraTab', type: 'checkbox', }, ], }, { label: 'Conditional Tab', admin: { condition: (data) => !!data.showExtraTab, }, fields: [ { name: 'extraField', type: 'text', }, ], }, ], } ``` ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { type: 'tabs', // required tabs: [ // required { label: 'Tab One Label', // required description: 'This will appear within the tab above the fields.', fields: [ // required { name: 'someTextField', type: 'text', required: true, }, ], }, { name: 'tabTwo', label: 'Tab Two Label', // required interfaceName: 'TabTwo', // optional (`name` must be present) fields: [ // required { name: 'numberField', // accessible via tabTwo.numberField type: 'number', required: true, }, ], }, ], }, ], } ``` # Text Field Source: https://payloadcms.com/docs/beta/fields/text The Text Field is one of the most commonly used fields. It saves a string to the database and provides the [Admin Panel](/docs/v4/admin/overview.md) with a simple text input. ![Shows a text field and read-only text field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/text.png) _Admin Panel screenshot of a Text field and read-only Text field_ To add a Text Field, set the `type` to `text` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyTextField: Field = { // ... type: 'text', // highlight-line } ``` ## Config Options | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. | | **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`hasMany`** | Makes this field an ordered array of text instead of just a single text. | | **`minRows`** | Minimum number of texts in the array, if `hasMany` is set to true. | | **`maxRows`** | Maximum number of texts in the array, if `hasMany` is set to true. | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Text Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyTextField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Text Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Option | Description | | ------------------ | --------------------------------------------------------------------------------------------------------------------------- | | **`placeholder`** | Set this property to define a placeholder string in the text input. | | **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. | | **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'pageTitle', // required type: 'text', // required required: true, }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { TextField } from '@payloadcms/ui' import type { TextFieldServerProps } from 'payload' export const CustomTextFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { TextField } from '@payloadcms/ui' import type { TextFieldClientProps } from 'payload' export const CustomTextFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { TextFieldLabelServerProps } from 'payload' export const CustomTextFieldLabelServer: React.FC< TextFieldLabelServerProps > = ({ clientField, path, required }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { TextFieldLabelClientProps } from 'payload' export const CustomTextFieldLabelClient: React.FC< TextFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # Textarea Field Source: https://payloadcms.com/docs/beta/fields/textarea The Textarea Field is nearly identical to the [Text Field](/docs/v4/fields/text.md) but it features a slightly larger input that is better suited to edit longer text. ![Shows a textarea field and read-only textarea field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/textarea.png) _Admin Panel screenshot of a Textarea field and read-only Textarea field_ To add a Textarea Field, set the `type` to `textarea` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyTextareaField: Field = { // ... type: 'textarea', // highlight-line } ``` ## Config Options | Option | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`minLength`** | Used by the default validation function to ensure values are of a minimum character length. | | **`maxLength`** | Used by the default validation function to ensure values are of a maximum character length. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Textarea Field in the [Admin Panel](/docs/v4/admin/overview.md), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyTextareaField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Textarea Field inherits all of the default admin options from the base [Field Admin Config](/docs/v4/fields/overview.md#admin-options), plus the following additional options: | Option | Description | | ------------------ | --------------------------------------------------------------------------------------------------------------------------- | | **`placeholder`** | Set this property to define a placeholder string in the textarea. | | **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. | | **`rows`** | Set the number of visible text rows in the textarea. Defaults to `2` if not specified. | | **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'metaDescription', // required type: 'textarea', // required required: true, }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { TextareaField } from '@payloadcms/ui' import type { TextareaFieldServerProps } from 'payload' export const CustomTextareaFieldServer: React.FC = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { TextareaField } from '@payloadcms/ui' import type { TextareaFieldClientProps } from 'payload' export const CustomTextareaFieldClient: React.FC = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { TextareaFieldLabelServerProps } from 'payload' export const CustomTextareaFieldLabelServer: React.FC< TextareaFieldLabelServerProps > = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { TextareaFieldLabelClientProps } from 'payload' export const CustomTextareaFieldLabelClient: React.FC< TextareaFieldLabelClientProps > = ({ field, path }) => { return ( ) } ``` # UI Field Source: https://payloadcms.com/docs/beta/fields/ui The UI (user interface) Field gives you a ton of power to add your own React components directly into the [Admin Panel](/docs/v4/admin/overview.md), nested directly within your other fields. It has absolutely no effect on the data of your documents. It is presentational-only. Think of it as a way for you to "plug in" your own React components directly within your other fields, so you can provide your editors with new controls exactly where you want them to go. With the UI Field, you can: - Add a custom message or block of text within the body of an Edit View to describe the purpose of surrounding fields - Add a "Refund" button to an Order's Edit View sidebar, which might make a fetch call to a custom `refund` endpoint - Add a "view page" button into a Pages List View to give editors a shortcut to view a page on the frontend of the site - Build a "clear cache" button or similar mechanism to manually clear caches of specific documents To add a UI Field, set the `type` to `ui` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyUIField: Field = { // ... type: 'ui', // highlight-line } ``` ## Config Options | Option | Description | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** \* | A unique identifier for this field. | | **`label`** | Human-readable label for this UI field. | | **`admin.components.Field`** \* | React component to be rendered for this field within the Edit View. [More details](/docs/v4/fields/overview.md#field). | | **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More details](/docs/v4/fields/overview.md#cell). | | **`admin.disabled`** | Pass `true` to disable the UI field everywhere, or an object `{ field?, column?, filter?, groupBy?, bulkEdit? }` for granular control. UI fields default to `disabled: { bulkEdit: true }`. [More details](/docs/v4/fields/overview.md#disabled). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | _\* An asterisk denotes that a property is required._ ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'myCustomUIField', // required type: 'ui', // required admin: { components: { Field: '/path/to/MyCustomUIField', Cell: '/path/to/MyCustomUICell', }, }, }, ], } ``` # Upload Field Source: https://payloadcms.com/docs/beta/fields/upload The Upload Field allows for the selection of a Document from a Collection supporting [Uploads](/docs/v4/upload/overview.md), and formats the selection as a thumbnail in the Admin Panel. Upload fields are useful for a variety of use cases, such as: - To provide a `Page` with a featured image - To allow for a `Product` to deliver a downloadable asset like PDF or MP3 - To give a layout building block the ability to feature a background image ![Shows an upload field in the Payload Admin Panel](https://payloadcms.com/images/docs/fields/upload.png) _Admin Panel screenshot of an Upload field_ To create an Upload Field, set the `type` to `upload` in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const MyUploadField: Field = { // ... // highlight-start type: 'upload', relationTo: 'media', // highlight-end } ``` > **Important:** To use the Upload Field, you must have a > [Collection](/docs/v4/configuration/collections.md) configured to allow > [Uploads](/docs/v4/upload/overview.md). ## Config Options | Option | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/v4/fields/overview.md#field-names). | | **`relationTo`** \* | Provide a single collection `slug` or an array of slugs to allow this field to accept a relation to. **Note: the related collections must be configured to support Uploads.** | | **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-upload-options). | | **`hasMany`** | Boolean which, if set to true, allows this field to have many relations instead of only one. | | **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with hasMany. | | **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with hasMany. | | **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](/docs/v4/queries/depth.md) | | **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. | | **`unique`** | Enforce that each entry in the Collection has a unique value for this field. This creates a database-level unique index on the field's path. [More details](/docs/v4/database/indexes.md#unique-fields). | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](/docs/v4/fields/overview.md#validation). | | **`index`** | Build an [index](/docs/v4/database/indexes.md) for this field to produce faster queries. Set this field to `true` if your users will perform queries on this field's data often. | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](/docs/v4/authentication/overview.md), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](/docs/v4/hooks/fields.md). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](/docs/v4/access-control/fields.md). | | **`hidden`** | Restrict this field's visibility from all APIs entirely. Will still be saved to the database, but will not appear in any API or the Admin Panel. | | **`defaultValue`** | Provide data to be used for this field's default value. [More details](/docs/v4/fields/overview.md#default-values). | | **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More details](/docs/v4/upload/overview.md#collection-upload-options). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/v4/configuration/localization.md) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [Admin Options](/docs/v4/fields/overview.md#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`jsonSchema`** | Override field JSON Schema used for type generation and MCP validation | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](/docs/v4/fields/overview.md#string-path-virtual-fields). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | | **`graphQL`** | Custom graphQL configuration for the field. [More details](/docs/v4/graphql/overview.md#field-complexity) | _\* An asterisk denotes that a property is required._ ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'backgroundImage', // required type: 'upload', // required relationTo: 'media', // required required: true, }, ], } ``` ## Filtering upload options Options can be dynamically limited by supplying a [query constraint](/docs/v4/queries/overview.md), which will be used both for validating input and filtering available uploads in the UI. The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to prevent all, or a `Where` query. When using a function, it will be called with an argument object with the following properties: | Property | Description | | ------------- | ----------------------------------------------------------------------------------------------------- | | `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property | | `data` | An object containing the full collection or global document currently being edited | | `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field | | `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation | | `user` | An object containing the currently authenticated user | | `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. | ### Example#filter-options-example ```ts const uploadField = { name: 'image', type: 'upload', relationTo: 'media', filterOptions: { mimeType: { contains: 'image' }, }, } ``` You can learn more about writing queries [here](/docs/v4/queries/overview.md). > **Note:** > > When an upload field has both **filterOptions** and a custom > **validate** function, the api will not validate **filterOptions** > unless you call the default upload field validation function imported from > **payload/shared** in your validate function. ## Bi-directional relationships The `upload` field on its own is used to reference documents in an upload collection. This can be considered a "one-way" relationship. If you wish to allow an editor to visit the upload document and see where it is being used, you may use the `join` field in the upload enabled collection. Read more about bi-directional relationships using the [Join field](/docs/v4/fields/join.md) ## Polymorphic Uploads Upload fields can reference multiple upload collections by providing an array of collection slugs to the `relationTo` property. ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'media', type: 'upload', relationTo: ['images', 'documents', 'videos'], // references multiple upload collections }, ], } ``` This can be combined with the `hasMany` property to allow multiple uploads from multiple collections. ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'media', type: 'upload', relationTo: ['images', 'documents', 'videos'], // references multiple upload collections hasMany: true, // allows multiple uploads }, ], } ``` # Access Control Source: https://payloadcms.com/docs/beta/access-control/overview [Video: Overview of Payload Access Control](https://www.youtube.com/watch?v=DoPLyXG26Dg) Access Control determines what a user can and cannot do with any given Document, as well as what they can and cannot see within the [Admin Panel](/docs/v4/admin/overview.md). By implementing Access Control, you can define granular restrictions based on the user, their roles (RBAC), Document data, or any other criteria your application requires. Access Control functions are scoped to the _operation_, meaning you can have different rules for `create`, `read`, `update`, `delete`, etc. Access Control functions are executed _before_ any changes are made and _before_ any operations are completed. This allows you to determine if the user has the necessary permissions before fulfilling the request. [On-demand validation](/docs/v4/validation/overview.md#access-control-and-hooks) uses its own first-class `validate` operation for collection, global, and field access control. Its access policy falls back to the corresponding `update` function unless `validate` is configured explicitly. There are many use cases for Access Control, including: - Allowing anyone `read` access to all posts - Only allowing public access to posts where a `status` field is equal to `published` - Giving only users with a `role` field equal to `admin` the ability to delete posts - Allowing anyone to submit contact forms, but only logged in users to `read`, `update` or `delete` them - Restricting a user to only be able to see their own orders, but no-one else's - Allowing users that belong to a certain organization to access only that organization's resources There are three main types of Access Control in Payload: - [Collection Access Control](/docs/v4/access-control/collections.md) - [Global Access Control](/docs/v4/access-control/globals.md) - [Field Access Control](/docs/v4/access-control/fields.md) ## Default Access Control Payload provides default Access Control so that your data is secured behind [Authentication](/docs/v4/authentication/overview.md) without additional configuration. To do this, Payload sets a default function that checks that a user is present on the request _and_ that the user belongs to the collection used to access the [Admin Panel](/docs/v4/admin/overview.md#the-admin-user-collection) (`config.admin.user`). You can override this default behavior by defining your own Access Control functions as needed. This means that adding a second [Authentication](/docs/v4/authentication/overview.md)-enabled collection does not automatically grant its users access to everything. Users from collections other than `config.admin.user` are denied by default unless you provide your own Access Control functions. Here is the default Access Control that Payload provides: ```ts const defaultPayloadAccess = ({ req: { payload, user } }) => { // Return `true` only if a user is found // and that user belongs to the admin user collection return Boolean(user) && user.collection === payload.config.admin.user // highlight-line } ``` > **Important:** By default, all [Local API](/docs/v4/local-api/overview.md) operations > respect Access Control based on the passed `user`. Set `overrideAccess: true` > only when the operation should entirely bypass Access Control. See [Local API > Access Control](/docs/v4/local-api/access-control.md). ## Base Access Control Use the top-level `baseAccess` config to apply an additional constraint to standard Collection and Global Access Control operations: ```ts export default buildConfig({ baseAccess: { collections: { create: ({ req, slug }) => slug === 'public-posts' || req.user?.role === 'admin', delete: ({ req, slug }) => slug === 'public-posts' || req.user?.role === 'admin', update: ({ req, slug }) => slug === 'public-posts' || req.user?.role === 'admin', }, globals: { update: ({ req, slug }) => slug === 'public-settings' || req.user?.role === 'admin', }, }, // ... }) ``` The `collections` and `globals` properties are typed as `CollectionAccess` and `GlobalAccess`. All Collection and Global Access Control callbacks, including normal document access, receive the document's `slug`. Payload combines each configured base function with the corresponding document-specific function using `AND` semantics. Returning `false` denies the operation before the document-specific function runs. For operations that support query constraints, returning one combines it with a document-specific constraint using an `and` query. Collection `create` access only supports booleans, so returning a query constraint for that operation throws an error. Collection `admin` access also only supports booleans. `baseAccess` applies only to Collection and Global Access Control. It does not apply to Field Access Control, custom endpoints, direct database adapter calls, job access, or other APIs with dedicated access configuration. Like other Access Control, `baseAccess` is skipped when Local API operations use `overrideAccess: true`. ## The Access Operation The Admin Panel responds dynamically to your changes to Access Control. For example, if you restrict editing `ExampleCollection` to only users that feature an "admin" role, Payload will **hide** that Collection from the Admin Panel entirely. This is super powerful and allows you to control who can do what within your Admin Panel using the same functions that secure your APIs. To accomplish this, Payload exposes the [Access Operation](/docs/v4/authentication/operations.md#access). Upon login, Payload executes each Access Control function at the top level, across all Collections, Globals, and Fields, and returns a response that contains a reflection of what the currently authenticated user can do within your application. > **Important:** When your access control functions are executed via the [Access > Operation](/docs/v4/authentication/operations.md#access), the `id`, `data`, `siblingData`, `blockData` and `doc` arguments > will be `undefined`. Additionally, `Where` queries returned from access control functions will not be run - we'll assume the user does not have access instead. > > This is because Payload is executing your functions without referencing a specific Document. If you use `id`, `data`, `siblingData`, `blockData` and `doc` within your access control functions, make sure to check that they are defined first. If they are not, then you can assume that your Access Control is being executed via the Access Operation to determine solely what the user can do within the Admin Panel. ## Locale Specific Access Control To implement locale-specific access control, you can use the `req.locale` argument in your access control functions. This argument allows you to evaluate the current locale of the request and determine access permissions accordingly. Here is an example: ```ts const access = ({ req }) => { // Grant access if the locale is 'en' if (req.locale === 'en') { return true } // Deny access for all other locales return false } ``` # Collection Access Control Source: https://payloadcms.com/docs/beta/access-control/collections Collection Access Control is [Access Control](/docs/v4/access-control/overview.md) used to restrict access to Documents within a [Collection](/docs/v4/configuration/collections.md), as well as what they can and cannot see within the [Admin Panel](/docs/v4/admin/overview.md) as it relates to that Collection. To add Access Control to a Collection, use the `access` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithAccessControl: CollectionConfig = { // ... access: { // highlight-line // ... }, } ``` ## Config Options Access Control is specific to the operation of the request. To add Access Control to a Collection, use the `access` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload'; export const CollectionWithAccessControl: CollectionConfig = { // ... // highlight-start access: { create: () => {...}, read: () => {...}, update: () => {...}, delete: () => {...}, validate: () => {...}, // Auth-enabled Collections only admin: () => {...}, unlock: () => {...}, // Version-enabled Collections only readVersions: () => {...}, }, // highlight-end } ``` The following options are available: | Function | Allows/Denies Access | | -------------- | ----------------------------------------------------------------------------------------------------------------- | | **`create`** | Used in the `create` operation. [More details](#create). | | **`read`** | Used in the `find` and `findByID` operations. [More details](#read). | | **`update`** | Used in the `update` operation. [More details](#update). | | **`delete`** | Used in the `delete` operation. [More details](#delete). | | **`validate`** | Optionally overrides `update` access for [on-demand validation](/docs/v4/validation/overview.md#access-control-and-hooks). | If a Collection supports [`Authentication`](/docs/v4/authentication/overview.md), the following additional options are available: | Function | Allows/Denies Access | | ------------ | ---------------------------------------------------------------------------------------- | | **`admin`** | Used to restrict access to the [Admin Panel](/docs/v4/admin/overview.md). [More details](#admin). | | **`unlock`** | Used to restrict which users can access the `unlock` operation. [More details](#unlock). | If a Collection supports [Versions](/docs/v4/versions/overview.md), the following additional options are available: | Function | Allows/Denies Access | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`readVersions`** | Used to control who can read versions, and who can't. Will automatically restrict the Admin UI version viewing access. [More details](#read-versions). | ### Create Returns a boolean which allows/denies access to the `create` request. To add create Access Control to a Collection, use the `create` property in the [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithCreateAccess: CollectionConfig = { // ... access: { // highlight-start create: ({ req: { user }, data }) => { return Boolean(user) }, // highlight-end }, } ``` The following arguments are provided to the `create` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`data`** | The data passed to create the document with. | | **`slug`** | The slug of the Collection being accessed. | ### Read Returns a boolean which allows/denies access to the `read` request. To add read Access Control to a Collection, use the `read` property in the [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithReadAccess: CollectionConfig = { // ... access: { // highlight-start read: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` > **Tip:** Return a [Query](/docs/v4/queries/overview.md) to limit the Documents to only > those that match the constraint. This can be helpful to restrict users' access > to specific Documents. [More details](/docs/v4/queries/overview.md). As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config: ```ts import type { Access } from 'payload' import type { Page } from '@/payload-types' export const canReadPage: Access = ({ req: { user } }) => { // Allow authenticated users if (user) { return true } // By returning a Query, guest users can read public Documents // Note: this assumes you have a `isPublic` checkbox field on your Collection return { isPublic: { equals: true, }, } } ``` The following arguments are provided to the `read` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`id`** | `id` of document requested, if within `findByID`. | | **`slug`** | The slug of the Collection being accessed. | ### Update Returns a boolean which allows/denies access to the `update` request. To add update Access Control to a Collection, use the `update` property in the [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithUpdateAccess: CollectionConfig = { // ... access: { // highlight-start update: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` > **Tip:** Return a [Query](/docs/v4/queries/overview.md) to limit the Documents to only > those that match the constraint. This can be helpful to restrict users' access > to specific Documents. [More details](/docs/v4/queries/overview.md). As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config: ```ts import type { Access } from 'payload' import type { User } from '@/payload-types' export const canUpdateUser: Access = ({ req: { user }, id }) => { // Allow users with a role of 'admin' if (user.roles && user.roles.some((role) => role === 'admin')) { return true } // allow any other users to update only oneself return user.id === id } ``` The following arguments are provided to the `update` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`id`** | `id` of document requested to update. | | **`data`** | The data passed to update the document with. | | **`slug`** | The slug of the Collection being accessed. | > **Tip:** For Collections with [Drafts](/docs/v4/versions/drafts.md) enabled, you can > use `update` access to [control who can > publish](/docs/v4/versions/drafts.md#controlling-who-can-publish) by returning a query > constraint on the `_status` field. ### Delete Similarly to the Update function, returns a boolean or a [query constraint](/docs/v4/queries/overview.md) to limit which documents can be deleted by which users. To add delete Access Control to a Collection, use the `delete` property in the [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithDeleteAccess: CollectionConfig = { // ... access: { // highlight-start delete: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config: ```ts import type { Access } from 'payload' import type { Customer } from '@/payload-types' export const canDeleteCustomer: Access = async ({ req, id }) => { if (!id) { // allow the admin UI to show controls to delete since it is indeterminate without the `id` return true } // Query another Collection using the `id` const result = await req.payload.find({ collection: 'contracts', overrideAccess: true, limit: 0, depth: 0, where: { customer: { equals: id }, }, }) return result.totalDocs === 0 } ``` The following arguments are provided to the `delete` function: | Option | Description | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object with additional `user` property, which is the currently logged in user. | | **`id`** | `id` of document requested to delete. | | **`data`** | The data being set on the document. For [Trash-enabled](/docs/v4/trash/overview.md) collections, check `data.deletedAt` to differentiate between soft delete and permanent delete operations. | | **`slug`** | The slug of the Collection being accessed. | > **Tip:** For Collections with [Trash](/docs/v4/trash/overview.md) enabled, you can use > the `data` argument to allow users to soft delete (trash) documents while > restricting permanent deletion. See [Trash Access > Control](/docs/v4/trash/overview.md#access-control) for examples. ### Admin If the Collection is used to access the [Admin Panel](/docs/v4/admin/overview.md#the-admin-user-collection), the `Admin` Access Control function determines whether or not the currently logged in user can access the admin UI. To add Admin Access Control to a Collection, use the `admin` property in the [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithAdminAccess: CollectionConfig = { // ... access: { // highlight-start admin: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` The following arguments are provided to the `admin` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`slug`** | The slug of the Collection being accessed. | ### Unlock Determines which users can [unlock](/docs/v4/authentication/operations.md#unlock) other users who may be blocked from authenticating successfully due to [failing too many login attempts](/docs/v4/authentication/overview.md#config-options). To add Unlock Access Control to a Collection, use the `unlock` property in the [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithUnlockAccess: CollectionConfig = { // ... access: { // highlight-start unlock: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` The following arguments are provided to the `unlock` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`slug`** | The slug of the Collection being accessed. | ### Read Versions If the Collection has [Versions](/docs/v4/versions/overview.md) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document. If `readVersions` is not configured, Payload uses the Collection's `read` Access Control. Boolean results are preserved, and query constraints are automatically adjusted to target fields within the stored version document. For `findVersionByID`, the inherited `read` function receives the parent Document's `id`, not the version record's `id`. To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithVersionsAccess: CollectionConfig = { // ... access: { // highlight-start readVersions: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` > **Note:** When you explicitly configure `readVersions`, returning a > [Query](/docs/v4/queries/overview.md) will apply the constraint to the [`versions` > collection](/docs/v4/versions/overview.md#database-impact), not the original > Collection. The following arguments are provided to the `readVersions` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`slug`** | The slug of the Collection being accessed. | # Globals Access Control Source: https://payloadcms.com/docs/beta/access-control/globals Global Access Control is [Access Control](/docs/v4/access-control/overview.md) used to restrict access to [Global](/docs/v4/configuration/globals.md) Documents, as well as what they can and cannot see within the [Admin Panel](/docs/v4/admin/overview.md) as it relates to that Global. To add Access Control to a Global, use the `access` property in your [Global Config](/docs/v4/configuration/globals.md): ```ts import type { GlobalConfig } from 'payload' export const GlobalWithAccessControl: GlobalConfig = { // ... access: { // highlight-line // ... }, } ``` ## Config Options Access Control is specific to the operation of the request. To add Access Control to a [Global](/docs/v4/configuration/globals.md), use the `access` property in the [Global Config](/docs/v4/configuration/globals.md): ```ts import { GlobalConfig } from 'payload' const GlobalWithAccessControl: GlobalConfig = { // ... // highlight-start access: { read: ({ req: { user } }) => {...}, update: ({ req: { user } }) => {...}, validate: ({ req: { user } }) => {...}, // Version-enabled Globals only readVersions: () => {...}, }, // highlight-end } export default Header ``` The following options are available: | Function | Allows/Denies Access | | -------------- | ----------------------------------------------------------------------------------------------------------------- | | **`read`** | Used in the `findOne` Global operation. [More details](#read). | | **`update`** | Used in the `update` Global operation. [More details](#update). | | **`validate`** | Optionally overrides `update` access for [on-demand validation](/docs/v4/validation/overview.md#access-control-and-hooks). | If a Global supports [Versions](/docs/v4/versions/overview.md), the following additional options are available: | Function | Allows/Denies Access | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`readVersions`** | Used to control who can read versions, and who can't. Will automatically restrict the Admin UI version viewing access. [More details](#read-versions). | ### Read Returns a boolean result or optionally a [query constraint](/docs/v4/queries/overview.md) which limits who can read this global based on its current properties. To add read Access Control to a [Global](/docs/v4/configuration/globals.md), use the `access` property in the [Global Config](/docs/v4/configuration/globals.md): ```ts import { GlobalConfig } from 'payload' const Header: GlobalConfig = { // ... // highlight-start access: { read: ({ req: { user } }) => { return Boolean(user) }, }, // highlight-end } ``` The following arguments are provided to the `read` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`slug`** | The slug of the Global being accessed. | ### Update Returns a boolean result or optionally a [query constraint](/docs/v4/queries/overview.md) which limits who can update this global based on its current properties. To add update Access Control to a [Global](/docs/v4/configuration/globals.md), use the `access` property in the [Global Config](/docs/v4/configuration/globals.md): ```ts import { GlobalConfig } from 'payload' const Header: GlobalConfig = { // ... // highlight-start access: { update: ({ req: { user }, data }) => { return Boolean(user) }, }, // highlight-end } ``` The following arguments are provided to the `update` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`data`** | The data passed to update the global with. | | **`slug`** | The slug of the Global being accessed. | ### Read Versions If the Global has [Versions](/docs/v4/versions/overview.md) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document. If `readVersions` is not configured, Payload uses the Global's `read` Access Control. Boolean results are preserved, and query constraints are automatically adjusted to target fields within the stored version document. To add Read Versions Access Control to a Global, use the `readVersions` property in the [Global Config](/docs/v4/configuration/globals.md): ```ts import type { GlobalConfig } from 'payload' export const GlobalWithVersionsAccess: GlobalConfig = { // ... access: { // highlight-start readVersions: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` > **Note:** When you explicitly configure `readVersions`, returning a > [Query](/docs/v4/queries/overview.md) will apply the constraint to the [`versions` > collection](/docs/v4/versions/overview.md#database-impact), not the original Global. The following arguments are provided to the `readVersions` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`slug`** | The slug of the Global being accessed. | # Field-level Access Control Source: https://payloadcms.com/docs/beta/access-control/fields Field Access Control is [Access Control](/docs/v4/access-control/overview.md) used to restrict access to specific [Fields](/docs/v4/fields/overview.md) within a Document. To add Access Control to a Field, use the `access` property in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const FieldWithAccessControl: Field = { // ... access: { // highlight-line // ... }, } ``` > **Note:** Field Access Control does not support returning > [Query](/docs/v4/queries/overview.md) constraints like [Collection Access > Control](/docs/v4/access-control/collections.md) does. ## Config Options Access Control is specific to the operation of the request. To add Access Control to a Field, use the `access` property in the [Field Config](/docs/v4/fields/overview.md): ```ts import type { CollectionConfig } from 'payload'; export const Posts: CollectionConfig = { slug: 'posts', fields: [ { name: 'title', type: 'text', // highlight-start access: { create: ({ req: { user } }) => { ... }, read: ({ req: { user } }) => { ... }, update: ({ req: { user } }) => { ... }, validate: ({ req: { user } }) => { ... }, }, // highlight-end }; ], }; ``` The following options are available: | Function | Purpose | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | **`create`** | Allows or denies the ability to set a field's value when creating a new document. [More details](#create). | | **`read`** | Allows or denies the ability to read a field's value. [More details](#read). | | **`update`** | Allows or denies the ability to update a field's value. [More details](#update). | | **`validate`** | Optionally overrides `update` access for candidate field data during [on-demand validation](/docs/v4/validation/overview.md#access-control-and-hooks). | ### Create Returns a boolean which allows or denies the ability to set a field's value when creating a new document. If `false` is returned, any passed values will be discarded. **Available argument properties:** | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` | | **`collection`** | The collection config that owns the field (present only for collection-owned fields). | | **`global`** | The global config that owns the field (present only for global-owned fields). | | **`data`** | The full data passed to create the document. | | **`siblingData`** | Immediately adjacent field data passed to create the document. | | **`blockData`** | The nearest parent block row data when this field is inside a block. | ### Read Returns a boolean which allows or denies the ability to read a field's value. If `false`, the entire property is omitted from the resulting document. **Available argument properties:** | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` | | **`collection`** | The collection config that owns the field (present only for collection-owned fields). | | **`global`** | The global config that owns the field (present only for global-owned fields). | | **`id`** | `id` of the document being read | | **`doc`** | The full document data. | | **`siblingData`** | Immediately adjacent field data of the document being read. | | **`blockData`** | The nearest parent block row data when this field is inside a block. | ### Update Returns a boolean which allows or denies the ability to update a field's value. If `false` is returned, any passed values will be discarded. If `false` is returned and you attempt to update the field's value, the operation will **not** throw an error however the field will be omitted from the update operation and the value will remain unchanged. **Available argument properties:** | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` | | **`collection`** | The collection config that owns the field (present only for collection-owned fields). | | **`global`** | The global config that owns the field (present only for global-owned fields). | | **`id`** | `id` of the document being updated | | **`data`** | The full data passed to update the document. | | **`siblingData`** | Immediately adjacent field data passed to update the document with. | | **`doc`** | The full document data, before the update is applied. | | **`blockData`** | The nearest parent block row data when this field is inside a block. | # Hooks Overview Source: https://payloadcms.com/docs/beta/hooks/overview Hooks allow you to execute your own side effects during specific events of the Document lifecycle. They allow you to do things like mutate data, perform business logic, integrate with third-parties, or anything else, all during precise moments within your application. [Video: Payload Hooks Explained: When to Use Them, and when to pass logic to the Jobs Queue](https://www.youtube.com/watch?v=zYhPeBi5PaU) With Hooks, you can transform Payload from a traditional CMS into a fully-fledged application framework. There are many use cases for Hooks, including: - Modify data before it is read or updated - Encrypt and decrypt sensitive data - Integrate with a third-party CRM like HubSpot or Salesforce - Send a copy of uploaded files to Amazon S3 or similar - Process orders through a payment provider like Stripe - Send emails when contact forms are submitted - Track data ownership or changes over time There are four main types of Hooks in Payload: - [Root Hooks](#root-hooks) - [Collection Hooks](/docs/v4/hooks/collections.md) - [Global Hooks](/docs/v4/hooks/globals.md) - [Field Hooks](/docs/v4/hooks/fields.md) > **Reminder:** Payload also ships a set of _React_ hooks that you can use in > your frontend application. Although they share a common name, these are very > different things and should not be confused. [More > details](/docs/v4/admin/react-hooks.md). ## Root Hooks Root Hooks are not associated with any specific Collection, Global, or Field. They are useful for globally-oriented side effects, such as when an error occurs at the application level. To add Root Hooks, use the `hooks` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... // highlight-start hooks: { afterError:[() => {...}] }, // highlight-end }) ``` The following options are available: | Option | Description | | ---------------- | ------------------------------------------------------ | | **`afterError`** | Runs after an error occurs in the Payload application. | ### afterError The `afterError` Hook is triggered when an error occurs in the Payload application. This can be useful for logging errors to a third-party service, sending an email to the development team, logging the error to Sentry or DataDog, etc. The output can be used to transform the result object / status code. ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... hooks: { afterError: [ async ({ error }) => { // Do something }, ], }, }) ``` The following arguments are provided to the `afterError` Hook: | Argument | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`error`** | The error that occurred. | | **`context`** | Custom context passed between Hooks. [More details](/docs/v4/hooks/context.md). | | **`graphqlResult`** | The GraphQL result object, available if the hook is executed within a GraphQL context. | | **`req`** | The `PayloadRequest` object that extends [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request). Contains currently authenticated `user` and the Local API instance `payload`. | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. This will be `undefined` if the hook is executed from a non-collection endpoint or GraphQL. | | **`result`** | The formatted error result object, available if the hook is executed from a REST context. | ## Awaited vs. non-blocking hooks Hooks can either block the request until they finish or run without blocking it. What matters is whether your hook returns a Promise. Awaited (blocking): If your hook returns a Promise (for example, if it’s declared async), Payload will wait for it to resolve before continuing that lifecycle step. Use this when your hook needs to modify data or influence the response. Hooks that return Promises run in series at the same lifecycle stage. Non-blocking (sometimes called “fire-and-forget”): If your hook does not return a Promise (returns nothing), Payload will not wait for it to finish. This can be useful for side-effects that don’t affect the outcome of the operation, but keep in mind that any work started this way might continue after the request has already completed. **Declaring a function with async does not make it “synchronous.” The async keyword simply makes the function return a Promise automatically — which is why Payload then awaits it.** > **Tip:** If your hook executes a long-running task that doesn't affect the > response in any way, consider [offloading it to the job > queue](#offloading-to-the-jobs-queue). That will free up the request to > continue processing without waiting for the task to complete. **Awaited** ```ts const beforeChange = async ({ data }) => { const enriched = await fetchProfile(data.userId) // Payload waits here return { ...data, profile: enriched } } ``` **Non-blocking** ```ts const afterChange = ({ doc }) => { // Trigger side-effect without blocking void pingAnalyticsService(doc.id) // No return → Payload does not wait } ``` ## Server-only Execution Hooks are only triggered on the server and are automatically excluded from the client-side bundle. This means that you can safely use sensitive business logic in your Hooks without worrying about exposing it to the client. ## Custom Error Messages You might want to return custom error messages in the API or admin panel in your hooks, if an operation cannot be performed - e.g. if a rate limit was exceeded. For this you can leverage the `APIError` class like this: ```ts import { APIError, type CollectionBeforeChangeHook } from 'payload' const beforeChangeHook: CollectionBeforeChangeHook = async ({ data }) => { ... if (rateLimitExceeded) { throw new APIError('You have sent too many requests', 429) } return data } ``` ## Performance Hooks are a powerful way to customize the behavior of your APIs, but some hooks are run very often and can add significant overhead to your requests if not optimized. When building hooks, combine together as many of these strategies as possible to ensure your hooks are as performant as they can be. > **Success** > > For more performance tips, see the [Performance > documentation](/docs/v4/performance/overview.md). ### Writing efficient hooks Consider when hooks are run. One common pitfall is putting expensive logic in hooks that run very often. For example, the `read` operation runs on every read request, so avoid putting expensive logic in a `beforeRead` or `afterRead` hook. ```ts { hooks: { beforeRead: [ async () => { // This runs on every read request - avoid expensive logic here await doSomethingExpensive() return data }, ], }, } ``` Instead, use `afterChange` for work that must run only after Payload saves a document. The `beforeChange` hook also runs during on-demand validation. ```ts { hooks: { beforeChange: [ async ({ context, operation }) => { if (operation === 'validate') { return } await doSomethingExpensive() // ... }, ] }, } ``` ### Using Hook Context Use [Hook Context](/docs/v4/hooks/context.md) to avoid infinite loops or to prevent repeating expensive operations across multiple hooks in the same request. ```ts { hooks: { beforeChange: [ async ({ context }) => { const somethingExpensive = await doSomethingExpensive() context.somethingExpensive = somethingExpensive // ... }, ], }, } ``` To learn more, see the [Hook Context documentation](/docs/v4/hooks/context.md). ### Offloading to the jobs queue If your hooks perform any long-running tasks that don't directly affect the request lifecycle, consider offloading them to the [jobs queue](/docs/v4/jobs-queue/overview.md). This will free up the request to continue processing without waiting for the task to complete. ```ts { hooks: { afterChange: [ async ({ doc, req }) => { // Offload to job queue await req.payload.jobs.queue(...) // ... }, ], }, } ``` To learn more, see the [Job Queue documentation](/docs/v4/jobs-queue/overview.md). # Collection Hooks Source: https://payloadcms.com/docs/beta/hooks/collections Collection Hooks are [Hooks](/docs/v4/hooks/overview.md) that run on Documents within a specific [Collection](/docs/v4/configuration/collections.md). They allow you to execute your own logic during specific events of the Document lifecycle. To add Hooks to a Collection, use the `hooks` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithHooks: CollectionConfig = { // ... hooks: { // highlight-line // ... }, } ``` > **Tip:** You can also set hooks on the field-level to isolate hook logic to > specific fields. [More details](/docs/v4/hooks/fields.md). ## Config Options All Collection Hooks accept an array of [synchronous or asynchronous functions](/docs/v4/hooks/overview.md#awaited-vs-non-blocking-hooks). Each Collection Hook receives specific arguments based on its own type, and has the ability to modify specific outputs. ```ts import type { CollectionConfig } from 'payload'; export const CollectionWithHooks: CollectionConfig = { // ... // highlight-start hooks: { beforeOperation: [(args) => {...}], beforeValidate: [(args) => {...}], beforeDelete: [(args) => {...}], beforeChange: [(args) => {...}], beforeRead: [(args) => {...}], afterChange: [(args) => {...}], afterRead: [(args) => {...}], afterDelete: [(args) => {...}], afterOperation: [(args) => {...}], afterError: [(args) => {....}], // Auth-enabled Hooks beforeLogin: [(args) => {...}], afterLogin: [(args) => {...}], afterLogout: [(args) => {...}], afterRefresh: [(args) => {...}], afterMe: [(args) => {...}], afterForgotPassword: [(args) => {...}], refresh: [(args) => {...}], me: [(args) => {...}], }, // highlight-end } ``` ### beforeOperation The `beforeOperation` hook can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins. ```ts import type { CollectionBeforeOperationHook } from 'payload' const beforeOperationHook: CollectionBeforeOperationHook = async ({ args, operation, req, }) => { return args // return modified operation arguments as necessary } ``` The following arguments are provided to the `beforeOperation` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between Hooks. [More details](/docs/v4/hooks/context.md). | | **`operation`** | The name of the operation that this hook is running within. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ### beforeValidate Runs during the `create`, `update`, and `validate` operations. This hook allows you to add or format data before the incoming data is validated server-side. On-demand validation runs this hook with `operation: 'validate'` without saving the candidate. Use the `operation` argument to skip external calls and other side effects that should only run when data is saved. Please do note that this does not run before client-side validation. If you render a custom field component in your front-end and provide it with a `validate` function, the order that validations will run in is: 1. `validate` runs on the client 2. if successful, `beforeValidate` runs on the server 3. `validate` runs on the server ```ts import type { CollectionBeforeValidateHook } from 'payload' import type { Post } from '@/payload-types' const beforeValidateHook: CollectionBeforeValidateHook = async ({ data, // Typed as Partial }) => { return data } ``` The following arguments are provided to the `beforeValidate` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between Hooks. [More details](/docs/v4/hooks/context.md). | | **`data`** | The incoming data passed through the operation. | | **`operation`** | The name of the operation that this hook is running within. | | **`originalDoc`** | The full document before changes are applied. Present on updates; undefined on creates. Use this to read the document id and any unchanged fields. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | **Why isn’t id in data?** `data` only represents the delta you’re saving. Read `originalDoc.id` during updates, and prefer `afterChange` if you need the `id` during creates > **Warning** > > On update operations, data contains only the fields being changed. It may omit > id and any unchanged fields. Use originalDoc to read existing values. On > create, data is the new submission and the document id is not yet available at > this stage. ### beforeChange Immediately before validation, beforeChange hooks run during `create`, `update`, and `validate` operations. At this stage, treat the data as unvalidated user input. Required fields can be missing, and fields can have an incorrect format. You can modify the data before Payload validates it. On-demand validation runs this hook with `operation: 'validate'` but does not run `afterChange`. Use the `operation` argument to skip external calls and other side effects that should only run when data is saved. ```ts import type { CollectionBeforeChangeHook } from 'payload' export const requireTitleOnUpdate: CollectionBeforeChangeHook = async ({ data, // Partial — changed fields only originalDoc, // Full doc before changes (defined on update) operation, // 'create' | 'update' | 'validate' }) => { // Need the id? Don't expect it in `data`. const id = operation === 'update' ? originalDoc.id : undefined // Example: enforce that title cannot be cleared on update if (operation === 'update' && 'title' in (data ?? {}) && !data.title) { throw new Error(`Document ${id} must have a title`) } return data } ``` The following arguments are provided to the `beforeChange` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`data`** | The incoming data passed through the operation. | | **`operation`** | The name of the operation that this hook is running within. | | **`originalDoc`** | The full document before changes are applied. Present on updates; undefined on creates. Use this to read the document id and any unchanged fields. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | **Why isn’t id in data?** `data` only represents the delta you’re saving. Read `originalDoc.id` during updates, and prefer `afterChange` if you need the `id` during creates > **Warning** > > On update operations, data contains only the fields being changed. It may omit > id and any unchanged fields. Use originalDoc to read existing values. On > create, data is the new submission and the document id is not yet available at > this stage. ### afterChange After a document is created or updated, the `afterChange` hook runs. This hook is helpful to recalculate statistics such as total sales within a global, syncing user profile changes to a CRM, and more. ```ts import type { CollectionAfterChangeHook } from 'payload' import type { Post } from '@/payload-types' const afterChangeHook: CollectionAfterChangeHook = async ({ doc, // Typed as Post previousDoc, // Typed as Post }) => { return doc } ``` The following arguments are provided to the `afterChange` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`data`** | The incoming data passed through the operation. | | **`doc`** | The resulting Document after changes are applied. | | **`operation`** | The name of the operation that this hook is running within. | | **`previousDoc`** | The Document before changes were applied. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ### beforeRead Runs before `find` and `findByID` operations are transformed for output by `afterRead`. This hook fires before hidden fields are removed and before localized fields are flattened into the requested locale. Using this Hook will provide you with all locales and all hidden fields via the `doc` argument. ```ts import type { CollectionBeforeReadHook } from 'payload' import type { Post } from '@/payload-types' const beforeReadHook: CollectionBeforeReadHook = async ({ doc, // Typed as Post }) => { return doc } ``` The following arguments are provided to the `beforeRead` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`doc`** | The resulting Document after changes are applied. | | **`query`** | The [Query](/docs/v4/queries/overview.md) of the request. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ### afterRead Runs as the last step before documents are returned. Flattens locales, hides protected fields, and removes fields that users do not have access to. ```ts import type { CollectionAfterReadHook } from 'payload' import type { Post } from '@/payload-types' const afterReadHook: CollectionAfterReadHook = async ({ doc, // Typed as Post }) => { return doc } ``` The following arguments are provided to the `afterRead` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`doc`** | The resulting Document after changes are applied. | | **`query`** | The [Query](/docs/v4/queries/overview.md) of the request. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ### beforeDelete Runs before the `delete` operation. Returned values are discarded. ```ts import type { CollectionBeforeDeleteHook } from 'payload'; const beforeDeleteHook: CollectionBeforeDeleteHook = async ({ req, id, }) => {...} ``` The following arguments are provided to the `beforeDelete` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`id`** | The ID of the Document being deleted. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ### afterDelete Runs immediately after the `delete` operation removes records from the database. Returned values are discarded. ```ts import type { CollectionAfterDeleteHook } from 'payload'; const afterDeleteHook: CollectionAfterDeleteHook = async ({ req, id, doc, }) => {...} ``` The following arguments are provided to the `afterDelete` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`doc`** | The resulting Document after changes are applied. | | **`id`** | The ID of the Document that was deleted. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ### afterOperation The `afterOperation` hook can be used to modify the result of operations or execute side-effects that run after an operation has completed. ```ts import type { CollectionAfterOperationHook } from 'payload' const afterOperationHook: CollectionAfterOperationHook = async ({ result }) => { return result } ``` The following arguments are provided to the `afterOperation` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`args`** | The arguments passed into the operation. | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | | **`operation`** | The name of the operation that this hook is running within. | | **`result`** | The result of the operation, before modifications. | ### afterError The `afterError` Hook is triggered when an error occurs in the Payload application. This can be useful for logging errors to a third-party service, sending an email to the development team, logging the error to Sentry or DataDog, etc. The output can be used to transform the result object / status code. ```ts import type { CollectionAfterErrorHook } from 'payload'; const afterErrorHook: CollectionAfterErrorHook = async ({ req, id, doc, }) => {...} ``` The following arguments are provided to the `afterError` Hook: | Argument | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`error`** | The error that occurred. | | **`context`** | Custom context passed between Hooks. [More details](/docs/v4/hooks/context.md). | | **`graphqlResult`** | The GraphQL result object, available if the hook is executed within a GraphQL context. | | **`req`** | The `PayloadRequest` object that extends [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request). Contains currently authenticated `user` and the Local API instance `payload`. | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`result`** | The formatted error result object, available if the hook is executed from a REST context. | ### beforeLogin For [Auth-enabled Collections](/docs/v4/authentication/overview.md), this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added to the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation. ```ts import type { CollectionBeforeLoginHook } from 'payload' const beforeLoginHook: CollectionBeforeLoginHook = async ({ user }) => { return user } ``` The following arguments are provided to the `beforeLogin` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | | **`user`** | The user being logged in. | ### afterLogin For [Auth-enabled Collections](/docs/v4/authentication/overview.md), this hook runs after successful `login` operations. You can optionally modify the user that is returned. ```ts import type { CollectionAfterLoginHook } from 'payload'; const afterLoginHook: CollectionAfterLoginHook = async ({ user, token, }) => {...} ``` The following arguments are provided to the `afterLogin` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | | **`token`** | The token generated for the user. | | **`user`** | The user being logged in. | ### afterLogout For [Auth-enabled Collections](/docs/v4/authentication/overview.md), this hook runs after `logout` operations. ```ts import type { CollectionAfterLogoutHook } from 'payload'; const afterLogoutHook: CollectionAfterLogoutHook = async ({ req, }) => {...} ``` The following arguments are provided to the `afterLogout` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ### afterMe For [Auth-enabled Collections](/docs/v4/authentication/overview.md), this hook runs after `me` operations. ```ts import type { CollectionAfterMeHook } from 'payload'; const afterMeHook: CollectionAfterMeHook = async ({ req, response, }) => {...} ``` The following arguments are provided to the `afterMe` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | | **`response`** | The response to return. | ### afterRefresh For [Auth-enabled Collections](/docs/v4/authentication/overview.md), this hook runs after `refresh` operations. ```ts import type { CollectionAfterRefreshHook } from 'payload'; const afterRefreshHook: CollectionAfterRefreshHook = async ({ token, }) => {...} ``` The following arguments are provided to the `afterRefresh` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`exp`** | The expiration time of the token. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | | **`token`** | The newly refreshed user token. | ### afterForgotPassword For [Auth-enabled Collections](/docs/v4/authentication/overview.md), this hook runs after successful `forgotPassword` operations. Returned values are discarded. ```ts import type { CollectionAfterForgotPasswordHook } from 'payload' const afterForgotPasswordHook: CollectionAfterForgotPasswordHook = async ({ args, context, collection, }) => {...} ``` The following arguments are provided to the `afterForgotPassword` hook: | Option | Description | | ---------------- | ------------------------------------------------------------------------------------- | | **`args`** | The arguments passed into the operation. | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | ### refresh For [Auth-enabled Collections](/docs/v4/authentication/overview.md), this hook allows you to optionally replace the default behavior of the `refresh` operation with your own. If you optionally return a value from your hook, the operation will not perform its own logic and continue. ```ts import type { CollectionRefreshHook } from 'payload' const myRefreshHook: CollectionRefreshHook = async ({ args, user, }) => {...} ``` The following arguments are provided to the `afterRefresh` hook: | Option | Description | | ---------- | ---------------------------------------- | | **`args`** | The arguments passed into the operation. | | **`user`** | The user being logged in. | ### me For [Auth-enabled Collections](/docs/v4/authentication/overview.md), this hook allows you to optionally replace the default behavior of the `me` operation with your own. If you optionally return a value from your hook, the operation will not perform its own logic and continue. ```ts import type { CollectionMeHook } from 'payload' const meHook: CollectionMeHook = async ({ args, user, }) => {...} ``` The following arguments are provided to the `me` hook: | Option | Description | | ---------- | ---------------------------------------- | | **`args`** | The arguments passed into the operation. | | **`user`** | The user being logged in. | ## TypeScript Payload exports a type for each Collection hook which can be accessed as follows: ```ts import type { CollectionBeforeOperationHook, CollectionBeforeValidateHook, CollectionBeforeChangeHook, CollectionAfterChangeHook, CollectionAfterReadHook, CollectionBeforeReadHook, CollectionBeforeDeleteHook, CollectionAfterDeleteHook, CollectionBeforeLoginHook, CollectionAfterLoginHook, CollectionAfterLogoutHook, CollectionAfterRefreshHook, CollectionAfterMeHook, CollectionAfterForgotPasswordHook, CollectionRefreshHook, CollectionMeHook, } from 'payload' ``` You can also pass a generic type to each hook for strongly-typed `doc`, `previousDoc`, and `data` properties: ```ts import type { CollectionAfterChangeHook } from 'payload' import type { Post } from '@/payload-types' const afterChangeHook: CollectionAfterChangeHook = async ({ doc, // Typed as Post previousDoc, // Typed as Post }) => { return doc } ``` # Global Hooks Source: https://payloadcms.com/docs/beta/hooks/globals Global Hooks are [Hooks](/docs/v4/hooks/overview.md) that run on [Global](/docs/v4/configuration/globals.md) Documents. They allow you to execute your own logic during specific events of the Document lifecycle. To add Hooks to a Global, use the `hooks` property in your [Global Config](/docs/v4/configuration/globals.md): ```ts import type { GlobalConfig } from 'payload' export const GlobalWithHooks: GlobalConfig = { // ... hooks: { // highlight-line // ... }, } ``` > **Tip:** You can also set hooks on the field-level to isolate hook logic to > specific fields. [More details](/docs/v4/hooks/fields.md). ## Config Options All Global Hooks accept an array of [synchronous or asynchronous functions](/docs/v4/hooks/overview.md#awaited-vs-non-blocking-hooks). Each Global Hook receives specific arguments based on its own type, and has the ability to modify specific outputs. ```ts import type { GlobalConfig } from 'payload'; const GlobalWithHooks: GlobalConfig = { // ... // highlight-start hooks: { beforeOperation: [(args) => {...}], beforeValidate: [(args) => {...}], beforeChange: [(args) => {...}], beforeRead: [(args) => {...}], afterChange: [(args) => {...}], afterRead: [(args) => {...}], } // highlight-end } ``` ### beforeOperation The `beforeOperation` hook can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins. ```ts import type { GlobalBeforeOperationHook } from 'payload' const beforeOperationHook: GlobalBeforeOperationHook = async ({ args, operation, req, }) => { return args // return modified operation arguments as necessary } ``` The following arguments are provided to the `beforeOperation` hook: | Option | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](/docs/v4/configuration/globals.md) in which this Hook is running against. Available operation include: `countVersions`, `read`, `restoreVersion`, and `update`. | | **`context`** | Custom context passed between Hooks. [More details](/docs/v4/hooks/context.md). | | **`operation`** | The name of the operation that this hook is running within. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ### beforeValidate Runs during the `update` and `validate` operations. This hook allows you to add or format data before the incoming data is validated server-side. On-demand validation runs this hook with `operation: 'validate'` without saving the candidate. Use the `operation` argument to skip external calls and other side effects that should only run when data is saved. Please do note that this does not run before client-side validation. If you render a custom field component in your front-end and provide it with a `validate` function, the order that validations will run in is: 1. `validate` runs on the client 2. if successful, `beforeValidate` runs on the server 3. `validate` runs on the server ```ts import type { GlobalBeforeValidateHook } from 'payload' import type { SiteSettings } from '@/payload-types' const beforeValidateHook: GlobalBeforeValidateHook = async ({ data, // Typed as Partial req, originalDoc, // Typed as SiteSettings }) => { return data } ``` The following arguments are provided to the `beforeValidate` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](/docs/v4/configuration/globals.md) in which this Hook is running against. | | **`context`** | Custom context passed between Hooks. [More details](/docs/v4/hooks/context.md). | | **`data`** | The incoming data passed through the operation. | | **`operation`** | The name of the operation that this hook is running within. | | **`originalDoc`** | The full document before changes are applied. Present on updates; undefined on creates. Use this to read the document id and any unchanged fields. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | > **Warning** > > On update operations, data contains only the fields being changed. It may omit > id and any unchanged fields. Use originalDoc to read existing values. On > create, data is the new submission and the document id is not yet available at > this stage. ### beforeChange Immediately before field validation, `beforeChange` hooks run during the `update` and `validate` operations. At this stage, treat the data as unvalidated user input. Required fields can be missing, and fields can have an incorrect format. You can modify the data before Payload validates it. On-demand validation runs this hook with `operation: 'validate'` but does not run `afterChange`. Use the `operation` argument to skip external calls and other side effects that should only run when data is saved. ```ts import type { GlobalBeforeChangeHook } from 'payload' import type { SiteSettings } from '@/payload-types' const beforeChangeHook: GlobalBeforeChangeHook = async ({ data, // Typed as Partial req, originalDoc, // Typed as SiteSettings }) => { return data } ``` The following arguments are provided to the `beforeChange` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](/docs/v4/configuration/globals.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`data`** | The incoming data passed through the operation. | | **`operation`** | The name of the operation that this hook is running within. | | **`originalDoc`** | The full document before changes are applied. Present on updates; undefined on creates. Use this to read the document id and any unchanged fields. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | > **Warning** > > On update operations, data contains only the fields being changed. It may omit > id and any unchanged fields. Use originalDoc to read existing values. On > create, data is the new submission and the document id is not yet available at > this stage. ### afterChange After a global is updated, the `afterChange` hook runs. Use this hook to purge caches of your applications, sync site data to CRMs, and more. ```ts import type { GlobalAfterChangeHook } from 'payload' import type { SiteSettings } from '@/payload-types' const afterChangeHook: GlobalAfterChangeHook = async ({ doc, // Typed as SiteSettings previousDoc, // Typed as SiteSettings req, }) => { return doc } ``` The following arguments are provided to the `afterChange` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](/docs/v4/configuration/globals.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`data`** | The incoming data passed through the operation. | | **`doc`** | The resulting Document after changes are applied. | | **`previousDoc`** | The Document before changes were applied. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ### beforeRead Runs before `findOne` global operation is transformed for output by `afterRead`. This hook fires before hidden fields are removed and before localized fields are flattened into the requested locale. Using this Hook will provide you with all locales and all hidden fields via the `doc` argument. ```ts import type { GlobalBeforeReadHook } from 'payload' const beforeReadHook: GlobalBeforeReadHook = async ({ doc, req, }) => {...} ``` The following arguments are provided to the `beforeRead` hook: | Option | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](/docs/v4/configuration/globals.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`doc`** | The resulting Document after changes are applied. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ### afterRead Runs as the last step before a global is returned. Flattens locales, hides protected fields, and removes fields that users do not have access to. ```ts import type { GlobalAfterReadHook } from 'payload' import type { SiteSettings } from '@/payload-types' const afterReadHook: GlobalAfterReadHook = async ({ doc, // Typed as SiteSettings req, findMany, }) => { return doc } ``` The following arguments are provided to the `afterRead` hook: | Option | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](/docs/v4/configuration/globals.md) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](/docs/v4/hooks/context.md). | | **`findMany`** | Boolean to denote if this hook is running against finding one, or finding many (useful in versions). | | **`doc`** | The resulting Document after changes are applied. | | **`query`** | The [Query](/docs/v4/queries/overview.md) of the request. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | ## TypeScript Payload exports a type for each Global hook which can be accessed as follows: ```ts import type { GlobalBeforeValidateHook, GlobalBeforeChangeHook, GlobalAfterChangeHook, GlobalBeforeReadHook, GlobalAfterReadHook, } from 'payload' ``` You can also pass a generic type to each hook for strongly-typed `doc`, `previousDoc`, and `data` properties: ```ts import type { GlobalAfterChangeHook } from 'payload' import type { SiteSettings } from '@/payload-types' const afterChangeHook: GlobalAfterChangeHook = async ({ doc, // Typed as SiteSettings previousDoc, // Typed as SiteSettings }) => { return doc } ``` # Field Hooks Source: https://payloadcms.com/docs/beta/hooks/fields Field Hooks are [Hooks](/docs/v4/hooks/overview.md) that run on Documents on a per-field basis. They allow you to execute your own logic during specific events of the Document lifecycle. Field Hooks offer incredible potential for isolating your logic from the rest of your [Collection Hooks](/docs/v4/hooks/collections.md) and [Global Hooks](/docs/v4/hooks/globals.md). To add Hooks to a Field, use the `hooks` property in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload' export const FieldWithHooks: Field = { // ... hooks: { // highlight-line // ... }, } ``` ## Config Options All Field Hooks accept an array of synchronous or asynchronous functions. These functions can optionally modify the return value of the field before the operation continues. All Field Hooks are formatted to accept the same arguments, although some arguments may be `undefined` based on the specific hook type. > **Important:** Due to GraphQL's typed nature, changing the type of data that > you return from a field will produce errors in the [GraphQL > API](/docs/v4/graphql/overview.md). If you need to change the shape or type of data, > consider [Collection Hooks](/docs/v4/hooks/collections.md) or [Global Hooks](/docs/v4/hooks/globals.md) > instead. To add hooks to a Field, use the `hooks` property in your [Field Config](/docs/v4/fields/overview.md): ```ts import type { Field } from 'payload'; const FieldWithHooks: Field = { name: 'name', type: 'text', // highlight-start hooks: { beforeValidate: [(args) => {...}], beforeChange: [(args) => {...}], beforeDuplicate: [(args) => {...}], afterChange: [(args) => {...}], afterRead: [(args) => {...}], } // highlight-end } ``` The following arguments are provided to all Field Hooks: | Option | Description | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](/docs/v4/configuration/collections.md) in which this Hook is running against. If the field belongs to a Global, this will be `null`. | | **`context`** | Custom context passed between Hooks. [More details](/docs/v4/hooks/context.md). | | **`data`** | In the `afterRead` hook this is the full Document. In the `create` and `update` operations, this is the incoming data passed through the operation. | | **`field`** | The [Field](/docs/v4/fields/overview.md) which the Hook is running against. | | **`findMany`** | Boolean to denote if this hook is running against finding one, or finding many within the `afterRead` hook. | | **`global`** | The [Global](/docs/v4/configuration/globals.md) in which this Hook is running against. If the field belongs to a Collection, this will be `null`. | | **`operation`** | The name of the operation that this hook is running within. Use it to differentiate between `create`, `update`, and `validate` operations. | | **`originalDoc`** | In the `update` operation, this is the Document before changes were applied. In the `afterChange` hook, this is the resulting Document. | | **`overrideAccess`** | A boolean to denote if the current operation is overriding [Access Control](/docs/v4/access-control/overview.md). | | **`path`** | The path to the [Field](/docs/v4/fields/overview.md) in the schema. | | **`previousDoc`** | In the `afterChange` Hook, this is the Document before changes were applied. | | **`previousSiblingDoc`** | The sibling data of the Document before changes being applied, only in `beforeChange` and `afterChange` hook. | | **`previousValue`** | The previous value of the field, before changes, only in `beforeChange` and `afterChange` hooks. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](/docs/v4/local-api/overview.md) operations. | | **`schemaPath`** | The path of the [Field](/docs/v4/fields/overview.md) in the schema. | | **`siblingData`** | The data of sibling fields adjacent to the field that the Hook is running against. | | **`siblingDocWithLocales`** | The sibling data of the Document with all [Locales](/docs/v4/configuration/localization.md). | | **`siblingFields`** | The sibling fields of the field which the hook is running against. | | **`value`** | The value of the [Field](/docs/v4/fields/overview.md). | > **Tip:** It's a good idea to conditionally scope your logic based on which > operation is executing. For example, if you are writing a `beforeChange` hook, > you may want to perform different logic based on if the current `operation` is > `create`, `update`, or `validate`. ### beforeValidate Runs during the `create`, `update`, and `validate` operations. This hook allows you to add or format data before the incoming data is validated server-side. On-demand validation runs this hook with `operation: 'validate'` without saving the candidate. Use the `operation` argument to skip external calls and other side effects that should only run when data is saved. Please do note that this does not run before client-side validation. If you render a custom field component in your front-end and provide it with a `validate` function, the order that validations will run in is: 1. `validate` runs on the client 2. if successful, `beforeValidate` runs on the server 3. `validate` runs on the server ```ts import type { Field } from 'payload' const usernameField: Field = { name: 'username', type: 'text', hooks: { beforeValidate: [ ({ value }) => { // Trim whitespace and convert to lowercase return value.trim().toLowerCase() }, ], }, } ``` In this example, the `beforeValidate` hook is used to process the `username` field. The hook takes the incoming value of the field and transforms it by trimming whitespace and converting it to lowercase. This ensures that the username is stored in a consistent format in the database. ### beforeChange Immediately before validation, beforeChange hooks run during `create`, `update`, and `validate` operations. At this stage, treat the data as unvalidated user input. Required fields can be missing, and fields can have an incorrect format. You can modify the value before Payload validates it. On-demand validation runs this hook with `operation: 'validate'` but does not run `afterChange`. Use the `operation` argument to skip external calls and other side effects that should only run when data is saved. ```ts import type { Field } from 'payload' const emailField: Field = { name: 'email', type: 'email', hooks: { beforeChange: [ ({ value, operation }) => { if (operation === 'create') { // Perform additional validation or transformation for 'create' operation } return value }, ], }, } ``` In the `emailField`, the `beforeChange` hook checks the `operation` type. If the operation is `create`, it performs additional validation or transformation on the email field value. This allows for operation-specific logic to be applied to the field. ### afterChange The `afterChange` hook is executed after a field's value has been changed and saved in the database. This hook is useful for post-processing or triggering side effects based on the new value of the field. ```ts import type { Field } from 'payload' const membershipStatusField: Field = { name: 'membershipStatus', type: 'select', options: [ { label: 'Standard', value: 'standard' }, { label: 'Premium', value: 'premium' }, { label: 'VIP', value: 'vip' }, ], hooks: { afterChange: [ ({ value, previousValue, req }) => { if (value !== previousValue) { // Log or perform an action when the membership status changes console.log( `User ID ${req.user.id} changed their membership status from ${previousValue} to ${value}.`, ) // Here, you can implement actions that could track conversions from one tier to another } }, ], }, } ``` In this example, the `afterChange` hook is used with a `membershipStatusField`, which allows users to select their membership level (Standard, Premium, VIP). The hook monitors changes in the membership status. When a change occurs, it logs the update and can be used to trigger further actions, such as tracking conversion from one tier to another or notifying them about changes in their membership benefits. ### afterRead The `afterRead` hook is invoked after a field value is read from the database. This is ideal for formatting or transforming the field data for output. ```ts import type { Field } from 'payload' const dateField: Field = { name: 'createdAt', type: 'date', hooks: { afterRead: [ ({ value }) => { // Format date for display return new Date(value).toLocaleDateString() }, ], }, } ``` Here, the `afterRead` hook for the `dateField` is used to format the date into a more readable format using `toLocaleDateString()`. This hook modifies the way the date is presented to the user, making it more user-friendly. ### beforeDuplicate The `beforeDuplicate` field hook is called on each locale (when using localization), when duplicating a document. It may be used when documents having the exact same properties may cause issue. This gives you a way to avoid duplicate names on `unique`, `required` fields or when external systems expect non-repeating values on documents. This hook gets called before the `beforeValidate` and `beforeChange` hooks are called. By Default, unique and required text fields Payload will append "- Copy" to the original document value. The default is not added if your field has its own, you must return non-unique values from your beforeDuplicate hook to avoid errors or enable the `disableDuplicate` option on the collection. Here is an example of a number field with a hook that increments the number to avoid unique constraint errors when duplicating a document: ```ts import type { Field } from 'payload' const numberField: Field = { name: 'number', type: 'number', hooks: { // increment existing value by 1 beforeDuplicate: [ ({ value }) => { return (value ?? 0) + 1 }, ], }, } ``` ## TypeScript Payload exports a type for field hooks which can be accessed and used as follows: ```ts import type { FieldHook } from 'payload' // Field hook type is a generic that takes three arguments: // 1: The document type // 2: The value type // 3: The sibling data type type ExampleFieldHook = FieldHook const exampleFieldHook: ExampleFieldHook = (args) => { const { value, // Typed as `string` as shown above data, // Typed as a Partial of your ExampleDocumentType siblingData, // Typed as a Partial of SiblingDataType originalDoc, // Typed as ExampleDocumentType operation, req, } = args // Do something here... return value // should return a string as typed above, undefined, or null } ``` ### Practical Example with Generated Types Here's a real-world example using generated Payload types: ```ts import type { FieldHook } from 'payload' import type { Post } from '@/payload-types' // Hook for a text field in a Post collection type PostTitleHook = FieldHook const slugifyTitle: PostTitleHook = ({ value, data, siblingData, originalDoc, }) => { // value is typed as string | undefined // data is typed as Partial // siblingData is typed as Partial // originalDoc is typed as Post | undefined // Generate slug from title if not provided if (!siblingData.slug && value) { const slug = value .toLowerCase() .replace(/[^\w\s-]/g, '') .replace(/\s+/g, '-') return slug } return value } // Hook for a relationship field type PostAuthorHook = FieldHook const setDefaultAuthor: PostAuthorHook = ({ value, req }) => { // value is typed as string | number | undefined // Set current user as author if not provided if (!value && req.user) { return req.user.id } return value } ``` > **Tip:** When defining field hooks, use the three generic parameters for full > type safety: document type, field value type, and sibling data type. This > provides autocomplete and type checking for all hook arguments. # Context Source: https://payloadcms.com/docs/beta/hooks/context The `context` object is used to share data across different Hooks. This persists throughout the entire lifecycle of a request and is available within every Hook. By setting properties to `req.context`, you can effectively share logic across multiple Hooks. ## When To Use Context Context gives you a way forward on otherwise difficult problems such as: 1. **Passing data between Hooks**: Needing data in multiple Hooks from a 3rd party API, it could be retrieved and used in `beforeChange` and later used again in an `afterChange` hook without having to fetch it twice. 2. **Preventing infinite loops**: Calling `payload.update()` on the same document that triggered an `afterChange` hook will create an infinite loop, control the flow by assigning a no-op condition to context 3. **Passing data to Local API**: Setting values on the `req.context` and pass it to `payload.create()` you can provide additional data to hooks without adding extraneous fields. 4. **Passing data between hooks and middleware or custom endpoints**: Hooks could set context across multiple collections and then be used in a final `postMiddleware`. ## How To Use Context Let's see examples on how context can be used in the first two scenarios mentioned above: ### Passing Data Between Hooks To pass data between hooks, you can assign values to context in an earlier hook in the lifecycle of a request and expect it in the context of a later hook. For example: ```ts import type { CollectionConfig } from 'payload' const Customer: CollectionConfig = { slug: 'customers', hooks: { beforeChange: [ async ({ context, data }) => { // assign the customerData to context for use later context.customerData = await fetchCustomerData(data.customerID) return { ...data, // some data we use here name: context.customerData.name, } }, ], afterChange: [ async ({ context, doc, req }) => { // use context.customerData without needing to fetch it again if (context.customerData.contacted === false) { createTodo('Call Customer', context.customerData) } }, ], }, fields: [/* ... */], } ``` ### Preventing Infinite Loops Let's say you have an `afterChange` hook, and you want to do a calculation inside the hook (as the document ID needed for the calculation is available in the `afterChange` hook, but not in the `beforeChange` hook). Once that's done, you want to update the document with the result of the calculation. Bad example: ```ts import type { CollectionConfig } from 'payload' const Customer: CollectionConfig = { slug: 'customers', hooks: { afterChange: [ async ({ doc, req }) => { await req.payload.update({ // DANGER: updating the same slug as the collection in an afterChange will create an infinite loop! collection: 'customers', overrideAccess: true, id: doc.id, data: { ...(await fetchCustomerData(data.customerID)), }, }) }, ], }, fields: [/* ... */], } ``` Instead of the above, we need to tell the `afterChange` hook to not run again if it performs the update (and thus not update itself again). We can solve that with context. Fixed example: ```ts import type { CollectionConfig } from 'payload' const MyCollection: CollectionConfig = { slug: 'slug', hooks: { afterChange: [ async ({ context, doc, req }) => { // return if flag was previously set if (context.triggerAfterChange === false) { return } await req.payload.update({ collection: contextHooksSlug, overrideAccess: true, id: doc.id, data: { ...(await fetchCustomerData(data.customerID)), }, context: { // set a flag to prevent from running again triggerAfterChange: false, }, }) }, ], }, fields: [/* ... */], } ``` ## TypeScript The default TypeScript interface for `context` is `{ [key: string]: unknown }`. If you prefer a more strict typing in your project or when authoring plugins for others, you can override this using the `declare module` syntax. This is known as [module augmentation / declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation), a TypeScript feature which allows us to add properties to existing types. Simply put this in any `.ts` or `.d.ts` file: ```ts declare module 'payload' { // Augment the RequestContext interface to include your custom properties export interface RequestContext { myObject?: string // ... } } ``` This will add the property `myObject` with a type of string to every context object. Make sure to follow this example correctly, as module augmentation can mess up your types if you do it wrong. # Local API Source: https://payloadcms.com/docs/beta/local-api/overview The Payload Local API gives you the ability to execute the same operations that are available through REST and GraphQL within Node, directly on your server. Here, you don't need to deal with server latency or network speed whatsoever and can interact directly with your database. > **Tip:** > > The Local API is incredibly powerful when used in React Server Components and other similar server-side contexts. With other headless CMS, you need to request your data from third-party servers via an HTTP layer, which can add significant loading time to your server-rendered pages. With Payload, you don't have to leave your server to gather the data you need. It can be incredibly fast and is definitely a game changer. Here are some common examples of how you can use the Local API: - Fetching Payload data within React Server Components - Seeding data via Node seed scripts that you write and maintain - Building custom server-side functionality that still relies on Payload - Within [Access Control](/docs/v4/access-control/overview.md) and [Hooks](/docs/v4/hooks/overview.md) - [Validating document candidates without saving them](/docs/v4/validation/overview.md#local-api) ## Accessing Payload You can gain access to the currently running `payload` object via two ways: #### Accessing from args or `req` In most places within Payload itself, you can access `payload` directly from the arguments of [Hooks](/docs/v4/hooks/overview.md), [Access Control](/docs/v4/access-control/overview.md), [Validation](/docs/v4/fields/overview.md#validation) functions, and similar. This is the simplest way to access Payload in most cases. Most config functions take the `req` (Request) object, which has Payload bound to it (`req.payload`). Example: ```ts const afterChangeHook: CollectionAfterChangeHook = async ({ req: { payload }, }) => { const posts = await payload.find({ collection: 'posts', overrideAccess: true, }) } ``` #### Importing it If you want to import Payload in places where you don't have the option to access it from function arguments or `req`, you can import it and initialize it. ```ts import { getPayload } from 'payload' import config from '@payload-config' const payload = await getPayload({ config }) ``` If you're working in your framework's development mode, Payload will work with Hot Module Replacement (HMR), and as you make changes to your Payload Config, your usage of Payload will always be in sync with your changes. In production, `getPayload` simply disables all HMR functionality so you don't need to write your code any differently. We handle optimization for you in production mode. If you are accessing Payload via function arguments or `req.payload`, HMR is automatically supported. For more information about using Payload outside of your host framework, [click here](/docs/v4/local-api/outside-frameworks.md). ## Local options available You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are executed in. | Local Option | Description | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `collection` | Required for Collection operations. Specifies the Collection slug to operate against. | | `data` | The data to use within the operation. Required for `create`, `update`. | | `depth` | [Control auto-population](/docs/v4/queries/depth.md) of nested relationship and upload fields. | | `locale` | Specify [locale](/docs/v4/configuration/localization.md) for any returned documents. | | `select` | Specify [select](/docs/v4/queries/select.md) to control which fields to include to the result. | | `populate` | Specify [populate](/docs/v4/queries/select.md#populate) to control which fields to include to the result from populated documents. | | `fallbackLocale` | Specify a [fallback locale](/docs/v4/configuration/localization.md) to use for any returned documents. This can be a single locale or array of locales. | | `overrideAccess` | Skip [Access Control](/docs/v4/local-api/access-control.md). Set to `true` to bypass it for trusted server-side work; where optional, it defaults to `false`, which respects it. | | `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](/docs/v4/admin/locked-documents.md). | | `user` | The user to run access control checks against. Pass the authenticated user when the operation acts on their behalf; omit it to evaluate access as an anonymous request. | | `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. | | `pagination` | Set to `false` to disable pagination calculations and document count queries. Payload still applies any positive `limit`. | | `context` | [Context](/docs/v4/hooks/context.md), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. | | `disableErrors` | When set to `true`, errors will not be thrown. Instead, the `findByID` operation will return `null`, and the `find` operation will return an empty documents array. | | `disableTransaction` | When set to `true`, a [database transactions](/docs/v4/database/transactions.md) will not be initialized. | _There are more options available on an operation by operation basis outlined below._ ## Transactions When your database uses transactions you need to thread req through to all local operations. Postgres uses transactions and MongoDB uses transactions when you are using replica sets. Passing req without transactions is still recommended. ```js const post = await payload.find({ collection: 'posts', overrideAccess: true, req, // passing req is recommended }) ``` > **Note:** > > Local API operations with an optional `overrideAccess` value respect Access Control by default. > Pass `overrideAccess: true` to skip it for trusted server-side work such as cron jobs, seeding, > and migrations. See [Access Control](/docs/v4/local-api/access-control.md). ## Collections The following Collection operations are available through the Local API: ### Create#collection-create ```js // The created Post document is returned const post = await payload.create({ collection: 'posts', // required data: { // required title: 'sure', description: 'maybe', }, locale: 'en', fallbackLocale: false, user: dummyUserDoc, overrideAccess: true, showHiddenFields: false, // If creating verification-enabled auth doc, // you can optionally disable the email that is auto-sent disableVerificationEmail: true, // If your collection supports uploads, you can upload // a file directly through the Local API by providing // its full, absolute file path. filePath: path.resolve(__dirname, './path-to-image.jpg'), // Alternatively, you can directly pass a File, // if file is provided, filePath will be omitted file: uploadedFile, // If you want to create a document that is a duplicate of another document duplicateFromID: 'document-id-to-duplicate', }) ``` ### Find#collection-find ```js // Result will be a paginated set of Posts. // See /docs/queries/pagination for more. const result = await payload.find({ collection: 'posts', // required depth: 2, page: 1, limit: 10, pagination: false, // If you want to disable pagination count, etc. where: {}, // pass a `where` query here sort: '-title', locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` > **Note** > > `pagination`, `page`, and `limit` are three related properties [documented > here](/docs/v4/queries/pagination.md). ### Find by ID#collection-find-by-id ```js // Result will be a Post document. const result = await payload.findByID({ collection: 'posts', // required id: '507f1f77bcf86cd799439011', // required depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Count#collection-count ```js // Result will be an object with: // { // totalDocs: 10, // count of the documents satisfies query // } const result = await payload.count({ collection: 'posts', // required locale: 'en', where: {}, // pass a `where` query here user: dummyUser, overrideAccess: false, }) ``` ### FindDistinct#collection-find-distinct ```js // Result will be an object with: // { // values: ['value-1', 'value-2'], // array of distinct values, // field: 'title', // the field // totalDocs: 10, // count of the distinct values satisfies query, // perPage: 10, // count of distinct values per page (based on provided limit) // } const result = await payload.findDistinct({ collection: 'posts', // required locale: 'en', where: {}, // pass a `where` query here user: dummyUser, overrideAccess: false, field: 'title', sort: 'title', }) ``` ### Update by ID#collection-update-by-id ```js // Result will be the updated Post document. const result = await payload.update({ collection: 'posts', // required id: '507f1f77bcf86cd799439011', // required data: { // required title: 'sure', description: 'maybe', }, depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks. showHiddenFields: true, // If your collection supports uploads, you can upload // a file directly through the Local API by providing // its full, absolute file path. filePath: path.resolve(__dirname, './path-to-image.jpg'), // If you are uploading a file and would like to replace // the existing file instead of generating a new filename, // you can set the following property to `true` overwriteExistingFiles: true, }) ``` ### Update Many#collection-update-many ```js // Result will be an object with: // { // docs: [], // each document that was updated // errors: [], // each error also includes the id of the document // } const result = await payload.update({ collection: 'posts', // required where: { // required fieldName: { equals: 'value' }, }, data: { // required title: 'sure', description: 'maybe', }, depth: 0, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks. showHiddenFields: true, // If your collection supports uploads, you can upload // a file directly through the Local API by providing // its full, absolute file path. filePath: path.resolve(__dirname, './path-to-image.jpg'), // If you are uploading a file and would like to replace // the existing file instead of generating a new filename, // you can set the following property to `true` overwriteExistingFiles: true, }) ``` ### Delete#collection-delete ```js // Result will be the now-deleted Post document. const result = await payload.delete({ collection: 'posts', // required id: '507f1f77bcf86cd799439011', // required depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks. showHiddenFields: true, }) ``` ### Delete Many#collection-delete-many ```js // Result will be an object with: // { // docs: [], // each document that is now deleted // errors: [], // any errors that occurred, including the id of the errored on document // } const result = await payload.delete({ collection: 'posts', // required where: { // required fieldName: { equals: 'value' }, }, depth: 0, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks. showHiddenFields: true, }) ``` ## Auth Operations If a collection has [`Authentication`](/docs/v4/authentication/overview.md) enabled, additional Local API operations will be available: ### Auth ```js // If you're using Next.js, you'll have to import headers from next/headers, like so: // import { headers as nextHeaders } from 'next/headers' // you'll also have to await headers inside your function, or component, like so: // const headers = await nextHeaders() // If you're using TanStack Start, get headers from the incoming request instead: // import { getRequest } from '@tanstack/react-start/server' // const headers = new Headers(getRequest().headers) // If you're using Payload outside of your host framework, you'll have to provide headers accordingly. // result will be formatted as follows: // { // permissions: { ... }, // object containing current user's permissions // user: { ... }, // currently logged in user's document // responseHeaders: { ... } // returned headers from the response // } const result = await payload.auth({ headers, canSetHeaders: false }) ``` ### Login ```js // result will be formatted as follows: // { // token: 'o38jf0q34jfij43f3f...', // JWT used for auth // user: { ... } // the user document that just logged in // exp: 1609619861 // the UNIX timestamp when the JWT will expire // } const result = await payload.login({ collection: 'users', // required data: { // required email: 'dev@payloadcms.com', password: 'rip', }, req: req, // optional, pass a Request object to be provided to all hooks depth: 2, locale: 'en', fallbackLocale: false, overrideAccess: false, showHiddenFields: true, }) ``` ### Forgot Password ```js // Returned token will allow for a password reset const token = await payload.forgotPassword({ collection: 'users', // required overrideAccess: true, data: { // required email: 'dev@payloadcms.com', }, req: req, // pass a Request object to be provided to all hooks }) ``` ### Reset Password ```js // Result will be formatted as follows: // { // token: 'o38jf0q34jfij43f3f...', // JWT used for auth // user: { ... } // the user document that just logged in // } const result = await payload.resetPassword({ collection: 'users', // required overrideAccess: false, // required data: { // required password: req.body.password, // the new password to set token: 'afh3o2jf2p3f...', // the token generated from the forgotPassword operation }, req: req, // optional, pass a Request object to be provided to all hooks }) ``` ### Unlock ```js // Returned result will be a boolean representing success or failure const result = await payload.unlock({ collection: 'users', // required data: { // required email: 'dev@payloadcms.com', }, req: req, // optional, pass a Request object to be provided to all hooks overrideAccess: true, }) ``` ### Verify ```js // Returned result will be a boolean representing success or failure const result = await payload.verifyEmail({ collection: 'users', // required token: 'afh3o2jf2p3f...', // the token saved on the user as `_verificationToken` }) ``` ## Globals The following Global operations are available through the Local API: ### Find#global-find ```js // Result will be the Header Global. const result = await payload.findGlobal({ slug: 'header', // required depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Update#global-update ```js // Result will be the updated Header Global. const result = await payload.updateGlobal({ slug: 'header', // required data: { // required nav: [ { url: 'https://google.com', }, { url: 'https://payloadcms.com', }, ], }, depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks. showHiddenFields: true, }) ``` ## TypeScript Local API calls will automatically infer your [generated types](/docs/v4/typescript/generating-types.md). Here is an example of usage: ```ts // Properly inferred as `Post` type const post = await payload.create({ collection: 'posts', overrideAccess: true, // Data will now be typed as Post and give you type hints data: { title: 'my title', description: 'my description', }, }) ``` # Using Payload Outside Your Host Framework Source: https://payloadcms.com/docs/beta/local-api/outside-frameworks Payload can be used completely outside of the framework that hosts your Admin Panel, which is helpful in cases like running scripts, using Payload in a separate backend service, or using Payload's Local API to fetch your data directly from your database in other frontend frameworks like SvelteKit, Remix, Nuxt, and similar. > **Note:** Payload and all of its official packages are fully ESM. If you want > to use Payload within your own projects, make sure you are writing your > scripts in ESM format or dynamically importing the Payload Config. ## Importing the Payload Config Outside Your Host Framework Payload provides a convenient way to run standalone scripts, which can be useful for tasks like seeding your database or performing one-off operations. In standalone scripts, you can simply import the Payload Config and use it right away. If you need an initialized copy of Payload, you can then use the `getPayload` function. This can be useful for tasks like seeding your database or performing other one-off operations. ```ts import { getPayload } from 'payload' import config from '@payload-config' const seed = async () => { // Get a local copy of Payload by passing your config const payload = await getPayload({ config }) const user = await payload.create({ collection: 'users', overrideAccess: true, data: { email: 'dev@payloadcms.com', password: 'some-password', }, }) const page = await payload.create({ collection: 'pages', overrideAccess: true, data: { title: 'My Homepage', // other data to seed here }, }) } // Call the function here to run your seed script await seed() ``` You can then execute the script using `payload run`. Example: if you placed this standalone script in `src/seed.ts`, you would execute it like this: ```sh payload run src/seed.ts ``` The `payload run` command does two things for you: 1. It loads the environment variables the same way your host framework loads them, eliminating the need for additional dependencies like `dotenv`. The usage of `dotenv` is not recommended, as host frameworks load environment variables differently from one another. By using `payload run`, you ensure consistent environment variable handling across your Payload and app setup. 2. It initializes tsx, allowing direct execution of TypeScript files manually installing tools like tsx or ts-node. ### Troubleshooting If you encounter import-related errors, you have 2 options: #### Option 1: enable swc mode by appending `--use-swc` to the `payload` command: Example: ```sh payload run src/seed.ts --use-swc ``` Note: Install @swc-node/register in your project first. While swc mode is faster than the default tsx mode, it might break for some imports. #### Option 2: use an alternative runtime like bun While we do not guarantee support for alternative runtimes, you are free to use them and disable Payload's own transpilation by appending the `--disable-transpile` flag to the `payload` command: ```sh bunx --bun payload run src/seed.ts --disable-transpile ``` You will need to have bun installed on your system for this to work. # Using Local API Operations with Server Functions Source: https://payloadcms.com/docs/beta/local-api/server-functions **Server functions** are special functions that run exclusively on the server, enabling secure backend logic execution while being callable from the frontend. These functions bridge the gap between client and server, allowing frontend components to perform backend operations without exposing sensitive logic. Both supported Admin Panel frameworks provide them: Next.js calls them server functions (previously **server actions**), declared with the `'use server'` directive, while TanStack Start defines them with `createServerFn` from `@tanstack/react-start`. The examples below use the Next.js `'use server'` syntax; on TanStack Start the body is identical — only the function declaration differs. ### Why Use Server Functions? - **Executing Backend Logic from the Frontend**: The Local API is designed for server environments and cannot be directly accessed from client-side code. Server functions enable frontend components to trigger backend operations securely. - **Security Benefits**: Instead of exposing a full REST or GraphQL API, server functions restrict access to only the necessary operations, reducing potential security risks. - **Performance Optimizations**: Next.js handles server functions efficiently, offering benefits like caching, optimized database queries, and reduced network overhead compared to traditional API calls. - **Simplified Development Workflow**: Rather than setting up full API routes with authentication and authorization checks, server functions allow for lightweight, direct execution of necessary operations. ### When to Use Server Functions Use server functions whenever you need to call Local API operations from the frontend. Since the Local API is only accessible from the backend, server functions act as a secure bridge, eliminating the need to expose additional API endpoints. ## Examples All Local API operations can be used within server functions, allowing you to interact with Payload's backend securely. For a full list of available operations, see the [Local API](/docs/v4/local-api/overview.md) overview. In the following examples, we'll cover some common use cases, including: - Creating a document - Updating a document - Handling file uploads when creating or updating a document - Authenticating a user ### Creating a Document First, let's create our server function. Here are some key points for this process: - Begin by adding `'use server'` at the top of the file. - You can still use utilities such as `getPayload()`. - Once the function structure is in place, call the Local API operation `payload.create()` and pass in the relevant data. - It's good practice to wrap this in a `try...catch` block for error handling. - Finally, make sure to return the created document (don't just run the operation). ```ts 'use server' import { headers } from 'next/headers' import { getPayload } from 'payload' import config from '@payload-config' export async function createPost(data) { const payload = await getPayload({ config }) const { user } = await payload.auth({ headers: await headers() }) try { const post = await payload.create({ collection: 'posts', overrideAccess: false, user, data, }) return post } catch (error) { throw new Error(`Error creating post: ${error.message}`) } } ``` Now, let's look at how to call the `createPost` function we just created from the frontend in a React component when a user clicks a button: ```ts 'use client'; import React, { useState } from 'react'; import { createPost } from '../server/actions'; // import the server function export const PostForm: React.FC = () => { const [result, setResult] = useState(''); return ( <>

{result}

); }; ``` ### Updating a Document The key points from the previous example also apply here. To update a document instead of creating one, you would use `payload.update()` with the relevant data and **passing the document ID.** Here's how the server function would look: ```ts 'use server' import { headers } from 'next/headers' import { getPayload } from 'payload' import config from '@payload-config' export async function updatePost(id, data) { const payload = await getPayload({ config }) const { user } = await payload.auth({ headers: await headers() }) try { const post = await payload.update({ collection: 'posts', overrideAccess: false, user, id, // the document id is required data, }) return post } catch (error) { throw new Error(`Error updating post: ${error.message}`) } } ``` Here is how you would call the `updatePost` function from a frontend React component: ```ts 'use client'; import React, { useState } from 'react'; import { updatePost } from '../server/actions'; // import the server function export const UpdatePostForm: React.FC = () => { const [result, setResult] = useState(''); return ( <>

{result}

); }; ``` ### Authenticating a User In this example, we will check if a user is authenticated using Payload's authentication system. Here's how it works: - First, we use the headers function from `next/headers` to retrieve the request headers. On TanStack Start, get them from the request instead: `const headers = new Headers(getRequest().headers)`, where `getRequest` is imported from `@tanstack/react-start/server`. - Next, we pass these headers to `payload.auth()` to fetch the user's authentication details. - If the user is authenticated, their information is returned. If not, handle the unauthenticated case accordingly. Here's the server function to authenticate a user: ```ts 'use server' import { headers as getHeaders } from 'next/headers' import config from '@payload-config' import { getPayload } from 'payload' export const authenticateUser = async () => { const payload = await getPayload({ config }) const headers = await getHeaders() const { user } = await payload.auth({ headers }) if (user) { return { hello: user.email } } return { hello: 'Not authenticated' } } ``` Here's a basic example of how to call the authentication server function from the frontend to test it: ```ts 'use client'; import React, { useState } from 'react'; import { authenticateUser } from '../server/actions'; // Import the server function export const AuthComponent: React.FC = () => { const [userInfo, setUserInfo] = useState(''); return (

{userInfo}

); }; ``` ### Creating a Document with File Upload This example demonstrates how to write a server function that creates a document with a file upload. Here are the key steps: - Pass two arguments: **data** for the document content and **upload** for the file - Merge the upload file into the document data as the media field - Use `payload.create()` to create a new post document with both the document data and file ```ts 'use server' import { headers } from 'next/headers' import { getPayload } from 'payload' import config from '@payload-config' export async function createPostWithUpload(data, upload) { const payload = await getPayload({ config }) const { user } = await payload.auth({ headers: await headers() }) try { // Prepare the data with the file const postData = { ...data, media: upload, } const post = await payload.create({ collection: 'posts', overrideAccess: false, user, data: postData, }) return post } catch (error) { throw new Error(`Error creating post: ${error.message}`) } } ``` Here is how you would use the server function we just created in a frontend component to allow users to submit a post along with a file upload: - The user enters the post title and selects a file to upload. - When the form is submitted, the `handleSubmit` function checks if a file has been chosen. - If a file is selected, it passes both the title and the file to the `createPostWithFile` server function. - And you are done! ```ts 'use client'; import React, { useState } from 'react'; import { createPostWithUpload } from '../server/actions'; export const PostForm: React.FC = () => { const [title, setTitle] = useState(''); const [file, setFile] = useState(null); const [result, setResult] = useState(''); const handleFileChange = (e: React.ChangeEvent) => { if (e.target.files) { setFile(e.target.files[0]); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!file) { setResult('Please upload a file.'); return; } try { // Call the server function to create the post with the file const newPost = await createPostWithUpload({ title }, file); setResult('Post created with file: ' + newPost.title); } catch (error) { setResult('Error: ' + error.message); } }; return (
setTitle(e.target.value)} placeholder="Post Title" />

{result}

); }; ``` > **Note** > > Payload also provides reusable server functions for authentication. See > [Authentication Server Functions](/docs/v4/authentication/server-functions.md) for > Next.js and TanStack Start examples. ## Error Handling in Server Functions When using server functions, proper error handling is essential to prevent unhandled exceptions and provide meaningful feedback to the frontend. ### Best Practices#error-handling-best-practices - Wrap Local API calls in **try/catch blocks** to catch potential errors. - **Log errors** on the server for debugging purposes. - Return structured **error responses** instead of exposing raw errors to the frontend. Example of good error handling: ```ts export async function createPost(data) { try { const payload = await getPayload({ config }) const { user } = await payload.auth({ headers: await headers() }) return await payload.create({ collection: 'posts', data, overrideAccess: false, user, }) } catch (error) { console.error('Error creating post:', error) return { error: 'Failed to create post' } } } ``` ## Security Considerations Using server functions helps prevent direct exposure of Local API operations to the frontend, but additional security best practices should be followed: ### Best Practices#security-best-practices - **Restrict access**: Ensure that sensitive actions (like user management) are only callable by authorized users. - **Avoid passing sensitive data**: Do not return sensitive information such as user data, passwords, etc. - **Use authentication & authorization**: Check user roles before performing actions. Example of restricting access based on user role: ```ts import { headers } from 'next/headers' import { getPayload, UnauthorizedError } from 'payload' import config from '@payload-config' export async function deletePost(postId) { const payload = await getPayload({ config }) const { user } = await payload.auth({ headers: await headers() }) if (!user || user.role !== 'admin') { throw new UnauthorizedError() } return await payload.delete({ collection: 'posts', id: postId, overrideAccess: false, user, }) } ``` # Respecting Access Control with Local API Operations Source: https://payloadcms.com/docs/beta/local-api/access-control Local API operations that support `overrideAccess` now default it to `false` — they **respect Access Control by default**. A few operations do not expose this option, and `resetPassword` requires it explicitly. - `overrideAccess: false` (default where optional) — respect Access Control. Pass the authenticated `user` when the operation acts on their behalf; without one, Payload evaluates access as an anonymous request. - `overrideAccess: true` — skip Access Control. The operation runs with full permissions. > **Warning** > > In Payload 3 this property defaulted to `true`, which meant an operation that > said nothing skipped Access Control. See the [migration > guide](/docs/v4/migration-guide/v4.md#overrideaccess-now-defaults-to-false-on-local-api-operations). ### Respecting Access Control Use `overrideAccess: false` whenever the operation acts for a person — server functions, API routes, custom endpoints, and anything reached from the front end. Pass the authenticated `user` so Payload knows whose permissions to check. ```ts const authedCreate = await payload.create({ collection: 'users', overrideAccess: false, // access control is enforced user, // whose permissions to check data: { email: 'test@test.com', password: 'test', }, }) ``` This creates the document only if `user` has permission to do so. Without the `user`, Payload treats the request as unauthenticated and applies the access rules for an anonymous visitor. ### Skipping Access Control Use `overrideAccess: true` for trusted server-side work with no user behind it — seed scripts, migrations, cron jobs, and independently authenticated system integrations that are intended to run with full permissions. Treat webhooks according to the authority they represent. A webhook acting for a Payload user should pass that `user` with `overrideAccess: false`. After independently authenticating and authorizing a system webhook, use `overrideAccess: true` only when it is intentionally allowed to bypass Payload Access Control. ```ts const seeded = await payload.create({ collection: 'users', overrideAccess: true, // access control is skipped data: { email: 'test@test.com', password: 'test', }, }) ``` ### Choosing a value Ask who the operation is for. If a person triggered it, pass their `user` — you can set `overrideAccess: false` explicitly, or simply omit the property, since that's now the default. If your server triggered it on its own schedule, set `overrideAccess: true` explicitly. When you are unsure, leave the default in place so that Payload enforces the configured Access Control instead of bypassing it. # REST API Source: https://payloadcms.com/docs/beta/rest-api/overview > **Note** > > A fully functional REST API is automatically generated from your Collection > and Global configs. The REST API is a fully functional HTTP client that allows you to interact with your Documents in a RESTful manner. It supports all CRUD operations and is equipped with automatic pagination, depth, and sorting. All Payload API routes are mounted and prefixed to your config's `routes.api` URL segment (default: `/api`). To check collection or global candidates without saving them, use the [on-demand validation endpoints](/docs/v4/validation/overview.md#rest-api). For example, if you have a Collection called `pages`, you can fetch its documents directly from the browser or any HTTP client: ```ts fetch('https://localhost:3000/api/pages') // highlight-line .then((res) => res.json()) .then((data) => console.log(data)) ``` To enhance DX, you can use [Payload SDK](#payload-rest-api-sdk) to query your REST API. **REST query parameters:** - [depth](/docs/v4/queries/depth.md) - automatically populates relationships and uploads - [locale](/docs/v4/configuration/localization.md#retrieving-localized-docs) - retrieves document(s) in a specific locale - [fallback-locale](/docs/v4/configuration/localization.md#retrieving-localized-docs) - specifies a fallback locale if no locale value exists - [select](/docs/v4/queries/select.md) - specifies which fields to include in the result - [populate](/docs/v4/queries/select.md#populate) - specifies which fields to include in the result from populated documents - [limit](/docs/v4/queries/pagination.md#options) - limits the number of documents returned - [page](/docs/v4/queries/pagination.md#options) - specifies which page to get documents from when used with a limit - [sort](/docs/v4/queries/sort.md#rest-api) - specifies the field(s) to use to sort the returned documents by - [where](/docs/v4/queries/overview.md) - specifies advanced filters to use to query documents - [joins](/docs/v4/fields/join.md#rest-api) - specifies the custom request for each join field by name of the field ## Collections Each collection is mounted using its `slug` value. For example, if a collection's slug is `users`, all corresponding routes will be mounted on `/api/users`. Note: Collection slugs must be formatted in kebab-case **All CRUD operations are exposed as follows:** ## Auth Operations Auth enabled collections are also given the following endpoints: ## Globals Globals cannot be created or deleted, so there are only two REST endpoints opened: ## Preferences In addition to the dynamically generated endpoints above Payload also has REST endpoints to manage the admin user [preferences](/docs/v4/admin/preferences.md) for data specific to the authenticated user. ## Custom Endpoints Additional REST API endpoints can be added to your application by providing an array of `endpoints` in various places within a Payload Config. Custom endpoints are useful for adding additional middleware on existing routes or for building custom functionality into Payload apps and plugins. Endpoints can be added at the top of the Payload Config, `collections`, and `globals` and accessed respective of the api and slugs you have configured. > **Warning** > > Custom endpoints are not authenticated by default. You are responsible for > securing your own endpoints. Each endpoint object needs to have: | Property | Description | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`path`** | A string for the endpoint route after the collection or globals slug | | **`method`** | The lowercase HTTP verb to use: 'get', 'head', 'post', 'put', 'delete', 'connect' or 'options' | | **`handler`** | A function that accepts **req** - `PayloadRequest` object which contains [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) properties, currently authenticated `user` and the Local API instance `payload`. | | **`root`** | When `true`, defines the endpoint on the root Next.js app, bypassing Payload handlers and the `routes.api` subpath. Note: this only applies to top-level endpoints of your Payload Config, endpoints defined on `collections` or `globals` cannot be root. | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | Example: ```ts import type { CollectionConfig } from 'payload' // a collection of 'orders' with an additional route for tracking details, reachable at /api/orders/:id/tracking export const Orders: CollectionConfig = { slug: 'orders', fields: [/* ... */], // highlight-start endpoints: [ { path: '/:id/tracking', method: 'get', handler: async (req) => { const tracking = await getTrackingInfo(req.routeParams.id) if (!tracking) { return Response.json({ error: 'not found' }, { status: 404 }) } return Response.json({ message: `Hello ${req.routeParams.name as string} @ ${req.routeParams.group as string}`, }) }, }, { path: '/:id/tracking', method: 'post', handler: async (req) => { // `data` is not automatically appended to the request // if you would like to read the body of the request // you can use `data = await req.json()` const data = await req.json() await req.payload.update({ collection: 'tracking', overrideAccess: false, req, data: { // data to update the document with }, }) return Response.json({ message: 'successfully updated tracking info', }) }, }, { path: '/:id/forbidden', method: 'post', handler: async (req) => { // this is an example of an authenticated endpoint if (!req.user) { return Response.json({ error: 'forbidden' }, { status: 403 }) } // do something return Response.json({ message: 'successfully updated tracking info', }) }, }, ], // highlight-end } ``` > **Note:** **req** will have the **payload** object and can be used inside your > endpoint handlers for making calls like req.payload.find() that will make use > of [Access Control](/docs/v4/access-control/overview.md) and > [Hooks](/docs/v4/hooks/overview.md). #### Helpful tips `req.data` Data is not automatically appended to the request. You can read the body data by calling `await req.json()`. Or you could use our helper function that mutates the request and appends data and file if found. ```ts import { addDataAndFileToRequest } from 'payload' // custom endpoint example { path: '/:id/tracking', method: 'post', handler: async (req) => { await addDataAndFileToRequest(req) await req.payload.update({ collection: 'tracking', overrideAccess: false, req, data: { // data to update the document with } }) return Response.json({ message: 'successfully updated tracking info' }) } } ``` `req.locale` & `req.fallbackLocale` The locale and the fallback locale are not automatically appended to custom endpoint requests. If you would like to add them you can use this helper function. ```ts import { addLocalesToRequestFromData } from 'payload' // custom endpoint example { path: '/:id/tracking', method: 'post', handler: async (req) => { await addLocalesToRequestFromData(req) // you now can access req.locale & req.fallbackLocale return Response.json({ message: 'success' }) } } ``` `headersWithCors` By default, custom endpoints don't handle CORS headers in responses. The `headersWithCors` function checks the Payload config and sets the appropriate CORS headers in the response accordingly. ```ts import { headersWithCors } from 'payload' // custom endpoint example { path: '/:id/tracking', method: 'post', handler: async (req) => { return Response.json( { message: 'success' }, { headers: headersWithCors({ headers: new Headers(), req, }) }, ) } } ``` ## Method Override for GET Requests Payload supports a method override feature that allows you to send GET requests using the HTTP POST method. This can be particularly useful in scenarios when the query string in a regular GET request is too long. ### How to Use To use this feature, include the `X-Payload-HTTP-Method-Override` header set to `GET` in your POST request. The parameters should be sent in the body of the request with the `Content-Type` set to `application/x-www-form-urlencoded`. ### Example Here is an example of how to use the method override to perform a GET request: #### Using Method Override (POST) ```ts const res = await fetch(`${api}/${collectionSlug}`, { method: 'POST', credentials: 'include', headers: { 'Accept-Language': i18n.language, 'Content-Type': 'application/x-www-form-urlencoded', 'X-Payload-HTTP-Method-Override': 'GET', }, body: qs.stringify({ depth: 1, locale: 'en', }), }) ``` #### Equivalent Regular GET Request ```ts const res = await fetch(`${api}/${collectionSlug}?depth=1&locale=en`, { method: 'GET', credentials: 'include', headers: { 'Accept-Language': i18n.language, }, }) ``` ### Passing as JSON When using `X-Payload-HTTP-Method-Override`, it expects the body to be a query string. If you want to pass JSON instead, you can set the `Content-Type` to `application/json` and include the JSON body in the request. #### Example ```ts const res = await fetch(`${api}/${collectionSlug}/${id}`, { // Only the findByID endpoint supports HTTP method overrides with JSON data method: 'POST', credentials: 'include', headers: { 'Accept-Language': i18n.language, 'Content-Type': 'application/json', 'X-Payload-HTTP-Method-Override': 'GET', }, body: JSON.stringify({ depth: 1, locale: 'en', }), }) ``` This can be more efficient for large JSON payloads, as you avoid converting data to and from query strings. However, only certain endpoints support this. Supported endpoints will read the parsed body under a `data` property, instead of reading from query parameters as with standard GET requests. ## Payload REST API SDK The best, fully type-safe way to query Payload REST API is to use the SDK package, which can be installed with: ```bash pnpm add @payloadcms/sdk ``` Its usage is very similar to [the Local API](/docs/v4/local-api/overview.md). > **Note:** The SDK package is currently in beta and may be subject to change in > minor versions updates prior to being stable. Example: ```ts import { PayloadSDK } from '@payloadcms/sdk' const sdk = new PayloadSDK({ baseURL: 'https://example.com/api', }) ``` For projects without a `payload-types.ts` file, or when working with multiple Payload configs, you can manually pass the types as a generic: ```ts import { PayloadSDK } from '@payloadcms/sdk' import type { Config } from './payload-types' const sdk = new PayloadSDK({ baseURL: 'https://example.com/api', }) ``` ### Operations ```ts // Find operation const posts = await sdk.find({ collection: 'posts', draft: true, limit: 10, locale: 'en', page: 1, where: { _status: { equals: 'published' } }, }) // Find by ID operation const posts = await sdk.findByID({ id, collection: 'posts', draft: true, locale: 'en', }) // Auth login operation const result = await sdk.login({ collection: 'users', data: { email: 'dev@payloadcms.com', password: '12345', }, }) // Create operation const result = await sdk.create({ collection: 'posts', data: { text: 'text' }, }) // Create operation with a file // `file` can be either a Blob | File object or a string URL const result = await sdk.create({ collection: 'media', file, data: {} }) // Count operation const result = await sdk.count({ collection: 'posts', where: { id: { equals: post.id } }, }) // Update (by ID) operation const result = await sdk.update({ collection: 'posts', id: post.id, data: { text: 'updated-text', }, }) // Update (bulk) operation const result = await sdk.update({ collection: 'posts', where: { id: { equals: post.id, }, }, data: { text: 'updated-text-bulk' }, }) // Delete (by ID) operation const result = await sdk.delete({ id: post.id, collection: 'posts' }) // Delete (bulk) operation const result = await sdk.delete({ where: { id: { equals: post.id } }, collection: 'posts', }) // Find Global operation const result = await sdk.findGlobal({ slug: 'global' }) // Update Global operation const result = await sdk.updateGlobal({ slug: 'global', data: { text: 'some-updated-global' }, }) // Auth Login operation const result = await sdk.login({ collection: 'users', data: { email: 'dev@payloadcms.com', password: '123456' }, }) // Auth Me operation const result = await sdk.me( { collection: 'users' }, { headers: { Authorization: `JWT ${user.token}`, }, }, ) // Auth Refresh Token operation const result = await sdk.refreshToken( { collection: 'users' }, { headers: { Authorization: `JWT ${user.token}` } }, ) // Auth Forgot Password operation const result = await sdk.forgotPassword({ collection: 'users', data: { email: user.email }, }) // Auth Reset Password operation const result = await sdk.resetPassword({ collection: 'users', data: { password: '1234567', token: resetPasswordToken }, }) // Find Versions operation const result = await sdk.findVersions({ collection: 'posts', where: { parent: { equals: post.id } }, }) // Find Version by ID operation const result = await sdk.findVersionByID({ collection: 'posts', id: version.id, }) // Restore Version operation const result = await sdk.restoreVersion({ collection: 'posts', id, }) // Find Global Versions operation const result = await sdk.findGlobalVersions({ slug: 'global', }) // Find Global Version by ID operation const result = await sdk.findGlobalVersionByID({ id: version.id, slug: 'global', }) // Restore Global Version operation const result = await sdk.restoreGlobalVersion({ slug: 'global', id, }) ``` Every operation has optional 3rd parameter which is used to add additional data to the RequestInit object (like headers): ```ts await sdk.me( { collection: 'users', }, { // RequestInit object headers: { Authorization: `JWT ${token}`, }, }, ) ``` To query custom endpoints, you can use the `request` method, which is used internally for all other methods: ```ts await sdk.request({ method: 'POST', path: '/send-data', json: { id: 1, }, }) ``` Custom `fetch` implementation and `baseInit` for shared `RequestInit` properties: ```ts const sdk = new PayloadSDK({ baseInit: { credentials: 'include' }, baseURL: 'https://example.com/api', fetch: async (url, init) => { console.log('before req') const response = await fetch(url, init) console.log('after req') return response }, }) ``` Example of a custom `fetch` implementation for testing the REST API without needing to spin up a next development server: ```ts import config from '@payload-config' import { REST_DELETE, REST_GET, REST_PATCH, REST_POST, REST_PUT, } from '@payloadcms/next/routes' import { PayloadSDK } from '@payloadcms/sdk' const api = { GET: REST_GET(config), POST: REST_POST(config), PATCH: REST_PATCH(config), DELETE: REST_DELETE(config), PUT: REST_PUT(config), } const awaitedConfig = await config export const sdk = new PayloadSDK({ baseURL: ``, fetch: (path: string, init: RequestInit) => { const [slugs, search] = path.slice(1).split('?') const url = `${awaitedConfig.serverURL || 'http://localhost:3000'}${awaitedConfig.routes.api}/${slugs}${search ? `?${search}` : ''}` if (init.body instanceof FormData) { const file = init.body.get('file') as Blob if (file && init.headers instanceof Headers) { init.headers.set('Content-Length', file.size.toString()) } } const request = new Request(url, init) const params = { params: Promise.resolve({ slug: slugs.split('/'), }), } return api[init.method.toUpperCase()](request, params) }, }) ``` # GraphQL Overview Source: https://payloadcms.com/docs/beta/graphql/overview In addition to its REST and Local APIs, Payload ships with a fully featured and extensible GraphQL API. By default, the GraphQL API is exposed via `/api/graphql`, but you can customize this URL via specifying your `routes` within the main Payload Config. The labels you provide for your Collections and Globals are used to name the GraphQL types that are created to correspond to your config. Special characters and spaces are removed. ## GraphQL Clients You can use any GraphQL client with Payload's GraphQL endpoint. Here are a few popular packages: - [`graphql-request`](https://www.npmjs.com/package/graphql-request) - a very lightweight GraphQL client - [`@apollo/client`](https://www.apollographql.com/docs/react/api/core/ApolloClient/) - an industry-standard GraphQL client with lots of nice features ## GraphQL Options At the top of your Payload Config you can define all the options to manage GraphQL. | Option | Description | | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `mutations` | Any custom Mutations to be added in addition to what Payload provides. [More](/docs/v4/graphql/extending.md) | | `queries` | Any custom Queries to be added in addition to what Payload provides. [More](/docs/v4/graphql/extending.md) | | `maxComplexity` | A number used to set the maximum allowed complexity allowed by requests [More](/docs/v4/graphql/overview.md#query-complexity-limits) | | `disablePlaygroundInProduction` | A boolean that if false will enable the GraphQL playground in production environments, defaults to true. [More](/docs/v4/graphql/overview.md#graphql-playground) | | `disableIntrospectionInProduction` | A boolean that if false will enable the GraphQL introspection in production environments, defaults to true. | | `disable` | A boolean that if true will disable the GraphQL entirely, defaults to false. | | `validationRules` | A function that takes the ExecutionArgs and returns an array of ValidationRules. | ## Collections Everything that can be done to a Collection via the REST or Local API can be done with GraphQL (outside of uploading files, which is REST-only). If you have a collection as follows: ```ts import type { CollectionConfig } from 'payload' export const PublicUser: CollectionConfig = { slug: 'public-users', auth: true, // Auth is enabled fields: [ ... ], } ``` **Payload will automatically open up the following queries:** | Query Name | Operation | | ------------------ | ------------------- | | `PublicUser` | `findByID` | | `PublicUsers` | `find` | | `countPublicUsers` | `count` | | `mePublicUser` | `me` auth operation | **And the following mutations:** | Query Name | Operation | | -------------------------- | ------------------------------- | | `createPublicUser` | `create` | | `updatePublicUser` | `update` | | `deletePublicUser` | `delete` | | `forgotPasswordPublicUser` | `forgotPassword` auth operation | | `resetPasswordPublicUser` | `resetPassword` auth operation | | `unlockPublicUser` | `unlock` auth operation | | `verifyPublicUser` | `verify` auth operation | | `loginPublicUser` | `login` auth operation | | `logoutPublicUser` | `logout` auth operation | | `refreshTokenPublicUser` | `refresh` auth operation | ## Globals Globals are also fully supported. For example: ```ts import type { GlobalConfig } from 'payload'; const Header: GlobalConfig = { slug: 'header', fields: [ ... ], } ``` **Payload will open the following query:** | Query Name | Operation | | ---------- | --------- | | `Header` | `findOne` | **And the following mutation:** | Query Name | Operation | | -------------- | --------- | | `updateHeader` | `update` | ## Preferences User [preferences](/docs/v4/admin/preferences.md) for the [Admin Panel](/docs/v4/admin/overview.md) are also available to GraphQL the same way as other collection schemas are generated. To query preferences you must supply an authorization token in the header and only the preferences of that user will be accessible. **Payload will open the following query:** | Query Name | Operation | | ------------ | --------- | | `Preference` | `findOne` | **And the following mutations:** | Query Name | Operation | | ------------------ | --------- | | `updatePreference` | `update` | | `deletePreference` | `delete` | ## Select Collection and global GraphQL queries accept an optional boolean `select` argument. When `select` is `true`, Payload maps your GraphQL selection set to the [Select API](/docs/v4/queries/select.md) and only queries those fields from the database. This can significantly reduce database load and response size for large documents. ```graphql query { Posts(select: true) { docs { id title slug } } } ``` Without `select: true`, GraphQL clients still receive only the fields they request, but Payload may still load the full document from the database. Enable `select` when you want database-level field projection. To learn more, including how this interacts with hooks and access control, see the [Select documentation](/docs/v4/queries/select.md#graphql-api). ## GraphQL Playground GraphQL Playground is enabled by default for development purposes, but disabled in production. You can enable it in production by passing `graphQL.disablePlaygroundInProduction` a `false` setting in the main Payload Config. You can even log in using the `login[collection-singular-label-here]` mutation to use the Playground as an authenticated user. > **Tip:** > > To see more regarding how the above queries and mutations are used, visit your GraphQL playground > (by default at > [`${SERVER_URL}/api/graphql-playground`](http://localhost:3000/api/graphql-playground)) > while your server is running. There, you can use the "Schema" and "Docs" buttons on the right to > see a ton of detail about how GraphQL operates within Payload. ## Custom Validation Rules You can add custom validation rules to your GraphQL API by defining a `validationRules` function in your Payload Config. This function should return an array of [Validation Rules](https://graphql.org/graphql-js/validation/#validation-rules) that will be applied to all incoming queries and mutations. ```ts import { GraphQL } from '@payloadcms/graphql/types' import { buildConfig } from 'payload' export default buildConfig({ // ... graphQL: { validationRules: (args) => [NoProductionIntrospection], }, // ... }) const NoProductionIntrospection: GraphQL.ValidationRule = (context) => ({ Field(node) { if (process.env.NODE_ENV === 'production') { if (node.name.value === '__schema' || node.name.value === '__type') { context.reportError( new GraphQL.GraphQLError( 'GraphQL introspection is not allowed, but the query contained __schema or __type', { nodes: [node] }, ), ) } } }, }) ``` ## Query complexity limits Payload comes with a built-in query complexity limiter to prevent bad people from trying to slow down your server by running massive queries. To learn more, [click here](/docs/v4/production/preventing-abuse.md#limiting-graphql-complexity). ## Field complexity You can define custom complexity for `relationship`, `upload` and `join` type fields. This is useful if you want to assign a higher complexity to a field that is more expensive to resolve. This can help prevent users from running queries that are too complex. ```ts const fieldWithComplexity = { name: 'authors', type: 'relationship', relationship: 'authors', graphQL: { complexity: 100, // highlight-line }, } ``` # Adding your own Queries and Mutations Source: https://payloadcms.com/docs/beta/graphql/extending You can add your own GraphQL queries and mutations to Payload, making use of all the types that Payload has defined for you. To do so, add your queries and mutations to the main Payload Config as follows: | Config Path | Description | | ------------------- | --------------------------------------------------------------------------- | | `graphQL.queries` | Function that returns an object containing keys to custom GraphQL queries | | `graphQL.mutations` | Function that returns an object containing keys to custom GraphQL mutations | The above properties each receive a function that is defined with the following arguments: **`GraphQL`** This is Payload's GraphQL dependency. You should not install your own copy of GraphQL as a dependency due to underlying restrictions based on how GraphQL works. Instead, you can use the Payload-provided copy via this argument. **`payload`** This is a copy of the currently running Payload instance, which provides you with existing GraphQL types for all of your Collections and Globals - among other things. ## Return value Both `graphQL.queries` and `graphQL.mutations` functions should return an object with properties equal to your newly written GraphQL queries and mutations. ## Example `payload.config.js`: ```ts import { buildConfig } from 'payload' import myCustomQueryResolver from './graphQL/resolvers/myCustomQueryResolver' export default buildConfig({ graphQL: { // highlight-start queries: (GraphQL, payload) => { return { MyCustomQuery: { type: new GraphQL.GraphQLObjectType({ name: 'MyCustomQuery', fields: { text: { type: GraphQL.GraphQLString, }, someNumberField: { type: GraphQL.GraphQLFloat, }, }, }), args: { argNameHere: { type: new GraphQL.GraphQLNonNull(GraphQLString), }, }, resolve: myCustomQueryResolver, }, } }, // highlight-end }, }) ``` ## Resolver function In your resolver, make sure you set `depth: 0` if you're returning data directly from the Local API so that GraphQL can correctly resolve queries to nested values such as relationship data. Your function will receive four arguments you can make use of: Example ```ts ;async (obj, args, context, info) => {} ``` **`obj`** The previous object. Not very often used and usually discarded. **`args`** The available arguments from your query or mutation will be available to you here, these must be configured via the custom operation first. **`context`** An object containing the `req` and `res` objects that will provide you with the `payload`, `user` instances and more, like any other Payload API handler. **`info`** Contextual information about the currently running GraphQL operation. You can get schema information from this as well as contextual information about where this resolver function is being run. ## Types We've exposed a few types and utilities to help you extend the API further. Payload uses the GraphQL.js package for which you can view the full list of available types in the [official documentation](https://graphql.org/graphql-js/type/). **`GraphQLJSON`** & **`GraphQLJSONObject`** ```ts import { GraphQLJSON, GraphQLJSONObject } from '@payloadcms/graphql/types' ``` **`GraphQL`** You can directly import the GraphQL package used by Payload, most useful for typing. ```ts import { GraphQL } from '@payloadcms/graphql/types' ``` > **Warning** > > For queries, mutations and handlers make sure you use the `GraphQL` and > `payload` instances provided via arguments. **`buildPaginatedListType`** This is a utility function that allows you to build a new GraphQL type for a paginated result similar to the Payload's generated schema. It takes in two arguments, the first for the name of this new schema type and the second for the GraphQL type to be used in the docs parameter. Example ```ts import { buildPaginatedListType } from '@payloadcms/graphql/types' export const getMyPosts = (GraphQL, payload) => { return { args: {}, resolve: Resolver, // The name of your new type has to be unique type: buildPaginatedListType( 'AuthorPosts', payload.collections['posts'].graphQL?.type, ), } } ``` **`payload.collections.slug.graphQL`** If you want to extend more of the provided API then the `graphQL` object on your collection slug will contain additional types to help you re-use code for types, mutations and queries. ```ts graphQL?: { type: GraphQLObjectType paginatedType: GraphQLObjectType JWT: GraphQLObjectType versionType: GraphQLObjectType whereInputType: GraphQLInputObjectType mutationInputType: GraphQLNonNull updateMutationInputType: GraphQLNonNull } ``` ## Best practices There are a few ways to structure your code, we recommend using a dedicated `graphql` directory so you can keep all of your logic in one place. You have total freedom of how you want to structure this but a common pattern is to group functions by type and with their resolver. Example ``` src/graphql ---- queries/ index.ts -- myCustomQuery/ index.ts resolver.ts ---- mutations/ ``` # GraphQL Schema Source: https://payloadcms.com/docs/beta/graphql/graphql-schema In Payload the schema is controlled by your collections and globals. All you need to do is run the generate command and the entire schema will be created for you. ## Schema generation script Install `@payloadcms/graphql` as a dev dependency: ```bash pnpm add @payloadcms/graphql -D ``` Run the following command to generate the schema: ```bash pnpm payload-graphql generate:schema ``` ## Custom Field Schemas For `array`, `block`, `group` and named `tab` fields, you can generate top level reusable interfaces. The following group field config: ```ts { type: 'group', name: 'meta', interfaceName: 'SharedMeta', // highlight-line fields: [ { name: 'title', type: 'text', }, { name: 'description', type: 'text', }, ], } ``` will generate: ```ts // A top level reusable type will be generated type SharedMeta { title: String description: String } // And will be referenced inside the generated schema type Collection1 { // ...other fields meta: SharedMeta } ``` The above example outputs all your definitions to a file relative from your Payload config as `./graphql/schema.graphql`. By default, the file will be output to your current working directory as `schema.graphql`. ### Adding an npm script > **Warning** > > **Important** > > Payload needs to be able to find your config to generate your GraphQL schema. Payload will automatically try and locate your config, but might not always be able to find it. For example, if you are working in a `/src` directory or similar, you need to tell Payload where to find your config manually by using an environment variable. If this applies to you, create an npm script to make generating types easier: ```json // package.json { "scripts": { "generate:graphQLSchema": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload-graphql generate:schema" } } ``` Now you can run `pnpm generate:graphQLSchema` to easily generate your schema. # Querying your Documents Source: https://payloadcms.com/docs/beta/queries/overview In Payload, "querying" means filtering or searching through Documents within a [Collection](/docs/v4/configuration/collections.md). The querying language in Payload is designed to be simple and powerful, allowing you to filter Documents with extreme precision through an intuitive and standardized structure. Payload provides three common APIs for querying your data: - [Local API](/docs/v4/local-api/overview.md) - Extremely fast, direct-to-database access - [REST API](/docs/v4/rest-api/overview.md) - Standard HTTP endpoints for querying and mutating data - [GraphQL](/docs/v4/graphql/overview.md) - A full GraphQL API with a GraphQL Playground Each of these APIs share the same underlying querying language, and fully support all of the same features. This means that you can learn Payload's querying language once, and use it across any of the APIs that you might use. To query your Documents, you can send any number of [Operators](#operators) through your request: ```ts import type { Where } from 'payload' const query: Where = { color: { equals: 'blue', }, } ``` _The exact query syntax will depend on the API you are using, but the concepts are the same across all APIs. [More details](#writing-queries)._ > **Tip:** You can also use queries within [Access > Control](/docs/v4/access-control/overview.md) functions. ## Operators The following operators are available for use in queries: | Operator | Description | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `equals` | The value must be exactly equal. With a nested query on a non-polymorphic `hasMany` relationship, every related document must match. | | `not_equals` | The query will return all documents where the value is not equal. With a nested query on a non-polymorphic `hasMany` relationship, no related document may match. | | `greater_than` | For numeric or date-based fields. | | `greater_than_equal` | For numeric or date-based fields. | | `less_than` | For numeric or date-based fields. | | `less_than_equal` | For numeric or date-based fields. | | `like` | Case-insensitive string must be present. If string of words, all words must be present, in any order. | | `contains` | Must contain the value entered, case-insensitive. With a nested query on a non-polymorphic `hasMany` relationship, at least one related document must match. | | `in` | The value must be found within the provided comma-delimited list of values. | | `not_in` | The value must NOT be within the provided comma-delimited list of values. | | `all` | The value must contain all values provided in the comma-delimited list. Note: currently this operator is supported only with the MongoDB adapter. | | `exists` | Only return documents where the value either exists (`true`) or does not exist (`false`). | | `near` | For distance related to a [Point Field](/docs/v4/fields/point.md) comma separated as `, , , `. | | `within` | For [Point Fields](/docs/v4/fields/point.md) to filter documents based on whether points are inside of the given area defined in GeoJSON. [Example](/docs/v4/fields/point.md#querying-within) | | `intersects` | For [Point Fields](/docs/v4/fields/point.md) to filter documents based on whether points intersect with the given area defined in GeoJSON. [Example](/docs/v4/fields/point.md#querying-intersects) | > **Tip:** If you know your users will be querying on certain fields a lot, add > `index: true` to the Field Config. This will speed up searches using that > field immensely. [More details](/docs/v4/database/indexes.md). ### And / Or Logic In addition to defining simple queries, you can join multiple queries together using AND / OR logic. These can be nested as deeply as you need to create complex queries. To join queries, use the `and` or `or` keys in your query object: ```ts import type { Where } from 'payload' const query: Where = { or: [ // highlight-line { color: { equals: 'mint', }, }, { and: [ // highlight-line { color: { equals: 'white', }, }, { featured: { equals: false, }, }, ], }, ], } ``` Written in plain English, if the above query were passed to a `find` operation, it would translate to finding posts where either the `color` is `mint` OR the `color` is `white` AND `featured` is set to false. ## Relationship queries Relationship fields can be queried through properties on their related documents. You can also apply a nested query to a non-polymorphic `hasMany` relationship or upload field. ### Nested relationship properties Use dot notation to query a field on a related document. For example, if a `Song` collection has an `artists` relationship field, you can query the `featured` field from the related `Artists` collection like this: ```ts import type { Where } from 'payload' const query: Where = { // `featured` is the nested property name to filter on 'artists.featured': { exists: true, }, } ``` For a `hasMany` relationship, a dot-notation query matches when at least one related document matches. For example, `'ingredients.isHealthy': { not_equals: false }` matches a burger when at least one ingredient has an `isHealthy` value other than `false`. A burger with healthy lettuce and an unhealthy sauce still matches because of the lettuce. Use the nested relationship operators below when you need to control how many related documents must match. ### Has-many relationship operators For non-polymorphic `hasMany` relationship and upload fields, pass a nested query to `contains`, `not_equals`, or `equals` to control how many related documents must match. These examples assume that `burgers.ingredients` is a `hasMany` relationship and that each ingredient has an `isHealthy` field: ```ts import type { Where } from 'payload' // At least one ingredient is healthy. const hasHealthyIngredient: Where = { ingredients: { contains: { isHealthy: { equals: true }, }, }, } // No ingredient is unhealthy. const hasNoUnhealthyIngredient: Where = { ingredients: { not_equals: { isHealthy: { equals: false }, }, }, } // Every ingredient is healthy. const hasOnlyHealthyIngredients: Where = { ingredients: { equals: { isHealthy: { equals: true }, }, }, } ``` | Operator | Description | | ------------ | ---------------------------------------------------------- | | `contains` | At least one related document must match the nested query. | | `not_equals` | No related document may match the nested query. | | `equals` | Every related document must match the nested query. | The value passed to the relationship operator is a normal Payload `where` query. It supports field operators, deeper relationship queries, and the same `and` and `or` groups as any other Payload query. All conditions inside one operator are evaluated against the same related document: ```ts const burgers = await payload.find({ collection: 'burgers', overrideAccess: true, where: { ingredients: { contains: { and: [ { isHealthy: { equals: true } }, { or: [ { name: { equals: 'lettuce' } }, { name: { equals: 'tomato' } }, ], }, ], }, }, }, }) ``` This finds burgers with at least one healthy ingredient named either `lettuce` or `tomato`. The conditions cannot be satisfied by different ingredients. An empty relationship does not match a nested `contains` query, but it does match nested `not_equals` and `equals` queries, just like JavaScript's `some` and `every` methods. ## Writing Queries Writing queries in Payload is simple and consistent across all APIs, with only minor differences in syntax between them. ### Local API The [Local API](/docs/v4/local-api/overview.md) supports the `find` operation that accepts a raw query object: ```ts import type { Payload } from 'payload' const getPosts = async (payload: Payload) => { const posts = await payload.find({ collection: 'posts', overrideAccess: true, where: { color: { equals: 'mint', }, }, }) return posts } ``` ### GraphQL API All `find` queries in the [GraphQL API](/docs/v4/graphql/overview.md) support the `where` argument that accepts a raw query object: ```ts query { Posts(where: { color: { equals: mint } }) { docs { color } totalDocs } } ``` ### REST API With the [REST API](/docs/v4/rest-api/overview.md), you can use the full power of Payload queries, but they are written as query strings instead: **`https://localhost:3000/api/posts?where[color][equals]=mint`** To understand the syntax, you need to understand that complex URL search strings are parsed into a JSON object. This one isn't too bad, but more complex queries get unavoidably more difficult to write. For this reason, we recommend to use the extremely helpful and ubiquitous [`qs-esm`](https://www.npmjs.com/package/qs-esm) package to parse your JSON / object-formatted queries into query strings: ```ts import { stringify } from 'qs-esm' import type { Where } from 'payload' const query: Where = { color: { equals: 'mint', }, // This query could be much more complex // and qs-esm would handle it beautifully } const getPosts = async () => { const stringifiedQuery = stringify( { where: query, // ensure that `qs-esm` adds the `where` property, too! }, { addQueryPrefix: true }, ) const response = await fetch( `http://localhost:3000/api/posts${stringifiedQuery}`, ) // Continue to handle the response below... } ``` ## Performance There are several ways to optimize your queries. Many of these options directly impact overall database overhead, response sizes, and/or computational load and can significantly improve performance. When building queries, combine as many of these strategies together as possible to ensure your queries are as performant as they can be. > **Success** > > For more performance tips, see the [Performance > documentation](/docs/v4/performance/overview.md). ### Indexes Build [Indexes](/docs/v4/database/indexes.md) for fields that are often queried or sorted by. When your query runs, the database will not search the entire document to find that one field, but will instead use the index to quickly locate the data. This is done by adding `index: true` to the Field Config for that field: ```ts // In your collection configuration { name: 'posts', fields: [ { name: 'title', type: 'text', // highlight-start index: true, // Add an index to the title field // highlight-end }, // Other fields... ], } ``` To learn more, see the [Indexes documentation](/docs/v4/database/indexes.md). ### Depth Set the [Depth](/docs/v4/queries/depth.md) to only the level that you need to avoid populating unnecessary related documents. Relationships will only populate down to the specified depth, and any relationships beyond that depth will only return the ID of the related document. ```ts const posts = await payload.find({ collection: 'posts', overrideAccess: true, where: { ... }, // highlight-start depth: 0, // Only return the IDs of related documents // highlight-end }) ``` To learn more, see the [Depth documentation](/docs/v4/queries/depth.md). ### Limit Set the [Limit](/docs/v4/queries/pagination.md#limit) if you can reliably predict the number of matched documents, such as when querying on a unique field. ```ts const posts = await payload.find({ collection: 'posts', overrideAccess: true, where: { slug: { equals: 'unique-post-slug', }, }, // highlight-start limit: 1, // Only expect one document to be returned // highlight-end }) ``` > **Tip:** Use in combination with `pagination: false` for best performance when > querying by unique fields. To learn more, see the [Limit documentation](/docs/v4/queries/pagination.md#limit). ### Select Use the [Select API](/docs/v4/queries/select.md) to only process and return the fields you need. This will reduce the amount of data returned from the request, and also skip processing of any fields that are not selected, such as running their field hooks. ```ts const posts = await payload.find({ collection: 'posts', overrideAccess: true, where: { ... }, // highlight-start select: { title: true, }, // highlight-end }) ``` This is a basic example, but there are many ways to use the Select API, including selecting specific fields, excluding fields, etc. To learn more, see the [Select documentation](/docs/v4/queries/select.md). ### Pagination [Disable Pagination](/docs/v4/queries/pagination.md#disabling-pagination) if you can reliably predict the number of matched documents, such as when querying on a unique field. ```ts const posts = await payload.find({ collection: 'posts', overrideAccess: true, where: { slug: { equals: 'unique-post-slug', }, }, // highlight-start pagination: false, // Return all matched documents without pagination // highlight-end }) ``` > **Tip:** Use in combination with `limit: 1` for best performance when querying > by unique fields. To learn more, see the [Pagination documentation](/docs/v4/queries/pagination.md). # Sort Source: https://payloadcms.com/docs/beta/queries/sort Documents in Payload can be easily sorted by a specific [Field](/docs/v4/fields/overview.md). When querying Documents, you can pass the name of any top-level field, and the response will sort the Documents by that field in _ascending_ order. If prefixed with a minus symbol ("-"), they will be sorted in _descending_ order. In Local API multiple fields can be specified by using an array of strings. In REST API multiple fields can be specified by separating fields with comma. The minus symbol can be in front of individual fields. Because sorting is handled by the database, the field cannot be a [Virtual Field](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) unless it's [linked with a relationship field](/docs/v4/fields/overview.md#string-path-virtual-fields). It must be stored in the database to be searchable. > **Tip:** For performance reasons, it is recommended to enable `index: true` > for the fields that will be sorted upon. [More details](/docs/v4/database/indexes.md). ## Local API To sort Documents in the [Local API](/docs/v4/local-api/overview.md), you can use the `sort` option in your query: ```ts import type { Payload } from 'payload' const getPosts = async (payload: Payload) => { const posts = await payload.find({ collection: 'posts', overrideAccess: true, sort: '-createdAt', // highlight-line }) return posts } ``` To sort by multiple fields, you can use the `sort` option with fields in an array: ```ts import type { Payload } from 'payload' const getPosts = async (payload: Payload) => { const posts = await payload.find({ collection: 'posts', overrideAccess: true, sort: ['priority', '-createdAt'], // highlight-line }) return posts } ``` ## REST API To sort in the [REST API](/docs/v4/rest-api/overview.md), you can use the `sort` parameter in your query: ```ts fetch('https://localhost:3000/api/posts?sort=-createdAt') // highlight-line .then((response) => response.json()) .then((data) => console.log(data)) ``` To sort by multiple fields, you can use the `sort` parameter with fields separated by comma: ```ts fetch('https://localhost:3000/api/posts?sort=priority,-createdAt') // highlight-line .then((response) => response.json()) .then((data) => console.log(data)) ``` You can also pass `sort` as an array so each item is a single field when using query string builder: ```ts import { stringify } from 'qs-esm' const getPosts = async () => { const stringifiedQuery = stringify( { sort: ['priority', '-createdAt'], }, { addQueryPrefix: true }, ) const response = await fetch( `https://localhost:3000/api/posts${stringifiedQuery}`, // highlight-line ) const data = await response.json() return data } ``` ## GraphQL API To sort in the [GraphQL API](/docs/v4/graphql/overview.md), you can use the `sort` parameter in your query: ```ts query { Posts(sort: "-createdAt") { docs { color } } } ``` To sort by multiple fields in GraphQL, pass a comma-separated list: ```ts query { Posts(sort: "priority,-createdAt") { docs { color } } } ``` # Depth Source: https://payloadcms.com/docs/beta/queries/depth Documents in Payload can have relationships to other Documents. This is true for both [Collections](/docs/v4/configuration/collections.md) as well as [Globals](/docs/v4/configuration/globals.md). When you query a Document, you can specify the depth at which to populate any of its related Documents either as full objects, or only their IDs. Since Documents can be infinitely nested or recursively related, it's important to be able to control how deep your API populates. Depth can impact the performance of your queries by affecting the load on the database and the size of the response. For example, when you specify a `depth` of `0`, the API response might look like this: ```json { "id": "5ae8f9bde69e394e717c8832", "title": "This is a great post", "author": "5f7dd05cd50d4005f8bcab17" } ``` But with a `depth` of `1`, the response might look like this: ```json { "id": "5ae8f9bde69e394e717c8832", "title": "This is a great post", "author": { "id": "5f7dd05cd50d4005f8bcab17", "name": "John Doe" } } ``` > **Important:** Depth has no effect in the [GraphQL API](/docs/v4/graphql/overview.md), > because there, depth is based on the shape of your queries. ## Local API To specify depth in the [Local API](/docs/v4/local-api/overview.md), you can use the `depth` option in your query: ```ts import type { Payload } from 'payload' const getPosts = async (payload: Payload) => { const posts = await payload.find({ collection: 'posts', overrideAccess: true, // highlight-start depth: 2, // highlight-end }) return posts } ``` > **Reminder:** This is the same for [Globals](/docs/v4/configuration/globals.md) using > the `findGlobal` operation. ## REST API To specify depth in the [REST API](/docs/v4/rest-api/overview.md), you can use the `depth` parameter in your query: ```ts // highlight-start fetch('https://localhost:3000/api/posts?depth=2') // highlight-end .then((res) => res.json()) .then((data) => console.log(data)) ``` > **Reminder:** This is the same for [Globals](/docs/v4/configuration/globals.md) using > the `/api/globals` endpoint. ## Default Depth If no depth is specified in the request, Payload will use its default depth for all requests. By default, this is set to `1`. To change the default depth on the application level, you can use the `defaultDepth` option in your root Payload config: ```ts import { buildConfig } from 'payload/config' export default buildConfig({ // ... // highlight-start defaultDepth: 2, // highlight-end // ... }) ``` ## Max Depth Fields like the [Relationship Field](/docs/v4/fields/relationship.md) or the [Upload Field](/docs/v4/fields/upload.md) can also set a maximum depth. If exceeded, this will limit the population depth regardless of what the depth might be on the request. To set a max depth for a field, use the `maxDepth` property in your field configuration: ```js { slug: 'posts', fields: [ { name: 'author', type: 'relationship', relationTo: 'users', // highlight-start maxDepth: 2, // highlight-end } ] } ``` # Select Source: https://payloadcms.com/docs/beta/queries/select By default, Payload's APIs will return _all fields_ for a given collection or global. But, you may not need all of that data for all of your queries. Sometimes, you might want just a few fields from the response. With the Select API, you can define exactly which fields you'd like to retrieve. This can impact the performance of your queries by affecting the load on the database and the size of the response. ## Local API To specify `select` in the [Local API](/docs/v4/local-api/overview.md), you can use the `select` option in your query: ```ts import type { Payload } from 'payload' // Include mode - result type will only contain: id, text, group.number, and array const getPosts = async (payload: Payload) => { const posts = await payload.find({ collection: 'posts', overrideAccess: true, // highlight-start select: { text: true, // select a specific field from group group: { number: true, }, // select all fields from array array: true, }, // highlight-end }) return posts } // Exclude mode - result type will contain all fields except: array and group.number const getPosts = async (payload: Payload) => { const posts = await payload.find({ collection: 'posts', overrideAccess: true, // Select everything except for array and group.number // highlight-start select: { array: false, group: { number: false, }, }, // highlight-end }) return posts } ``` ### Empty select The `id` field is always included in the result, regardless of your `select` query. If you pass an empty `select` object, only the `id` will be returned: ```ts const post = await payload.findByID({ collection: 'posts', overrideAccess: true, id: '1', select: {}, }) console.log(post) // { id: '1' } ``` > **Important:** To perform querying with `select` efficiently, Payload > implements your `select` query on the database level. Because of that, your > `beforeRead` and `afterRead` hooks may not receive the full `doc`. To ensure > that some fields are always selected for your hooks / access control, > regardless of the `select` query you can use the [entity-level > `select`](#entity-level-select-config) function on your Collection or Global > config. ## Entity-level select config Both Collections and Globals accept a top-level `select` function that runs before every read and lets you transform the caller's `select` based on request context. It receives `{ operation, req, select }` and returns the final `select` to apply, replacing the caller's. Return `undefined` to leave the caller's `select` untouched. Common uses: - Force a field to always be populated so it's available in hooks / access control. - Differentiate API requests from admin panel requests to optimize payload size. ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', // Always include `title`, regardless of the caller's `select`. select: ({ select }) => (select ? { ...select, title: true } : undefined), fields: [ // ... ], } ``` The function form **replaces** the caller's select rather than deep-merging, so spread the caller's `select` into your returned object when you want to add to it instead of replacing it. Per-document data is not available — this runs before the read. ## REST API To specify select in the [REST API](/docs/v4/rest-api/overview.md), you can use the `select` parameter in your query: ```ts fetch( // highlight-start 'https://localhost:3000/api/posts?select[color]=true&select[group][number]=true', // highlight-end ) .then((res) => res.json()) .then((data) => console.log(data)) ``` To understand the syntax, you need to understand that complex URL search strings are parsed into a JSON object. This one isn't too bad, but more complex queries get unavoidably more difficult to write. For this reason, we recommend to use the extremely helpful and ubiquitous [`qs-esm`](https://www.npmjs.com/package/qs-esm) package to parse your JSON / object-formatted queries into query strings: ```ts import { stringify } from 'qs-esm' import type { Where } from 'payload' const select: Where = { text: true, group: { number: true, }, // This query could be much more complex // and QS would handle it beautifully } const getPosts = async () => { const stringifiedQuery = stringify( { select, // ensure that `qs` adds the `select` property, too! }, { addQueryPrefix: true }, ) const response = await fetch( `http://localhost:3000/api/posts${stringifiedQuery}`, ) // Continue to handle the response below... } ``` > **Reminder:** This is the same for [Globals](/docs/v4/configuration/globals.md) using > the `/api/globals` endpoint. ## GraphQL API In the [GraphQL API](/docs/v4/graphql/overview.md), `select` is a **boolean** argument on collection and global queries (including version queries). When set to `true`, Payload builds a [Select](/docs/v4/queries/select.md) projection from your GraphQL selection set and only loads those fields from the database. Unlike the Local and REST APIs, you do not pass a field map. The fields you request in the GraphQL query become the select projection: ```graphql query { # highlight-start Posts(select: true) { # highlight-end docs { id text group { number } } } } ``` The same argument works on singular document queries and globals: ```graphql query { Post(id: "123", select: true) { id text } } query { Header(select: true) { title } } ``` Without `select: true`, GraphQL still returns only the fields in your selection set over the wire, but Payload may load the full document from the database. Passing `select: true` applies the Select API at the database level for better performance. > **Important:** Because `select` is applied on the database level, your > `beforeRead` and `afterRead` hooks may not receive the full `doc`. To ensure > that some fields are always selected for your hooks / access control, use the > [entity-level `select`](#entity-level-select-config) function on your > Collection or Global config. ## defaultPopulate collection config property The `defaultPopulate` property allows you specify which fields to select when populating the collection from another document. This is especially useful for links where only the `slug` is needed instead of the entire document. With this feature, you can dramatically reduce the amount of JSON that is populated from [Relationship](/docs/v4/fields/relationship.md) or [Upload](/docs/v4/fields/upload.md) fields. For example, in your content model, you might have a `Link` field which links out to a different page. When you go to retrieve these links, you really only need the `slug` of the page. Loading all of the page content, its related links, and everything else is going to be overkill and will bog down your Payload APIs. Instead, you can define the `defaultPopulate` property on your `Pages` collection, so that when Payload "populates" a related Page, it only selects the `slug` field and therefore returns significantly less JSON: ```ts import type { CollectionConfig } from 'payload' // The TSlug generic can be passed to have type safety for `defaultPopulate`. // If avoided, the `defaultPopulate` type resolves to `SelectType`. export const Pages: CollectionConfig<'pages'> = { slug: 'pages', // Specify `select`. defaultPopulate: { slug: true, }, fields: [ { name: 'slug', type: 'text', required: true, }, ], } ``` > **Important:** When using `defaultPopulate` on a collection with > [Uploads](/docs/v4/fields/upload.md) enabled and you want to select the `url` field, > it is important to specify `filename: true` as well, otherwise Payload will > not be able to construct the correct file URL, instead returning `url: null`. ## Populate Setting `defaultPopulate` will enforce that each time Payload performs a "population" of a related document, only the fields specified will be queried and returned. However, you can override `defaultPopulate` with the `populate` property in the Local and REST API: **Local API:** ```ts import type { Payload } from 'payload' const getPosts = async (payload: Payload) => { const posts = await payload.find({ collection: 'posts', overrideAccess: true, populate: { // Select only `text` from populated docs in the "pages" collection // Now, no matter what the `defaultPopulate` is set to on the "pages" collection, // it will be overridden, and the `text` field will be returned instead. pages: { text: true, }, // highlight-line }, }) return posts } ``` **REST API:** ```ts fetch('https://localhost:3000/api/posts?populate[pages][text]=true') // highlight-line .then((res) => res.json()) .then((data) => console.log(data)) ``` # Pagination Source: https://payloadcms.com/docs/beta/queries/pagination With Pagination you can limit the number of documents returned per page, and get a specific page of results. This is useful for creating paginated lists of documents within your application. All paginated responses include documents nested within a `docs` array, and return top-level meta data related to pagination such as `totalDocs`, `limit`, `totalPages`, `page`, and more. > **Note:** Collection `find` queries are paginated automatically. ## Options All Payload APIs support the pagination controls below. With them, you can create paginated lists of documents within your application: | Control | Default | Description | | ------------ | ------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | `limit` | `10` when pagination is enabled | Limits the number of documents returned per page. [More details](#limit). | | `pagination` | `true` | Set to `false` to disable pagination calculations and document count queries. This respects any specified limit. | | `page` | `1` | Get a specific page number. | ## Local API To specify pagination controls in the [Local API](/docs/v4/local-api/overview.md), you can use the `limit`, `page`, and `pagination` options in your query: ```ts import type { Payload } from 'payload' const getPosts = async (payload: Payload) => { const posts = await payload.find({ collection: 'posts', overrideAccess: true, // highlight-start limit: 10, page: 2, // highlight-end }) return posts } ``` ## REST API With the [REST API](/docs/v4/rest-api/overview.md), you can use the pagination controls below as query strings: ```ts // highlight-start fetch('https://localhost:3000/api/posts?limit=10&page=2') // highlight-end .then((res) => res.json()) .then((data) => console.log(data)) ``` ## Response All paginated responses include documents nested within a `docs` array, and return top-level meta data related to pagination. The `find` operation includes the following properties in its response: | Property | Description | | --------------- | --------------------------------------------------------- | | `docs` | Array of documents in the collection | | `totalDocs` | Total available documents within the collection | | `limit` | Limit query parameter - defaults to `10` | | `totalPages` | Total pages available, based upon the `limit` queried for | | `page` | Current page number | | `pagingCounter` | `number` of the first doc on the current page | | `hasPrevPage` | `true/false` if previous page exists | | `hasNextPage` | `true/false` if next page exists | | `prevPage` | `number` of previous page, `null` if it doesn't exist | | `nextPage` | `number` of next page, `null` if it doesn't exist | **Example response:** ```json { // Document Array // highlight-line "docs": [ { "title": "Page Title", "description": "Some description text", "priority": 1, "createdAt": "2020-10-17T01:19:29.858Z", "updatedAt": "2020-10-17T01:19:29.858Z", "id": "5f8a46a1dd05db75c3c64760" } ], // Metadata // highlight-line "totalDocs": 6, "limit": 1, "totalPages": 6, "page": 1, "pagingCounter": 1, "hasPrevPage": false, "hasNextPage": true, "prevPage": null, "nextPage": 2 } ``` ## Limit You can specify a `limit` to restrict the number of documents returned per page. > **Reminder:** By default, any query with `limit: 0` will automatically > [disable pagination](#disabling-pagination). #### Performance benefits If you are querying for a specific document and can reliably expect only one document to match, you can set a limit of `1` (or another low number) to reduce the number of database lookups and improve performance. For example, when querying a document by a unique field such as `slug`, you can set the limit to `1` since you know there will only be one document with that slug. To do this, set the `limit` option in your query: ```ts await payload.find({ collection: 'posts', overrideAccess: true, where: { slug: { equals: 'post-1', }, }, // highlight-start limit: 1, // highlight-end }) ``` ## Disabling pagination Disabling pagination can improve performance by reducing the overhead of pagination calculations and improve query speed. For `find` operations within the Local API, you can disable pagination calculations and document count queries by passing `pagination: false` to the `find` local operation. If you do not specify a `limit`, Payload returns all matching documents. If you specify a positive `limit`, Payload still applies that limit. You can also set `limit: 0` to return all matching documents and disable pagination automatically. To do this, set `pagination: false` in your query: ```ts import type { Payload } from 'payload' const getPost = async (payload: Payload) => { const posts = await payload.find({ collection: 'posts', overrideAccess: true, where: { title: { equals: 'My Post' }, }, // highlight-start pagination: false, // highlight-end }) return posts } ``` # The Admin Panel Source: https://payloadcms.com/docs/beta/admin/overview Payload dynamically generates a beautiful, [fully type-safe](/docs/v4/typescript/overview.md) Admin Panel to manage your users and data. It is highly performant, even with 100+ fields, and is translated in over 30 languages. Within the Admin Panel you can manage content, [render your site](/docs/v4/live-preview/overview.md), [preview drafts](/docs/v4/admin/preview.md), [diff versions](/docs/v4/versions/overview.md), and so much more. The Admin Panel is designed to [white-label your brand](https://payloadcms.com/blog/white-label-admin-ui). You can endlessly customize and extend the Admin Panel UI by swapping in your own [Custom Components](/docs/v4/custom-components/overview.md)—everything from simple field labels to entire views can be modified or replaced to perfectly tailor the interface for your editors. The Admin Panel is written in [TypeScript](https://www.typescriptlang.org) and built with [React](https://react.dev) with your choice of [Next.js App Router](https://nextjs.org/docs/app) or [TanStack Start](https://tanstack.com/start/latest). It supports [React Server Components](https://react.dev/reference/rsc/server-components), enabling the use of the [Local API](/docs/v4/local-api/overview.md) on the front-end. You can install Payload into your [existing app in just one line](/docs/v4/getting-started/installation.md) and [deploy it anywhere](/docs/v4/production/deployment.md). > **Success** > > The Payload Admin Panel is designed to be as minimal and straightforward as > possible to allow easy customization and control. [Learn > more](/docs/v4/custom-components/overview.md). ![Admin Panel with collapsible sidebar](https://payloadcms.com/images/docs/admin.jpg) _Redesigned Admin Panel with a collapsible sidebar that's open by default, providing greater extensibility and enhanced horizontal real estate._ ## Project Structure The Admin Panel serves as the entire HTTP layer for Payload, providing a full CRUD interface for your app. This means that both the [REST](/docs/v4/rest-api/overview.md) and [GraphQL](/docs/v4/graphql/overview.md) APIs are routes that exist directly alongside your front-end application. Once you [install Payload](/docs/v4/getting-started/installation.md), the following files and directories will be created in your app: ### Next.js ```plaintext app ├─ (payload) ├── admin ├─── [[...segments]] ├──── page.tsx ├──── not-found.tsx ├── api ├─── [...slug] ├──── route.ts ├── graphql ├──── route.ts ├── graphql-playground ├──── route.ts ├── custom.css ├── layout.tsx ``` > **Note** > > If you are not familiar with Next.js project structure, you can [learn more > about it here](https://nextjs.org/docs/getting-started/project-structure). Payload integrates with Next.js through a small set of helpers exported from `@payloadcms/next`. The `(payload)` [route group](https://nextjs.org/docs/app/building-your-application/routing/route-groups) creates a boundary that scopes all Admin Panel layouts and styles. The `layout.tsx` file within this directory, for example, is where Payload manages the `html` tag of the document to set proper [`lang`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang) and [`dir`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/dir) attributes, etc. The `admin` directory contains all the _pages_ related to the interface itself, whereas the `api` and `graphql` directories contain all the _routes_ related to the [REST API](/docs/v4/rest-api/overview.md) and [GraphQL API](/docs/v4/graphql/overview.md). All admin routes are [easily configurable](#customizing-routes) to meet your application's exact requirements. > **Tip:** The `(payload)` folder location is flexible. You can move it to match > your application's structure by updating the `routes` and `admin.importMap` > configuration. See [Customizing Admin Panel Location](/docs/v4/admin/admin-panel-location.md) > for step-by-step instructions. ### TanStack Start ```plaintext src ├─ router.tsx ├─ routeTree.gen.ts └─ app ├─ __root.tsx ├─ _payload.tsx └─ _payload ├─ admin.$.tsx ├─ admin.index.tsx ├─ api.$.ts ├─ custom.css ├─ server.functions.ts └─ importMap.js ``` > **Note** > > If you are not familiar with TanStack Start project structure, you can [learn > more about it > here](https://tanstack.com/start/latest/docs/framework/react/build-from-scratch). Payload integrates with TanStack Start through a small set of helpers exported from `@payloadcms/tanstack-start`. Rather than a Next.js route group, the `_payload` [pathless layout route](https://tanstack.com/router/latest/docs/framework/react/routing/routing-concepts#pathless-layout-routes) creates the boundary that scopes all Admin Panel layouts and styles. The `_payload` directory contains the routes that power the Admin Panel: `admin.$.tsx` and `admin.index.tsx` are the _pages_ of the interface itself, `api.$.ts` serves the [REST API](/docs/v4/rest-api/overview.md) and [GraphQL API](/docs/v4/graphql/overview.md), and `server.functions.ts` contains the server functions used by the Admin Panel. `router.tsx` and `routeTree.gen.ts`, alongside `app`, are where the TanStack Router is created and regenerated. `routeTree.gen.ts` is regenerated automatically by the TanStack Router Vite plugin and should not be edited by hand. ### Shared Behavior `custom.css` is where you can add or override globally-oriented styles in the Admin Panel, such as modify the color palette. Customizing the look and feel through CSS alone is a powerful feature of the Admin Panel, [more on that here](/docs/v4/admin/customizing-css.md). If you don't intend to use the Admin Panel or [REST](/docs/v4/rest-api/overview.md)/[GraphQL](/docs/v4/graphql/overview.md) APIs, you can opt out by deleting their corresponding directories or route files. The overhead is completely constrained to these routes and will not slow down or affect Payload when not in use. > **TanStack Start:** `api.$.ts` serves both REST and GraphQL, so deleting it > disables both APIs. The generated layout, admin pages, and route files contain the following comment at the top of each file, on both frameworks: ```tsx /* THIS FILE WAS GENERATED AUTOMATICALLY BY PAYLOAD. MODIFY AT YOUR OWN RISK. */ ``` The auto-generated `importMap.js` is the only one of these files that is also regenerated automatically on an ongoing basis; do not edit it by hand. ## Admin Options All root-level options for the Admin Panel are defined in your [Payload Config](/docs/v4/configuration/overview.md) under the `admin` property: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... // highlight-start admin: { // ... }, // highlight-end }) ``` The following options are available: | Option | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `avatar` | Set account profile picture. Options: `gravatar`, `default` or a custom React component. | | `autoLogin` | Used to automate log-in for dev and demonstration convenience. [More details](/docs/v4/authentication/overview.md). | | `autoRefresh` | Used to automatically refresh user tokens for users logged into the dashboard. [More details](/docs/v4/authentication/overview.md). | | `components` | Component overrides that affect the entirety of the Admin Panel. [More details](/docs/v4/custom-components/overview.md). | | `constrainWidth` | **Experimental.** Constrain the Admin Panel content width to 1080px on desktop. Defaults to `false`. | | `custom` | Any custom properties you wish to pass to the Admin Panel. | | `dateFormat` | The date format that will be used for all dates within the Admin Panel. Any valid [date-fns](https://date-fns.org/) format pattern can be used. | | `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](/docs/v4/live-preview/overview.md). | | `meta` | Base metadata to use for the Admin Panel. [More details](/docs/v4/admin/metadata.md). | | `routes` | Replace built-in Admin Panel routes with your own custom routes. [More details](#customizing-routes). | | `suppressHydrationWarning` | If set to `true`, suppresses React hydration mismatch warnings during the hydration of the root `` tag. Defaults to `false`. | | `theme` | Restrict the Admin Panel theme to use only one of your choice. Default is `all`. | | `timezones` | Configure the timezone settings for the admin panel. [More details](#timezones) | | `toast` | Customize the handling of toast messages within the Admin Panel. [More details](#toast) | | `user` | The `slug` of the Collection that you want to allow to login to the Admin Panel. [More details](#the-admin-user-collection). | > **Reminder:** These are the _root-level_ options for the Admin Panel. You can > also customize [Collection Admin > Options](/docs/v4/configuration/collections.md#admin-options) and [Global Admin > Options](/docs/v4/configuration/globals.md#admin-options) through their respective > `admin` keys. ### The Admin User Collection To specify which Collection to allow to login to the Admin Panel, pass the `admin.user` key equal to the slug of any auth-enabled Collection: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { user: 'admins', // highlight-line }, }) ``` > **Important:** > > The Admin Panel can only be used by a single auth-enabled Collection. To enable authentication for a Collection, simply set `auth: true` in the Collection's configuration. See [Authentication](/docs/v4/authentication/overview.md) for more information. By default, if you have not specified a Collection, Payload will automatically provide a `User` Collection with access to the Admin Panel. You can customize or override the fields and settings of the default `User` Collection by adding your own Collection with `slug: 'users'`. Doing this will force Payload to use your provided `User` Collection instead of its default version. You can use whatever Collection you'd like to access the Admin Panel as long as the Collection supports [Authentication](/docs/v4/authentication/overview.md). It doesn't need to be called `users`. For example, you may wish to have two Collections that both support authentication: - `admins` - meant to have a higher level of permissions to manage your data and access the Admin Panel - `customers` - meant for end users of your app that should not be allowed to log into the Admin Panel To do this, specify `admin: { user: 'admins' }` in your config. This will provide access to the Admin Panel to only `admins`. Any users authenticated as `customers` will be prevented from accessing the Admin Panel. See [Access Control](/docs/v4/access-control/overview.md) for full details. ### Role-based Access Control It is also possible to allow multiple user types into the Admin Panel with limited permissions, known as role-based access control (RBAC). For example, you may wish to have two roles within the `admins` Collection: - `super-admin` - full access to the Admin Panel to perform any action - `editor` - limited access to the Admin Panel to only manage content To do this, add a `roles` or similar field to your auth-enabled Collection, then use the `access.admin` property to grant or deny access based on the value of that field. See [Access Control](/docs/v4/access-control/overview.md) for full details. For a complete, working example of role-based access control, check out the official [Auth Example](https://github.com/payloadcms/payload/tree/main/examples/auth). ## Customizing Routes You have full control over the routes that Payload binds itself to. This includes both [Root-level Routes](#root-level-routes) such as the [REST API](/docs/v4/rest-api/overview.md), and [Admin-level Routes](#admin-level-routes) such as the user's account page. You can customize these routes to meet the needs of your application simply by specifying the desired paths in your config. ### Root-level Routes Root-level routes are those that are not behind the `/admin` path, such as the [REST API](/docs/v4/rest-api/overview.md) and [GraphQL API](/docs/v4/graphql/overview.md), or the root path of the Admin Panel itself. To customize root-level routes, use the `routes` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... routes: { admin: '/custom-admin-route', // highlight-line }, }) ``` The following options are available: | Option | Default route | Description | | ------------------- | --------------------- | ------------------------------------------------- | | `admin` | `/admin` | The Admin Panel itself. | | `api` | `/api` | The [REST API](/docs/v4/rest-api/overview.md) base path. | | `graphQL` | `/graphql` | The [GraphQL API](/docs/v4/graphql/overview.md) base path. | | `graphQLPlayground` | `/graphql-playground` | The GraphQL Playground. | > **Important:** Changing Root-level Routes also requires a change to [Project > Structure](#project-structure) to match the new route. [More > details](#customizing-root-level-routes). > **Tip:** You can easily add _new_ routes to the Admin Panel through [Custom > Endpoints](/docs/v4/rest-api/overview.md#custom-endpoints) and [Custom > Views](/docs/v4/custom-components/custom-views.md). #### Customizing Root-level Routes You can change the Root-level Routes as needed, such as to mount the Admin Panel at the root of your application. This change, however, also requires a change to your [Project Structure](#project-structure) to match the new route. For example, if you set `routes.admin` to `/`: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... routes: { admin: '/', // highlight-line }, }) ``` Then you would need to completely remove the `admin` directory from the project structure: ```plaintext app ├─ (payload) ├── [[...segments]] ├──── ... ├── layout.tsx ``` > **Note:** If you set Root-level Routes _before_ auto-generating the Admin > Panel via `create-payload-app`, your [Project Structure](#project-structure) > will already be set up correctly. > **Advanced Routing:** For complex scenarios like nesting the admin panel at > custom paths (e.g., `/admin/content`) or organizing multiple admin dashboards, > see [Customizing Admin Panel Location](/docs/v4/admin/admin-panel-location.md). ### Admin-level Routes Admin-level routes are those behind the `/admin` path. These are the routes that are part of the Admin Panel itself, such as the user's account page, the login page, etc. To customize admin-level routes, use the `admin.routes` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { routes: { account: '/my-account', // highlight-line }, }, }) ``` The following options are available: | Option | Default route | Description | | ----------------- | -------------------- | ----------------------------------------- | | `account` | `/account` | The user's account page. | | `createFirstUser` | `/create-first-user` | The page to create the first user. | | `forgot` | `/forgot` | The password reset page. | | `inactivity` | `/logout-inactivity` | The page to redirect to after inactivity. | | `login` | `/login` | The login page. | | `logout` | `/logout` | The logout page. | | `reset` | `/reset` | The password reset page. | | `unauthorized` | `/unauthorized` | The unauthorized page. | > **Note:** You can also swap out entire _views_ out for your own, using the > `admin.views` property of the Payload Config. See [Custom > Views](/docs/v4/custom-components/custom-views.md) for more information. ## I18n The Payload Admin Panel is translated in over [30 languages and counting](https://github.com/payloadcms/payload/tree/main/packages/translations). Languages are automatically detected based on the user's browser and used by the Admin Panel to display all text in that language. If no language was detected, or if the user's language is not yet supported, English will be chosen. Users can easily specify their language by selecting one from their account page. See [I18n](/docs/v4/configuration/i18n.md) for more information. ## Light and Dark Modes Users in the Admin Panel have the ability to choose between light mode and dark mode for their editing experience. Users can select their preferred theme from their account page. Once selected, it is saved to their user's preferences and persisted across sessions and devices. If no theme was selected, the Admin Panel will automatically detect the operation system's theme and use that as the default. ## Timezones The `admin.timezones` configuration allows you to configure timezone settings for the Admin Panel. You can customise the available list of timezones and in the future configure the default timezone for the Admin Panel and for all users. Dates in Payload are always stored in UTC in the database. The timezone settings in the Admin Panel affect only how dates are displayed to editors to help ensure consistency for multi-region editorial teams. The following options are available: | Option | Description | | -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `supportedTimezones` | An array of label/value options for selectable timezones where the value is the IANA name eg. `America/Detroit`. Also supports a function that is given the defaultTimezones list. | | `defaultTimezone` | The `value` of the default selected timezone. eg. `America/Los_Angeles` | We validate the supported timezones array by checking the value against the list of IANA timezones supported via the Intl API, specifically `Intl.supportedValuesOf('timeZone')`. > **Note** > > **Important** You must enable timezones on each individual date field via > `timezone: true`. See [Date Fields](/docs/v4/fields/date.md) for more information. For example: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { timezones: { supportedTimezones: [ { label: 'Europe/Dublin', value: 'Europe/Dublin', }, { label: 'Europe/Amsterdam', value: 'Europe/Amsterdam', }, { label: 'Europe/Bucharest', value: 'Europe/Bucharest', }, ], defaultTimezone: 'Europe/Amsterdam', }, }, }) ``` ### Extending supported timezones For `supportedTimezones` we also support using a function that is given the defaultTimezones list. This allows you to easily extend the default list of timezones rather than replacing it completely. ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { timezones: { supportedTimezones: ({ defaultTimezones }) => [ ...defaultTimezones, // list provided by Payload { label: 'Europe/Dublin', value: 'Europe/Dublin', }, { label: 'Europe/Amsterdam', value: 'Europe/Amsterdam', }, { label: 'Europe/Bucharest', value: 'Europe/Bucharest', }, ], defaultTimezone: 'Europe/Amsterdam', }, }, }) ``` ### Using a UTC timezone In some situations you may want the displayed date and time to match exactly what's being stored in the database where we always store values in UTC. You can do this by adding UTC as a valid timezone option. Using a UTC timezone means that an editor inputing for example '1pm' will always see '1pm' and the stored value will be '13:00:00Z'. ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { timezones: { supportedTimezones: [ { label: 'UTC', value: 'UTC', }, // ...other timezones ], defaultTimezone: 'UTC', }, }, }) ``` ## Toast The `admin.toast` configuration allows you to customize the handling of toast messages within the Admin Panel, such as increasing the duration they are displayed and limiting the number of visible toasts at once. > **Note:** The Admin Panel currently uses the > [Sonner](https://sonner.emilkowal.ski) library for toast notifications. The following options are available for the `admin.toast` configuration: | Option | Description | Default | | ---------- | ---------------------------------------------------------------------------------------------------------------- | -------------- | | `duration` | The length of time (in milliseconds) that a toast message is displayed. | `6000` | | `expand` | If `true`, will expand the message stack so that all messages are shown simultaneously without user interaction. | `false` | | `limit` | The maximum number of toasts that can be visible on the screen at once. | `5` | | `position` | The position of the toast on the screen. | `bottom-right` | ## Frequently Asked Questions ### Can I move the Payload admin panel to a custom location? Yes! Payload is flexible about where the admin panel lives. You can move it to custom routes like `/dashboard`, `/cms`, or nested paths like `/admin/content`. See [Customizing Admin Panel Location](/docs/v4/admin/admin-panel-location.md) for step-by-step instructions. ### Which auto-generated files can I safely modify? Most files in the `(payload)` folder are only created once and can be safely modified. Only the `importMap.js` file is regenerated automatically. See [Understanding Auto-Generated Files](/docs/v4/admin/admin-panel-location.md#understanding-auto-generated-files) for a complete reference table. ### What happens if I edit the import map file directly? Don't do this - it will be overwritten on the next startup or HMR. Instead, configure component paths in your Payload config and let Payload regenerate the import map automatically. # Preview Source: https://payloadcms.com/docs/beta/admin/preview Preview is a feature that allows you to generate a direct link to your front-end application. When enabled, a "preview" button will appear on the Edit View within the [Admin Panel](/docs/v4/admin/overview.md) with an href pointing to the URL you provide. This will provide your editors with a quick way of navigating to the front-end application where that Document's data is represented. Otherwise, they'd have to determine that URL themselves which is not always straightforward especially in complex apps. The Preview feature can also be used to achieve something known as "Draft Preview". With Draft Preview, you can navigate to your front-end application and enter "draft mode", where your queries are modified to fetch draft content instead of published content. This is useful for seeing how your content will look before being published. [More details](#draft-preview). > **Note:** Preview is different than [Live Preview](/docs/v4/live-preview/overview.md). > Live Preview loads your app within an iframe and renders it in the Admin Panel > allowing you to see changes in real-time. Preview, on the other hand, allows > you to generate a direct link to your front-end application. To add Preview, pass a function to the `admin.preview` property in any [Collection Config](/docs/v4/configuration/collections.md#admin-options) or [Global Config](/docs/v4/configuration/globals.md#admin-options): ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', admin: { preview: ({ slug }) => `http://localhost:3000/${slug}`, }, fields: [ { name: 'slug', type: 'text', }, ], } ``` ## Options The `preview` function resolves to a string that points to your front-end application with additional URL parameters. This can be an absolute URL or a relative path, and can run async if needed. The following arguments are provided to the `preview` function: | Path | Description | | ------------- | ------------------------------------------------------------------------------------------ | | **`doc`** | The data of the Document being edited. This includes changes that have not yet been saved. | | **`options`** | An object with additional properties. | The `options` object contains the following properties: | Path | Description | | ------------ | -------------------------------------------------- | | **`locale`** | The current locale of the Document being edited. | | **`req`** | The Payload Request object. | | **`token`** | The JWT token of the currently authenticated user. | If your application requires a fully qualified URL, such as within deploying to Vercel Preview Deployments, you can use the `req` property to build this URL: ```ts preview: (doc, { req }) => `${req.protocol}//${req.host}/${doc.slug}` // highlight-line ``` ## Draft Preview The Preview feature can be used to achieve "Draft Preview". After clicking the preview button from the Admin Panel, you can enter into "draft mode" within your front-end application. This will allow you to adjust your page queries to include the `draft: true` param. When this param is present on the request, Payload will send back a draft document as opposed to a published one based on the document's `_status` field. To enter draft mode, the URL provided to the `preview` function can point to a custom endpoint in your front-end application that sets a cookie or session variable to indicate that draft mode is enabled. This is framework specific, so the mechanisms here vary from framework to framework although the underlying concept is the same. ### Next.js If you're using Next.js, you can do the following code to enter [Draft Mode](https://nextjs.org/docs/app/building-your-application/configuring/draft-mode). > **Note** > > This walkthrough uses Next.js' `draftMode()` API and a `route.ts` handler. > TanStack Start has no `draftMode()` equivalent — implement the same concept in > a [server function](/docs/v4/local-api/server-functions.md) that verifies the preview > secret and sets your own draft cookie, then read that cookie in your loaders > to toggle `draft: true`. #### Step 1: Format the Preview URL First, format your `admin.preview` function to point to a custom endpoint that you'll open on your front-end. This URL should include a few key query search params: ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', admin: { preview: ({ slug }) => { const encodedParams = new URLSearchParams({ slug, collection: 'pages', path: `/${slug}`, previewSecret: process.env.PREVIEW_SECRET || '', }) return `/preview?${encodedParams.toString()}` // highlight-line }, }, fields: [ { name: 'slug', type: 'text', }, ], } ``` #### Step 2: Create the Preview Route Then, create an API route that verifies the preview secret, authenticates the user, and enters draft mode: `/app/preview/route.ts` ```ts import type { PayloadRequest } from 'payload' import { getPayload } from 'payload' import { draftMode } from 'next/headers' import { redirect } from 'next/navigation' import configPromise from '@payload-config' export async function GET( req: { cookies: { get: (name: string) => { value: string } } } & Request, ): Promise { const payload = await getPayload({ config: configPromise }) const { searchParams } = new URL(req.url) const path = searchParams.get('path') const previewSecret = searchParams.get('previewSecret') if (previewSecret !== process.env.PREVIEW_SECRET) { return new Response('You are not allowed to preview this page', { status: 403, }) } if (!path) { return new Response('Insufficient search params', { status: 404 }) } if (!path.startsWith('/')) { return new Response( 'This endpoint can only be used for relative previews', { status: 500 }, ) } let user try { user = await payload.auth({ req: req as unknown as PayloadRequest, headers: req.headers, }) } catch (error) { payload.logger.error( { err: error }, 'Error verifying token for live preview', ) return new Response('You are not allowed to preview this page', { status: 403, }) } const draft = await draftMode() if (!user) { draft.disable() return new Response('You are not allowed to preview this page', { status: 403, }) } // You can add additional checks here to see if the user is allowed to preview this page draft.enable() redirect(path) } ``` #### Step 3: Query Draft Content Finally, in your front-end application, you can detect draft mode and adjust your queries to include drafts: `/app/[slug]/page.tsx` ```ts export default async function Page({ params: paramsPromise }) { const { slug = 'home' } = await paramsPromise const { isEnabled: isDraftMode } = await draftMode() const payload = await getPayload({ config }) const page = await payload.find({ collection: 'pages', depth: 0, draft: isDraftMode, // highlight-line limit: 1, overrideAccess: isDraftMode, where: { slug: { equals: slug, }, }, })?.then(({ docs }) => docs?.[0]) if (page === null) { return notFound() } return (

{page?.title}

) } ``` > **Note:** For fully working example of this, check out the official [Draft > Preview > Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview) > in the [Examples > Directory](https://github.com/payloadcms/payload/tree/main/examples). ### Conditional Preview URLs You can also conditionally enable or disable the preview button based on the document's data. This is useful for scenarios where you only want to show the preview button when certain criteria are met. To do this, simply return `null` from the `preview` function when you want to hide the preview button: ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', admin: { preview: (doc) => { return doc?.enabled ? `http://localhost:3000/${doc.slug}` : null }, }, fields: [ { name: 'slug', type: 'text', }, { name: 'enabled', type: 'checkbox', }, ], } ``` # Custom Admin Panel Location Source: https://payloadcms.com/docs/beta/admin/admin-panel-location Payload is flexible about where your Admin Panel lives within your application. You can customize routes, move folders, and organize your project structure to match your application's needs. > **Note** > > This guide applies to Next.js. TanStack Start does not yet support customizing > the Admin Panel's route location. See [Project > Structure](/docs/v4/admin/overview.md#project-structure) for the default folder layout of both > frameworks. ## Common Use Cases - **Custom admin routes** - Change `/admin` to `/dashboard`, `/cms`, etc. - **Nested admin panels** - Place Payload at `/admin/content` alongside custom admin routes - **Multiple admin interfaces** - Organize Payload and custom dashboards together - **Organizational preferences** - Match your team's folder structure conventions ## Scenario 1: Simple Route Change To change the admin route from `/admin` to `/dashboard`: **1. Update your Payload Config:** ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... routes: { admin: '/dashboard', // Changed from '/admin' }, }) ``` **2. Move the folder:** ```bash # Move the admin folder mv app/(payload)/admin app/(payload)/dashboard ``` **3. Update the import path in `layout.tsx`:** Edit `app/(payload)/layout.tsx` and update the import map reference to match the new folder name: ```ts // Before import { importMap } from './admin/importMap.js' // After import { importMap } from './dashboard/importMap.js' ``` > **Note** > > Your IDE may auto-update this import when you move the folder, but it may not. > To catch any stale references to the old path, run a build or type-check > (`pnpm build` or `pnpm tsc --noEmit`) and fix any import errors until it > passes. **4. Restart your dev server** Payload will now be available at `/dashboard`. ## Scenario 2: Moving the Entire Payload Folder (Admin Panel and API) To move both the Payload Admin Panel and API under a custom path, you move the entire `(payload)` folder. This example places everything under `/admin/content`. > **Note** > > Because the `admin/` subfolder inside `(payload)` is a Next.js route segment, > moving `(payload)` to `app/admin/content/(payload)` results in the admin panel > being served at `/admin/content/admin`, not `/admin/content`. Keep this in > mind when choosing your target path and setting `routes.admin`. **1. Update your Payload Config with all routes and import map settings:** ```ts import { buildConfig } from 'payload' import path from 'path' import { fileURLToPath } from 'url' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) export default buildConfig({ // ... routes: { admin: '/admin/content/admin', api: '/admin/content/api', graphQL: '/admin/content/api/graphql', graphQLPlayground: '/admin/content/api/graphql-playground', }, admin: { importMap: { baseDir: path.resolve(dirname, './src/app/admin/content/(payload)'), importMapFile: path.resolve( dirname, './src/app/admin/content/(payload)/admin/importMap.js', ), }, }, }) ``` **2. Move the `(payload)` folder:** ```bash # Create the new directory structure mkdir -p app/admin/content # Move the entire (payload) folder mv app/(payload) app/admin/content/(payload) ``` **3. Update the import path in `layout.tsx`:** Edit `app/admin/content/(payload)/layout.tsx`: ```ts // Change this import path to match your new structure import { importMap } from './admin/importMap.js' ``` The import path should be relative from `layout.tsx` to `importMap.js`. In this case, it remains `'./admin/importMap.js'`. **4. Regenerate the import map:** ```bash pnpm payload generate:importmap ``` Your Payload admin is now at `/admin/content/admin` and the API at `/admin/content/api`. ## Scenario 3: Multiple Admin Dashboards If you want both Payload and custom admin routes under `/admin`: **Folder Structure:** ``` app/ ├── admin/ │ ├── content/ # Payload admin │ │ └── (payload)/ │ │ ├── admin/ │ │ ├── api/ │ │ └── layout.tsx │ ├── platform/ # Your custom admin │ │ ├── dashboard/ │ │ │ └── page.tsx │ │ └── layout.tsx │ └── page.tsx # Choose between admins ``` **Configuration:** ```ts export default buildConfig({ routes: { admin: '/admin/content', api: '/admin/content/api', graphQL: '/admin/content/api/graphql', graphQLPlayground: '/admin/content/api/graphql-playground', }, admin: { importMap: { baseDir: path.resolve(dirname, './src/app/admin/content/(payload)'), importMapFile: path.resolve( dirname, './src/app/admin/content/(payload)/admin/importMap.js', ), }, }, }) ``` ## Understanding Auto-Generated Files Not all files in the `(payload)` folder are auto-generated. Here's what you need to know: | File | Auto-Generated? | Safe to Edit? | When Regenerated? | Notes | | ------------------------------------- | --------------- | ------------- | ---------------------------- | ------------------------------------------------------------ | | `layout.tsx` | No | Yes | Never | Created once during setup. Safe to modify import paths. | | `admin/importMap.js` | Yes | No | Startup, HMR, manual command | Always regenerated. Configure via `admin.importMap` instead. | | `admin/[[...segments]]/page.tsx` | No | Rarely needed | Never | Part of template. Usually no need to edit. | | `admin/[[...segments]]/not-found.tsx` | No | Rarely needed | Never | Part of template. Usually no need to edit. | | `api/[...slug]/route.ts` | No | Rarely needed | Never | Part of template. Usually no need to edit. | | `custom.css` | No | Yes | Never | Intended for your custom styles. | ## Editing layout.tsx Safely When you move the Payload folder, you'll need to update the import path in `layout.tsx`: ```tsx // app/admin/content/(payload)/layout.tsx import { importMap } from './admin/importMap.js' // Update this path as needed ``` **What you can safely edit:** - Import map path (required when moving folders) - Custom imports for additional functionality - Custom CSS imports - Server function logic (advanced use cases) **What you should NOT edit:** - The core `RootLayout` usage (required for Payload) - The `serverFunction` pattern (required for Payload to work) - The import map prop to `RootLayout` ## Import Map Regeneration The `importMap.js` file is automatically regenerated in these scenarios: - **Application startup** - Every time you start your dev server or production build - **Hot Module Replacement (HMR)** - When you save changes to component files in development - **Manual generation** - When you run `pnpm payload generate:importmap` The import map is **never regenerated** during: - Normal runtime (only at startup) - After the production build completes > **Important:** Never manually edit the `importMap.js` file. Configure > component paths in your Payload config and let Payload regenerate the import > map automatically. ### Generating the Import Map on build Running `payload build` generates the Import Map (and your TypeScript types) and then runs `next build`. Because the Import Map is statically imported at compile time, generating it _before_ `next build` ensures your production build always bundles an up-to-date Import Map. The Payload templates seed this for you: ```json { "scripts": { "build": "payload build" } } ``` `payload build` forwards any extra arguments to `next build` (e.g. `payload build --turbopack`). Pass `--no-types` to skip type generation (`payload build --no-types`). ## Troubleshooting ### Import map not found error If you see an error about the import map not being found: 1. Verify `admin.importMap.importMapFile` points to the correct location 2. Run `pnpm payload generate:importmap` manually 3. Check that the path is absolute, not relative ### Layout.tsx import error If you see an import error in `layout.tsx`: 1. Verify the import path is relative from `layout.tsx` to `importMap.js` 2. Use `./admin/importMap.js` if importMap is in a sibling `admin` folder 3. Use `./importMap.js` if you moved importMap to the same folder as layout ### Admin panel 404 error If the admin panel returns 404: 1. Verify `routes.admin` matches your actual folder structure 2. Ensure you moved the entire `(payload)` folder, not just `admin` 3. Check that Next.js is recognizing your new routes ### Components not loading If custom components aren't loading: 1. Verify `admin.importMap.baseDir` is correct 2. Regenerate the import map: `pnpm payload generate:importmap` 3. Check component paths in your config are relative to `baseDir` ## Additional Resources - [Admin Panel Overview](/docs/v4/admin/overview.md) - [Custom Components](/docs/v4/custom-components/overview.md) - [Import Map Configuration](/docs/v4/custom-components/overview.md#import-map) - [Customizing Routes](/docs/v4/admin/overview.md#customizing-routes) > **Success** > > **Need Help?** If you're still having issues, check the [Community > Help](https://payloadcms.com/community-help) or [open a new > issue](https://github.com/payloadcms/payload/issues/new/choose). # Document Locking Source: https://payloadcms.com/docs/beta/admin/locked-documents Document locking in Payload ensures that only one user at a time can edit a document, preventing data conflicts and accidental overwrites. When a document is locked, other users are prevented from making changes until the lock is released, ensuring data integrity in collaborative environments. The lock is automatically triggered when a user begins editing a document within the Admin Panel and remains in place until the user exits the editing view or the lock expires due to inactivity. ## How it works When a user starts editing a document, Payload locks it for that user. If another user attempts to access the same document, they will be notified that it is currently being edited. They can then choose one of the following options: - View in Read-Only: View the document without the ability to make any changes. - Take Over: Take over editing from the current user, which locks the document for the new editor and notifies the original user. - Return to Dashboard: Navigate away from the locked document and continue with other tasks. The lock will automatically expire after a set period of inactivity, configurable using the `duration` property in the `lockDocuments` configuration, after which others can resume editing. > **Note:** If your application does not require document locking, you can > disable this feature for any collection or global by setting the > `lockDocuments` property to `false`. ### Config Options The `lockDocuments` property exists on both the Collection Config and the Global Config. Document locking is enabled by default, but you can customize the lock duration or turn off the feature for any collection or global. Here's an example configuration for document locking: ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', fields: [ { name: 'title', type: 'text', }, // other fields... ], lockDocuments: { duration: 600, // Duration in seconds }, } ``` #### Locking Options | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`lockDocuments`** | Enables or disables document locking for the collection or global. By default, document locking is enabled. Set to an object to configure, or set to false to disable locking. | | **`duration`** | Specifies the duration (in seconds) for how long a document remains locked without user interaction. The default is 300 seconds (5 minutes). | ### Impact on APIs Document locking affects both the Local and REST APIs, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error. Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal. #### Overriding Locks For operations like `update` and `delete`, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock. By default, `overrideLock` is set to `true`, which means that document locks are ignored, and the operation will proceed even if the document is locked. To enforce locks and prevent updates or deletes on locked documents, set `overrideLock: false`. ```ts const result = await payload.update({ collection: 'posts', overrideAccess: true, id: '123', data: { title: 'New title', }, overrideLock: false, // Enforces the document lock, preventing updates if the document is locked }) ``` This option is particularly useful in scenarios where administrative privileges or specific workflows require you to override the lock and ensure the operation is completed. # Accessibility Source: https://payloadcms.com/docs/beta/admin/accessibility Payload is committed to ensuring that our admin panel is accessible to all users, including those with disabilities. We follow best practices and guidelines to create an inclusive experience. > **Note** > > We are actively working towards full compliance with WCAG 2.2 AA standards. If > you encounter any accessibility issues, please report them in our [GitHub > Discussion](https://github.com/payloadcms/payload/discussions/14489) page. ## Compliance standards | Standard | Status | Description | | -------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------- | | [WCAG 2.2 AA](https://www.w3.org/TR/WCAG22/) | In Progress | Web Content Accessibility Guidelines (WCAG) 2.2 AA is a widely recognized standard for web accessibility. | You can view our [report](https://github.com/payloadcms/payload/discussions/14489) on the current state of the admin panel's accessibility compliance. ## Our approach - Integrated Axe within our e2e test suites to ensure long term compliance. - Custom utilities to test keyboard navigation, window overflow and focus indicators across our components. - Manual testing with screen readers and other assistive technologies. # Customizing CSS Source: https://payloadcms.com/docs/beta/admin/customizing-css Customizing the Payload [Admin Panel](/docs/v4/admin/overview.md) through CSS alone is one of the easiest and most powerful ways to customize the look and feel of the dashboard. To allow for this level of customization, Payload: 1. Exposes a [root-level stylesheet](#global-css) for you to inject custom selectors 1. Provides a [CSS library](#css-library) that can be easily overridden or extended 1. Uses [BEM naming conventions](http://getbem.com) so that class names are globally accessible To customize the CSS within the Admin UI, determine scope and change you'd like to make, and then add your own CSS to the configuration as needed. ## Global CSS Global CSS refers to the CSS that is applied to the entire [Admin Panel](/docs/v4/admin/overview.md). This is where you can have a significant impact to the look and feel of the Admin UI through just a few lines of code. You can add your own global CSS through the root `custom.css` file of your app. This file is loaded into the root of the Admin Panel and can be used to inject custom selectors or styles however needed. Here is an example of how you might target the Dashboard View and change the background color: ```css .dashboard { background-color: red; // highlight-line } ``` > **Note:** If you are building [Custom > Components](/docs/v4/custom-components/overview.md), it is best to import your own > stylesheets directly into your components, rather than using the global > stylesheet. You can continue to use the [CSS library](#css-library) as needed. ### Specificity rules All Payload CSS is encapsulated inside CSS layers under `@layer payload-default`. Any custom css will now have the highest possible specificity. We have also provided a layer `@layer payload` if you want to use layers and ensure that your styles are applied after payload. To override existing styles in a way that the previous rules of specificity would be respected you can use the default layer like so ```css @layer payload-default { // my styles within the Payload specificity } ``` ## Re-using Payload CSS variables Payload exposes its design tokens as global [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). These are defined globally on `:root`, so you can consume them in your own stylesheets without importing anything. ```css @layer payload-default { .my-component { padding: var(--spacer-3); color: var(--color-text); } } ``` ## CSS Library To make it as easy as possible for you to override default styles, Payload uses [BEM naming conventions](http://getbem.com/) for all CSS within the Admin UI. If you provide your own CSS, you can override any built-in styles easily, including targeting nested components and their various component states. You can also override Payload's built-in [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). These variables are widely consumed by the Admin Panel, so modifying them has a significant impact on the look and feel of the Admin UI. The following variables are defined and can be overridden: - [Breakpoints](https://github.com/payloadcms/payload/blob/main/packages/ui/src/css/spacing.css) - [Colors](https://github.com/payloadcms/payload/blob/main/packages/ui/src/css/colors.css) - Semantic tokens for background, text, border, and icon colors (e.g. `--color-bg`, `--color-text`, `--color-border`, `--color-icon`) - Success / warning / danger color tokens - The raw `--ramp-*` color palette - [Sizing](https://github.com/payloadcms/payload/blob/main/packages/ui/src/css/app.css) - Horizontal gutter - Transition speeds - Font sizes - Etc. For an up-to-date, comprehensive list of all available variables, please refer to the [Source Code](https://github.com/payloadcms/payload/blob/main/packages/ui/src/css). > **Warning:** If you're overriding colors, make sure to consider how [your > changes will affect dark mode](#dark-mode). #### Dark Mode Colors are designed to automatically adapt to the theme of the [Admin Panel](/docs/v4/admin/overview.md). All `--color-*` tokens are redefined under `[data-theme='dark']`, so semantic tokens like `--color-bg` and `--color-text`, along with the success / warning / danger colors, adjust automatically for dark mode. # React Hooks Source: https://payloadcms.com/docs/beta/admin/react-hooks Payload provides a variety of powerful [React Hooks](https://react.dev/reference/react-dom/hooks) that can be used within your own [Custom Components](/docs/v4/custom-components/overview.md), such as [Custom Fields](/docs/v4/fields/overview.md#custom-components). With them, you can interface with Payload itself to build just about any type of complex customization you can think of. > **Reminder:** All Custom Components are [React Server > Components](https://react.dev/reference/rsc/server-components) by default. > Hooks, on the other hand, are only available in client-side environments. To > use hooks, [ensure your component is a client > component](/docs/v4/custom-components/overview.md#client-components). ## useField The `useField` hook is used internally within all field components. It manages sending and receiving a field's state from its parent form. When you build a [Custom Field Component](/docs/v4/fields/overview.md#custom-components), you will be responsible for sending and receiving the field's `value` to and from the form yourself. To do so, import the `useField` hook as follows: ```tsx 'use client' import type React from 'react' import type { TextFieldClientProps } from 'payload' import { useField } from '@payloadcms/ui' export const CustomTextField: React.FC = ({ path }) => { const { value, setValue } = useField({ path }) // highlight-line return (

{path}

{ setValue(e.target.value) }} value={value} />
) } ``` The `useField` hook accepts the following arguments: | Property | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `path` | If you do not provide a `path`, `name` will be used instead. This is the path to the field in the form data. | | `validate` | A validation function executed client-side _before_ submitting the form to the server. Different than [Field-level Validation](/docs/v4/fields/overview.md#validation) which runs strictly on the server. | | `disableFormData` | If `true`, the field will not be included in the form data when the form is submitted. | | `hasRows` | If `true`, the field will be treated as a field with rows. This is useful for fields like `array` and `blocks`. | The `useField` hook returns the following object: ```ts type FieldType = { errorMessage?: string errorPaths?: string[] filterOptions?: FilterOptionsResult formInitializing: boolean formProcessing: boolean formSubmitted: boolean initialValue?: T path: string permissions: FieldPermissions rows?: Row[] schemaPath: string setValue: (val: unknown, disableModifyingForm?: boolean) => void showError: boolean valid?: boolean value: T } ``` ### Updating Lexical Rich Text Fields The Lexical editor uses `initialValue` — not `value` — as the trigger to re-mount and visually update its content. This is intentional: it prevents unnecessary re-renders on every keystroke. However, it means that calling `setValue` alone will update the form's `value` (and save correctly to the database), but **the editor UI will not reflect the change** until the page is refreshed. To programmatically update a Lexical rich text field and have the editor visually reflect the new content, you must update both `value` and `initialValue` simultaneously. The way to do this is via `dispatchFields` with an `UPDATE` action, which can set any property of a field's form state at once: ```tsx 'use client' import { useAllFormFields } from '@payloadcms/ui' export const MyComponent: React.FC = () => { const [, dispatchFields] = useAllFormFields() const updateRichTextField = (newValue) => { dispatchFields({ type: 'UPDATE', path: 'myRichTextField', // the field's path in the form value: newValue, initialValue: newValue, // required to trigger Lexical re-render }) } return ( ) } ``` > **Warning** > > Using `setValue` from `useField` on a Lexical rich text field will update the > form data (and persist correctly on save), but the editor UI will not visually > update. Use `dispatchFields` with both `value` and `initialValue` set to the > new content to trigger a re-render of the editor. ## useFormFields There are times when a custom field component needs to have access to data from other fields, and you have a few options to do so. The `useFormFields` hook is a powerful and highly performant way to retrieve a form's field state, as well as to retrieve the `dispatchFields` method, which can be helpful for setting other fields form states. > **Success** > > **This hook is great for retrieving only certain fields from form state** > because it ensures that it will only cause a rerender when the items that you > ask for change. Thanks to the awesome package [`use-context-selector`](https://github.com/dai-shi/use-context-selector), you can retrieve a specific field's state easily. This is ideal because you can ensure you have an up-to-date field state, and your component will only re-render when _that field's state_ changes. You can pass a Redux-like selector into the hook, which will ensure that you retrieve only the field that you want. The selector takes an argument with type of `[fields: Fields, dispatch: React.Dispatch]]`. ```tsx 'use client' import { useFormFields } from '@payloadcms/ui' const MyComponent: React.FC = () => { // Get only the `amount` field state, and only cause a rerender when that field changes const amount = useFormFields(([fields, dispatch]) => fields.amount) // Do the same thing as above, but to the `feePercentage` field const feePercentage = useFormFields( ([fields, dispatch]) => fields.feePercentage, ) if ( typeof amount?.value !== 'undefined' && typeof feePercentage?.value !== 'undefined' ) { return The fee is ${(amount.value * feePercentage.value) / 100} } } ``` > **Warning** > > Be aware: in the example above, `MyComponent` may re-render if an ancestor > component re-renders or if any of its props change (standard React behavior), > but not because any fields other than `amount` or `feePercentage` changed. ## useAllFormFields **To retrieve more than one field**, you can use the `useAllFormFields` hook. Unlike the `useFormFields` hook, this hook does not accept a "selector", and it always returns an array with type of `[fields: Fields, dispatch: React.Dispatch]]`. > **Warning:** Your component will re-render when _any_ field changes, so use > this hook only if you absolutely need to. You can do lots of powerful stuff by retrieving the full form state, like using built-in helper functions to reduce field state to values only, or to retrieve sibling data by path. ```tsx 'use client' import { useAllFormFields } from '@payloadcms/ui' import { reduceFieldsToValues, getSiblingData } from 'payload/shared' const ExampleComponent: React.FC = () => { // the `fields` const will be equal to all fields' state, // and the `dispatchFields` method is usable to send field state up to the form const [fields, dispatchFields] = useAllFormFields(); // Pass in fields, and indicate if you'd like to "unflatten" field data. // The result below will reflect the data stored in the form at the given time const formData = reduceFieldsToValues(fields, true); // Pass in field state and a path, // and you will be sent all sibling data of the path that you've specified const siblingData = getSiblingData(fields, 'someFieldName'); return ( // return some JSX here if necessary ) }; ``` #### Updating other fields' values If you are building a Custom Component, then you should use `setValue` which is returned from the `useField` hook to programmatically set your field's value. But if you're looking to update _another_ field's value, you can use `dispatchFields` returned from `useAllFormFields`. You can send the following actions to the `dispatchFields` function. | Action | Description | | ---------------------- | -------------------------------------------------------------------------- | | **`ADD_ROW`** | Adds a row of data (useful in array / block field data) | | **`DUPLICATE_ROW`** | Duplicates a row of data (useful in array / block field data) | | **`MODIFY_CONDITION`** | Updates a field's conditional logic result (true / false) | | **`MOVE_ROW`** | Moves a row of data (useful in array / block field data) | | **`REMOVE`** | Removes a field from form state | | **`REMOVE_ROW`** | Removes a row of data from form state (useful in array / block field data) | | **`REPLACE_STATE`** | Completely replaces form state | | **`UPDATE`** | Update any property of a specific field's state | To see types for each action supported within the `dispatchFields` hook, check out the Form types [here](https://github.com/payloadcms/payload/blob/main/packages/ui/src/forms/Form/types.ts). ## useForm The `useForm` hook can be used to interact with the form itself, and sends back many methods that can be used to reactively fetch form state without causing rerenders within your components each time a field is changed. This is useful if you have action-based callbacks that your components fire, and need to interact with form state _based on a user action_. > **Warning:** > > This hook is optimized to avoid causing rerenders when fields change, and as such, its `fields` > property will be out of date. You should only leverage this hook if you need to perform actions > against the form in response to your users' actions. Do not rely on its returned "fields" as being > up-to-date. They will be removed from this hook's response in an upcoming version. The `useForm` hook returns an object with the following properties: \`\`\`tsx import { useForm } from "@payloadcms/ui" export const CustomArrayManager = () => { const { addFieldRow } = useForm() return ( ) } \`\`\` An example config to go along with the Custom Component \`\`\`tsx const ExampleCollection = { slug: "example-collection", fields: [ { name: "arrayField", type: "array", fields: [ { name: "textField", type: "text", }, ], }, { type: "ui", name: "customArrayManager", admin: { components: { Field: '/path/to/CustomArrayManagerField', }, }, }, ], } \`\`\` ` } ], [ { value: "**`removeFieldRow`**", }, { value: "Method to remove a row from an array or block field", }, { drawerTitle: 'removeFieldRow', drawerDescription: 'A useful method to programmatically remove a row from an array or block field.', drawerSlug: 'removeFieldRow', drawerContent: ` \`\`\`tsx import { useForm } from "@payloadcms/ui" export const CustomArrayManager = () => { const { removeFieldRow } = useForm() return ( ) } \`\`\` An example config to go along with the Custom Component \`\`\`tsx const ExampleCollection = { slug: "example-collection", fields: [ { name: "arrayField", type: "array", fields: [ { name: "textField", type: "text", }, ], }, { type: "ui", name: "customArrayManager", admin: { components: { Field: '/path/to/CustomArrayManagerField', }, }, }, ], } \`\`\` ` } ], [ { value: "**`replaceFieldRow`**", }, { value: "Method to replace a row from an array or block field", }, { drawerTitle: 'replaceFieldRow', drawerDescription: 'A useful method to programmatically replace a row from an array or block field.', drawerSlug: 'replaceFieldRow', drawerContent: ` \`\`\`tsx import { useForm } from "@payloadcms/ui" export const CustomArrayManager = () => { const { replaceFieldRow } = useForm() return ( ) } \`\`\` An example config to go along with the Custom Component \`\`\`tsx const ExampleCollection = { slug: "example-collection", fields: [ { name: "arrayField", type: "array", fields: [ { name: "textField", type: "text", }, ], }, { type: "ui", name: "customArrayManager", admin: { components: { Field: '/path/to/CustomArrayManagerField', }, }, }, ], } \`\`\` ` } ], ]} /> ## useDocumentForm The `useDocumentForm` hook works the same way as the [useForm](#useform) hook, but it always gives you access to the top-level `Form` of a document. This is useful if you need to access the document's `Form` context from within a child `Form`. An example where this could happen would be custom components within lexical blocks, as lexical blocks initialize their own child `Form`. ```tsx 'use client' import { useDocumentForm } from '@payloadcms/ui' const MyComponent: React.FC = () => { const { fields: parentDocumentFields } = useDocumentForm() return (

The document's Form has ${Object.keys(parentDocumentFields).length} fields

) } ``` ## useCollapsible The `useCollapsible` hook allows you to control parent collapsibles: | Property | Description | | ------------------------- | ------------------------------------------------------------------------------------------------------------- | | **`isCollapsed`** | State of the collapsible. `true` if open, `false` if collapsed. | | **`isVisible`** | If nested, determine if the nearest collapsible is visible. `true` if no parent is closed, `false` otherwise. | | **`toggle`** | Toggles the state of the nearest collapsible. | | **`isWithinCollapsible`** | Determine when you are within another collapsible. | **Example:** ```tsx 'use client' import React from 'react' import { useCollapsible } from '@payloadcms/ui' const CustomComponent: React.FC = () => { const { isCollapsed, toggle } = useCollapsible() return (

I am {isCollapsed ? 'closed' : 'open'}

) } ``` ## useDocumentInfo The `useDocumentInfo` hook provides information about the current document being edited, including the following: | Property | Description | | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | **`action`** | The URL attached to the action attribute on the underlying form element, which specifies where to send the form data when the form is submitted. | | **`apiURL`** | The API URL for the current document. | | **`collectionSlug`** | The slug of the collection if editing a collection document. | | **`currentEditor`** | The user currently editing the document. | | **`docConfig`** | Either the Collection or Global config of the document, depending on what is being edited. | | **`docPermissions`** | The current document's permissions. Fallback to collection permissions when no id is present. | | **`documentIsLocked`** | Whether the document is currently locked by another user. [More details](/docs/v4/admin/locked-documents.md). | | **`getDocPermissions`** | Method to retrieve document-level permissions. | | **`getDocPreferences`** | Method to retrieve document-level user preferences. [More details](/docs/v4/admin/preferences.md). | | **`globalSlug`** | The slug of the global if editing a global document. | | **`hasPublishedDoc`** | Whether the document has a published version. | | **`hasPublishPermission`** | Whether the current user has permission to publish the document. | | **`hasSavePermission`** | Whether the current user has permission to save the document. | | **`id`** | If the doc is a collection, its ID will be returned. | | **`incrementVersionCount`** | Method to increment the version count of the document. | | **`initialData`** | The initial data of the document. | | **`isEditing`** | Whether the document is being edited (as opposed to created). | | **`isInitializing`** | Whether the document info is still initializing. | | **`isLocked`** | Whether the document is locked. [More details](/docs/v4/admin/locked-documents.md). | | **`lastUpdateTime`** | Timestamp of the last update to the document. | | **`mostRecentVersionIsAutosaved`** | Whether the most recent version is an autosaved version. | | **`preferencesKey`** | The `preferences` key to use when interacting with document-level user preferences. [More details](/docs/v4/admin/preferences.md). | | **`data`** | The saved data of the document. | | **`setDocFieldPreferences`** | Method to set preferences for a specific field. [More details](/docs/v4/admin/preferences.md). | | **`setHasPublishedDoc`** | Method to update whether the document has been published. | | **`unlockDocument`** | Method to unlock a document. [More details](/docs/v4/admin/locked-documents.md). | | **`unpublishedVersionCount`** | The number of unpublished versions of the document. | | **`updateDocumentEditor`** | Method to update who is currently editing the document. [More details](/docs/v4/admin/locked-documents.md). | | **`setData`** | Method to update the saved document data. | | **`uploadStatus`** | Status of any uploads in progress ('idle', 'uploading', or 'failed'). | | **`versionCount`** | The current version count of the document. | **Example:** ```tsx 'use client' import { useDocumentInfo } from '@payloadcms/ui' const LinkFromCategoryToPosts: React.FC = () => { // highlight-start const { id } = useDocumentInfo() // highlight-end // id will be undefined on the create form if (!id) { return null } return ( View posts ) } ``` ## useDocumentTitle The `useDocumentTitle` hook returns the current document's rendered title and a setter to override it. It is split out of `useDocumentInfo` so that components which only depend on the title do not re-render when other document state changes. | Property | Description | | ---------------------- | ------------------------------------- | | **`title`** | The current title of the document. | | **`setDocumentTitle`** | Method to imperatively set the title. | **Example:** ```tsx 'use client' import { useDocumentTitle } from '@payloadcms/ui' const Heading: React.FC = () => { const { title } = useDocumentTitle() return

{title}

} ``` ## useListQuery The `useListQuery` hook is used to subscribe to the data, current query, and other properties used within the List View. You can use this hook within any Custom Component rendered within the List View. ```tsx 'use client' import { useListQuery } from '@payloadcms/ui' const MyComponent: React.FC = () => { // highlight-start const { data, query } = useListQuery() // highlight-end // ... } ``` The `useListQuery` hook returns an object with the following properties: | Property | Description | | ------------------------- | -------------------------------------------------------------------------------------- | | **`data`** | The data that is being displayed in the List View. | | **`defaultLimit`** | The default limit of items to display in the List View. | | **`defaultSort`** | The default sort order of items in the List View. | | **`handlePageChange`** | A method to handle page changes in the List View. | | **`handlePerPageChange`** | A method to handle per page changes in the List View. | | **`handleSearchChange`** | A method to handle search changes in the List View. | | **`handleSortChange`** | A method to handle sort changes in the List View. | | **`handleWhereChange`** | A method to handle where changes in the List View. | | **`modified`** | Whether the query has been changed from its [Query Preset](/docs/v4/query-presets/overview.md). | | **`query`** | The current query that is being used to fetch the data in the List View. | ## useSelection The `useSelection` hook provides information on the selected rows in the List view as well as helper methods to simplify selection. The `useSelection` hook returns an object with the following properties: | Property | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`count`** | The number of currently selected rows. | | **`getQueryParams`** | A function that generates a query string based on the current selection state and optional additional filtering parameters. | | **`selectAll`** | An enum value representing the selection range: `'allAvailable'`, `'allInPage'`, `'none'`, and `'some'`. The enum, `SelectAllStatus`, is exported for easier comparisons. | | **`selected`** | A map of document id keys and boolean values representing their selection status. | | **`setSelection`** | A function that toggles the selection status of a document row. | | **`toggleAll`** | A function that toggles selection for all documents on the current page or selects all available documents when passed `true`. | | **`totalDocs`** | The number of total documents in the collection. | **Example:** ```tsx 'use client' import { useSelection } from '@payloadcms/ui' const MyComponent: React.FC = () => { // highlight-start const { count, toggleAll, totalDocs } = useSelection() // highlight-end return ( <> Selected {count} out of {totalDocs} docs! ) } ``` ## useLocale In any Custom Component you can get the selected locale object with the `useLocale` hook. `useLocale` gives you the full locale object, consisting of a `label`, `rtl` (right-to-left) property, and `code`. It returns `null` when localization is disabled or configured without any locales. Here is a simple example: ```tsx 'use client' import { useLocale } from '@payloadcms/ui' const Greeting: React.FC = () => { // highlight-start const locale = useLocale() // highlight-end if (!locale) { return null } const trans = { en: 'Hello', es: 'Hola', } return {trans[locale.code]} } ``` ## useAuth Useful to retrieve info about the currently logged in user as well as methods for interacting with it. It sends back an object with the following properties: | Property | Description | | ------------------------ | --------------------------------------------------------------------------------------- | | **`user`** | The currently logged in user | | **`logOut`** | A method to log out the currently logged in user | | **`refreshCookie`** | A method to trigger the silent refreshing of a user's auth token | | **`setToken`** | Set the token of the user, to be decoded and used to reset the user and token in memory | | **`token`** | The logged in user's token (useful for creating preview links, etc.) | | **`refreshPermissions`** | Load new permissions (useful when content that affects permissions has been changed) | | **`permissions`** | The permissions of the current user | ```tsx 'use client' import { useAuth } from '@payloadcms/ui' import type { User } from '../payload-types.ts' const Greeting: React.FC = () => { // highlight-start const { user } = useAuth() // highlight-end return Hi, {user.email}! } ``` ## useConfig Used to retrieve the Payload [Client Config](/docs/v4/custom-components/overview.md#accessing-the-payload-config). ```tsx 'use client' import { useConfig } from '@payloadcms/ui' const MyComponent: React.FC = () => { // highlight-start const { config } = useConfig() // highlight-end return {config.serverURL} } ``` If you need to retrieve a specific collection or global config by its slug, `getEntityConfig` is the most efficient way to do so: ```tsx 'use client' import { useConfig } from '@payloadcms/ui' const MyComponent: React.FC = () => { // highlight-start const { getEntityConfig } = useConfig() const mediaConfig = getEntityConfig({ collectionSlug: 'media' }) // highlight-end return ( The media collection has {mediaConfig.fields.length} fields. ) } ``` ## useEditDepth Sends back how many editing levels "deep" the current component is. Edit depth is relevant while adding new documents / editing documents in modal windows and other cases. ```tsx 'use client' import { useEditDepth } from '@payloadcms/ui' const MyComponent: React.FC = () => { // highlight-start const editDepth = useEditDepth() // highlight-end return My component is {editDepth} levels deep } ``` ## usePreferences Returns methods to set and get user preferences. More info can be found [here](/docs/v4/admin/preferences.md). ## useTheme Returns the currently selected theme (`light`, `dark` or `auto`), a set function to update it and a boolean `autoMode`, used to determine if the theme value should be set automatically based on the user's device preferences. ```tsx 'use client' import { useTheme } from '@payloadcms/ui' const MyComponent: React.FC = () => { // highlight-start const { autoMode, setTheme, theme } = useTheme() // highlight-end return ( <> The current theme is {theme} and autoMode is {autoMode} ) } ``` ## useTableColumns Returns properties and methods to manipulate table columns: | Property | Description | | ------------------------ | ------------------------------------------------------------------------------------------ | | **`columns`** | The current state of columns including their active status and configuration | | **`LinkedCellOverride`** | A component override for linked cells in the table | | **`moveColumn`** | A method to reorder columns. Accepts `{ fromIndex: number, toIndex: number }` as arguments | | **`resetColumnsState`** | A method to reset columns back to their default configuration as defined in the collection config | | **`setActiveColumns`** | A method to set specific columns to active state while preserving the existing column order. Accepts an array of column names to activate | | **`toggleColumn`** | A method to toggle a single column's visibility. Accepts a column name as string | ```tsx 'use client' import { useTableColumns } from '@payloadcms/ui' const MyComponent: React.FC = () => { // highlight-start const { setActiveColumns, resetColumnsState } = useTableColumns() const activateSpecificColumns = () => { // Only activates the id and createdAt columns // Other columns retain their current active/inactive state // The original column order is preserved setActiveColumns(['id', 'createdAt']) } const resetToDefaults = () => { // Resets to the default columns defined in the collection config resetColumnsState() } // highlight-end return (
) } ``` ## useDocumentEvents The `useDocumentEvents` hook provides a way of subscribing to cross-document events, such as updates made to nested documents within a drawer. This hook will report document events that are outside the scope of the document currently being edited. This hook provides the following: | Property | Description | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | **`mostRecentUpdate`** | An object containing the most recently updated document. It contains the `entitySlug`, `id` (if collection), and `updatedAt` properties | | **`reportUpdate`** | A method used to report updates to documents. It accepts the same arguments as the `mostRecentUpdate` property. | **Example:** ```tsx 'use client' import { useDocumentEvents } from '@payloadcms/ui' const ListenForUpdates: React.FC = () => { const { mostRecentUpdate } = useDocumentEvents() return {JSON.stringify(mostRecentUpdate)} } ``` > **Note** > > Right now the `useDocumentEvents` hook only tracks recently updated documents, > but in the future it will track more document-related events as needed, such > as document creation, deletion, etc. ## useStepNav The `useStepNav` hook provides a way to change the step-nav breadcrumb links in the app header. | Property | Description | | ---------------- | -------------------------------------------------------------------------------- | | **`setStepNav`** | A state setter function which sets the `stepNav` array. | | **`stepNav`** | A `StepNavItem` array where each `StepNavItem` has a label and optionally a url. | **Example:** ```tsx 'use client' import type React from 'react' import { type StepNavItem, useStepNav } from '@payloadcms/ui' import { useEffect } from 'react' export const MySetStepNavComponent: React.FC<{ nav: StepNavItem[] }> = ({ nav }) => { const { setStepNav } = useStepNav() useEffect(() => { setStepNav(nav) }, [setStepNav, nav]) return null } ``` ## usePayloadAPI The `usePayloadAPI` hook is a useful tool for making REST API requests to your Payload instance and handling responses reactively. It allows you to fetch and interact with data while automatically updating when parameters change. This hook returns an array with two elements: 1. An object containing the API response. 2. A set of methods to modify request parameters. **Example:** ```tsx 'use client' import { usePayloadAPI } from '@payloadcms/ui' const MyComponent: React.FC = () => { // Fetch data from a collection item using its ID const [{ data, isError, isLoading }, { setParams }] = usePayloadAPI( '/api/posts/123', { initialParams: { depth: 1 }, }, ) if (isLoading) return

Loading...

if (isError) return

Error occurred while fetching data.

return (

{data?.title}

) } ``` **Arguments:** | Property | Description | | ------------- | ----------------------------------------------------------------------------------------------- | | **`url`** | The API endpoint to fetch data from. Relative URLs will be prefixed with the Payload API route. | | **`options`** | An object containing initial request parameters and initial state configuration. | The `options` argument accepts the following properties: | Property | Description | | ------------------- | --------------------------------------------------------------------------------------------------- | | **`initialData`** | Uses this data instead of making an initial request. If not provided, the request runs immediately. | | **`initialParams`** | Defines the initial parameters to use in the request. Defaults to an empty object `{}`. | **Returned Value:** The first item in the returned array is an object containing the following properties: | Property | Description | | --------------- | -------------------------------------------------------- | | **`data`** | The API response data. | | **`isError`** | A boolean indicating whether the request failed. | | **`isLoading`** | A boolean indicating whether the request is in progress. | The second item is an object with the following methods: | Property | Description | | --------------- | ----------------------------------------------------------- | | **`setParams`** | Updates request parameters, triggering a refetch if needed. | #### Updating Data The `setParams` function can be used to update the request and trigger a refetch: ```tsx setParams({ depth: 2 }) ``` This is useful for scenarios where you need to trigger another fetch regardless of the `url` argument changing. ## useRouteTransition Route transitions are useful in showing immediate visual feedback to the user when navigating between pages. This is especially useful on slow networks when navigating to data heavy or process intensive pages. By default, any instances of `Link` from `@payloadcms/ui` will trigger route transitions by default. ```tsx import { Link } from '@payloadcms/ui' const MyComponent = () => { return Go Somewhere } ``` You can also trigger route transitions programmatically, such as when calling `router.push`. To do this, wrap your function calls with the `startRouteTransition` method provided by the `useRouteTransition` hook. Import `useRouter` from `@payloadcms/ui` (not `next/navigation`) so your component works on both the Next.js and TanStack Start adapters. ```ts 'use client' import React, { useCallback } from 'react' import { useRouter, useTransition } from '@payloadcms/ui' const MyComponent: React.FC = () => { const router = useRouter() const { startRouteTransition } = useRouteTransition() const redirectSomewhere = useCallback(() => { startRouteTransition(() => router.push('/somewhere')) }, [startRouteTransition, router]) // ... } ``` # Managing User Preferences Source: https://payloadcms.com/docs/beta/admin/preferences As your users interact with the [Admin Panel](/docs/v4/admin/overview.md), you might want to store their preferences in a persistent manner, so that when they revisit the Admin Panel in a different session or from a different device, they can pick right back up where they left off. Out of the box, Payload handles the persistence of your users' preferences in a handful of ways, including: 1. Columns in the Collection List View: their active state and order 1. The user's last active [Locale](/docs/v4/configuration/localization.md) 1. The "collapsed" state of `blocks`, `array`, and `collapsible` fields 1. The last-known state of the `Nav` component, etc. > **Important:** > > All preferences are stored on an individual user basis. Payload automatically recognizes the user > that is reading or setting a preference via all provided authentication methods. ## Use Cases This API is used significantly for internal operations of the Admin Panel, as mentioned above. But, if you're building your own React components for use in the Admin Panel, you can allow users to set their own preferences in correspondence to their usage of your components. For example: - If you have built a "color picker", you could "remember" the last used colors that the user has set for easy access next time - If you've built a custom `Nav` component, and you've built in an "accordion-style" UI, you might want to store the `collapsed` state of each Nav collapsible item. This way, if an editor returns to the panel, their `Nav` state is persisted automatically - You might want to store `recentlyAccessed` documents to give admin editors an easy shortcut back to their recently accessed documents on the `Dashboard` or similar - Many other use cases exist. Invent your own! Give your editors an intelligent and persistent editing experience. ## Database Payload automatically creates an internally used `payload-preferences` Collection that stores user preferences. Each document in the `payload-preferences` Collection contains the following shape: | Key | Value | | ----------------- | ----------------------------------------------------------------- | | `id` | A unique ID for each preference stored. | | `key` | A unique `key` that corresponds to the preference. | | `user.value` | The ID of the `user` that is storing its preference. | | `user.relationTo` | The `slug` of the Collection that the `user` is logged in as. | | `value` | The value of the preference. Can be any data shape that you need. | | `createdAt` | A timestamp of when the preference was created. | | `updatedAt` | A timestamp set to the last time the preference was updated. | ## APIs Preferences are available to both [GraphQL](/docs/v4/graphql/overview.md#preferences) and [REST](/docs/v4/rest-api/overview.md#preferences) APIs. ## Adding or reading Preferences in your own components The Payload Admin Panel offers a `usePreferences` hook. The hook is only meant for use within the Admin Panel itself. It provides you with two methods: #### `getPreference` This async method provides an easy way to retrieve a user's preferences by `key`. It will return a promise containing the resulting preference value. **Arguments** - `key`: the `key` of your preference to retrieve. #### `setPreference` Also async, this method provides you with an easy way to set a user preference. It returns `void`. **Arguments:** - `key`: the `key` of your preference to set. - `value`: the `value` of your preference that you're looking to set. ## Example Here is an example for how you can utilize `usePreferences` within your custom Admin Panel components. Note - this example is not fully useful and is more just a reference for how to utilize the Preferences API. In this case, we are demonstrating how to set and retrieve a user's last used colors history within a `ColorPicker` or similar type component. ```tsx 'use client' import React, { Fragment, useState, useEffect, useCallback } from 'react' import { usePreferences } from '@payloadcms/ui' const lastUsedColorsPreferenceKey = 'last-used-colors' export function CustomComponent() { const { getPreference, setPreference } = usePreferences() // Store the last used colors in local state const [lastUsedColors, setLastUsedColors] = useState([]) // Callback to add a color to the last used colors const updateLastUsedColors = useCallback( (color) => { // First, check if color already exists in last used colors. // If it already exists, there is no need to update preferences const colorAlreadyExists = lastUsedColors.indexOf(color) > -1 if (!colorAlreadyExists) { const newLastUsedColors = [...lastUsedColors, color] setLastUsedColors(newLastUsedColors) setPreference(lastUsedColorsPreferenceKey, newLastUsedColors) } }, [lastUsedColors, setPreference], ) // Retrieve preferences on component mount // This will only be run one time, because the `getPreference` method never changes useEffect(() => { const asyncGetPreference = async () => { const lastUsedColorsFromPreferences = await getPreference( lastUsedColorsPreferenceKey, ) setLastUsedColors(lastUsedColorsFromPreferences) } asyncGetPreference() }, [getPreference]) return (
{lastUsedColors && (
Last used colors:
    {lastUsedColors?.map((color) => (
  • {color}
  • ))}
)}
) } ``` # Page Metadata Source: https://payloadcms.com/docs/beta/admin/metadata Every page within the Admin Panel automatically receives dynamic, auto-generated metadata derived from live document data, the user's current locale, and more. This includes the page title, description, og:image, etc. and requires no additional configuration. Metadata is fully configurable at the root level and cascades down to individual collections, documents, and custom views. This allows for the ability to control metadata on any page with high precision, while also providing sensible defaults. This metadata is used to generate the `` of pages within the Admin Panel. On Next.js it is injected into the [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) function; on TanStack Start it feeds the route's `head`. The metadata option shape follows Next.js' [`Metadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) API in both cases, and all of those options are exposed by Payload. Within the Admin Panel, metadata can be customized at the following levels: - [Root Metadata](#root-metadata) - [Collection Metadata](#collection-metadata) - [Global Metadata](#global-metadata) - [View Metadata](#view-metadata) All of these types of metadata share a similar structure, with a few key differences on the Root level. To customize metadata, consult the list of available scopes. Determine the scope that corresponds to what you are trying to accomplish, then author your metadata within the Payload Config accordingly. ## Root Metadata Root Metadata is the metadata that is applied to all pages within the Admin Panel. This is where you can control things like the suffix appended onto each page's title, the favicon displayed in the browser's tab, and the Open Graph data that is used when sharing the Admin Panel on social media. To customize Root Metadata, use the `admin.meta` key in your Payload Config: ```ts { // ... admin: { // highlight-start meta: { // highlight-end title: 'My Admin Panel', description: 'The best admin panel in the world', icons: [ { rel: 'icon', type: 'image/png', url: '/favicon.png', }, ], }, }, } ``` The following options are available for Root Metadata: | Key | Type | Description | | -------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `defaultOGImageType` | `dynamic` (default), `static`, or `off` | The type of default OG image to use. If set to `dynamic`, Payload will use Next.js image generation to create an image with the title of the page. If set to `static`, Payload will use the `defaultOGImage` URL. If set to `off`, Payload will not generate an OG image. | | `titleSuffix` | `string` | A suffix to append to the end of the title of every page. Defaults to "- Payload". | | `[keyof Metadata]` | `unknown` | Any other properties that Next.js supports within the `generateMetadata` function. [More details](https://nextjs.org/docs/app/api-reference/functions/generate-metadata). | > **Reminder:** These are the _root-level_ options for the Admin Panel. You can > also customize metadata on the [Collection](/docs/v4/configuration/collections.md), > [Global](/docs/v4/configuration/globals.md), and Document levels through their > respective configs. ### Icons The Icons Config corresponds to the `` tags that are used to specify icons for the Admin Panel. The `icons` key is an array of objects, each of which represents an individual icon. Icons are differentiated from one another by their `rel` attribute, which specifies the relationship between the document and the icon. The most common icon type is the favicon, which is displayed in the browser tab. This is specified by the `rel` attribute `icon`. Other common icon types include `apple-touch-icon`, which is used by Apple devices when the Admin Panel is saved to the home screen, and `mask-icon`, which is used by Safari to mask the Admin Panel icon. To customize icons, use the `admin.meta.icons` property in your Payload Config: ```ts { // ... admin: { meta: { // highlight-start icons: [ // highlight-end { rel: 'icon', type: 'image/png', url: '/favicon.png', }, { rel: 'apple-touch-icon', type: 'image/png', url: '/apple-touch-icon.png', }, ], }, }, } ``` For a full list of all available Icon options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#icons). ### Open Graph Open Graph metadata is a set of tags that are used to control how URLs are displayed when shared on social media platforms. Open Graph metadata is automatically generated by Payload, but can be customized at the Root level. To customize Open Graph metadata, use the `admin.meta.openGraph` property in your Payload Config: ```ts { // ... admin: { meta: { // highlight-start openGraph: { // highlight-end description: 'The best admin panel in the world', images: [ { url: 'https://example.com/image.jpg', width: 800, height: 600, }, ], siteName: 'Payload', title: 'My Admin Panel', }, }, }, } ``` For a full list of all available Open Graph options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#opengraph). ### Robots Setting the `robots` property will allow you to control the `robots` meta tag that is rendered within the `` of the Admin Panel. This can be used to control how search engines index pages and displays them in search results. By default, the Admin Panel is set to prevent search engines from indexing pages within the Admin Panel. To customize the Robots Config, use the `admin.meta.robots` property in your Payload Config: ```ts { // ... admin: { meta: { // highlight-start robots: 'noindex, nofollow', // highlight-end }, }, } ``` For a full list of all available Robots options, see the [Next.js documentation](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#robots). ##### Prevent Crawling While setting meta tags via `admin.meta.robots` can prevent search engines from _indexing_ web pages, it does not prevent them from being _crawled_. To prevent your pages from being crawled altogether, add a `robots.txt` file to your root directory. ```text User-agent: * Disallow: /admin/ ``` > **Note:** If you've customized the path to your Admin Panel via > `config.routes`, be sure to update the `Disallow` directive to match your > custom path. ## Collection Metadata Collection Metadata is the metadata that is applied to all pages within any given Collection within the Admin Panel. This metadata is used to customize the title and description of all views within any given Collection, unless overridden by the view itself. To customize Collection Metadata, use the `admin.meta` key within your Collection Config: ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { // highlight-start meta: { // highlight-end title: 'My Collection', description: 'The best collection in the world', }, }, } ``` The Collection Meta config has the same options as the [Root Metadata](#root-metadata) config. ## Global Metadata Global Metadata is the metadata that is applied to all pages within any given Global within the Admin Panel. This metadata is used to customize the title and description of all views within any given Global, unless overridden by the view itself. To customize Global Metadata, use the `admin.meta` key within your Global Config: ```ts import { GlobalConfig } from 'payload' export const MyGlobal: GlobalConfig = { // ... admin: { // highlight-start meta: { // highlight-end title: 'My Global', description: 'The best admin panel in the world', }, }, } ``` The Global Meta config has the same options as the [Root Metadata](#root-metadata) config. ## View Metadata View Metadata is the metadata that is applied to specific [Views](/docs/v4/custom-components/custom-views.md) within the Admin Panel. This metadata is used to customize the title and description of a specific view, overriding any metadata set at the [Root](#root-metadata), [Collection](#collection-metadata), or [Global](#global-metadata) level. To customize View Metadata, use the `meta` key within your View Config: ```ts { // ... admin: { views: { dashboard: { // highlight-start meta: { // highlight-end title: 'My Dashboard', description: 'The best dashboard in the world', } }, }, }, } ``` # Swap in your own React components Source: https://payloadcms.com/docs/beta/custom-components/overview The Payload [Admin Panel](/docs/v4/admin/overview.md) is designed to be as minimal and straightforward as possible to allow for easy customization and full control over the UI. In order for Payload to support this level of customization, Payload provides a pattern for you to supply your own React components through your [Payload Config](/docs/v4/configuration/overview.md). All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default. This enables the use of the [Local API](/docs/v4/local-api/overview.md) directly on the front-end. Custom Components are available for nearly every part of the Admin Panel for extreme granularity and control. > **Note:** Client Components continue to be fully supported. To use Client > Components in your app, simply include the `'use client'` directive. Payload > will automatically detect and remove all > [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) > default props before rendering your component. [More > details](#client-components). There are four main types of Custom Components in Payload: - [Root Components](/docs/v4/custom-components/root-components.md) - [Collection Components](/docs/v4/configuration/collections.md#custom-components) - [Global Components](/docs/v4/configuration/globals.md#custom-components) - [Field Components](/docs/v4/fields/overview.md#custom-components) To swap in your own Custom Component, first determine the scope that corresponds to what you are trying to accomplish, consult the list of available components, then [author your React component(s)](#building-custom-components) accordingly. ## Defining Custom Components As Payload compiles the Admin Panel, it checks your config for Custom Components. When detected, Payload either replaces its own default component with yours, or if none exists by default, renders yours outright. While there are many places where Custom Components are supported in Payload, each is defined in the same way using [Component Paths](#component-paths). To add a Custom Component, point to its file path in your Payload Config: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { components: { logout: { Button: '/src/components/Logout#MyComponent', // highlight-line }, }, }, }) ``` > **Note:** All Custom Components can be either Server Components or Client > Components, depending on the presence of the `'use client'` directive at the > top of the file. ### Component Paths In order to ensure the Payload Config is fully Node.js compatible and as lightweight as possible, components are not directly imported into your config. Instead, they are identified by their file path for the Admin Panel to resolve on its own. Component Paths, by default, are relative to your project's base directory. This is either your current working directory, or the directory specified in `config.admin.importMap.baseDir`. Components using named exports are identified either by appending `#` followed by the export name, or using the `exportName` property. If the component is the default export, the `#` and export name can be omitted. **Both default exports and named exports are fully supported.** Choose the pattern that works best for your codebase. ```ts import { buildConfig } from 'payload' import { fileURLToPath } from 'node:url' import path from 'path' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) const config = buildConfig({ // ... admin: { importMap: { baseDir: path.resolve(dirname, 'src'), // highlight-line }, components: { logout: { Button: '/components/Logout#MyComponent', // highlight-line }, }, }, }) ``` In this example, we set the base directory to the `src` directory, and omit the `/src/` part of our component path string. **Examples of component path syntax:** ```ts // Named export using hash syntax Button: '/components/Logout#MyComponent' // Default export (no hash needed) Button: '/components/Logout' // Named export using exportName property Button: { path: '/components/Logout', exportName: 'MyComponent', } ``` ### Component Config While Custom Components are usually defined as a string, you can also pass in an object with additional options: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { components: { logout: { // highlight-start Button: { path: '/src/components/Logout', exportName: 'MyComponent', }, // highlight-end }, }, }, }) ``` The following options are available: | Property | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------- | | `clientProps` | Props to be passed to the Custom Components if it's a Client Component. [More details](#custom-props). | | `exportName` | Instead of declaring named exports using `#` in the component path, you can also omit them from `path` and pass them in here. | | `path` | File path to the Custom Component. Named exports can be appended to the end of the path, separated by a `#`. | | `serverProps` | Props to be passed to the Custom Component if it's a Server Component. [More details](#custom-props). | For details on how to build Custom Components, see [Building Custom Components](#building-custom-components). ### Import Map In order for Payload to make use of [Component Paths](#component-paths), an "Import Map" is automatically generated at either `src/app/(payload)/admin/importMap.js` or `app/(payload)/admin/importMap.js`. This file contains every Custom Component in your config, keyed to their respective paths. When Payload needs to lookup a component, it uses this file to find the correct import. The Import Map is automatically regenerated at startup and whenever Hot Module Replacement (HMR) runs, or you can run `payload generate:importmap` to manually regenerate it. #### When Import Maps Are Regenerated The import map file is automatically regenerated in these scenarios: - **Application startup** - Every time you start your development server or build your application - **Hot Module Replacement (HMR)** - When you save changes to component files during development - **Manual generation** - When you run `payload generate:importmap` or `pnpm payload generate:importmap` The import map is **never regenerated** during: - Normal runtime (only at startup) - After a production build completes > **Important:** The import map file should never be manually edited. However, > the `layout.tsx` file that imports it is safe to modify when customizing your > folder structure. See [Customizing Admin Panel > Location](/docs/v4/admin/admin-panel-location.md) for details. #### Overriding Import Map Location Using the `config.admin.importMap.importMapFile` property, you can override the location of the import map. This is useful if you want to place the import map in a different location, or if you want to use a custom file name. ```ts import { buildConfig } from 'payload' import { fileURLToPath } from 'node:url' import path from 'path' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) const config = buildConfig({ // ... admin: { importMap: { baseDir: path.resolve(dirname, 'src'), importMapFile: path.resolve( dirname, 'app', '(payload)', 'custom-import-map.js', ), // highlight-line }, }, }) ``` #### Custom Imports If needed, custom items can be appended onto the Import Map. This is mostly only relevant for plugin authors who need to add a custom import that is not referenced in a known location. To add a custom import to the Import Map, use the `admin.dependencies` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // ... dependencies: { myTestComponent: { // myTestComponent is the key - can be anything path: '/components/TestComponent.js#TestComponent', type: 'component', clientProps: { test: 'hello', }, }, }, }, }) ``` ## Building Custom Components All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default. This enables the use of the [Local API](/docs/v4/local-api/overview.md) directly on the front-end, among other things. ### Default Props To make building Custom Components as easy as possible, Payload automatically provides common props, such as the [`payload`](/docs/v4/local-api/overview.md) class and the [`i18n`](/docs/v4/configuration/i18n.md) object. This means that when building Custom Components within the Admin Panel, you do not have to get these yourself. Here is an example: ```tsx import React from 'react' import type { ServerProps } from 'payload' async function MyServerComponent({ payload, // highlight-line user, // highlight-line }: Pick) { const page = await payload.findByID({ collection: 'pages', overrideAccess: false, user, id: '123', }) return

{page.title}

} ``` Each Custom Component receives the following props by default: | Prop | Description | | --------- | ------------------------------------------------------------------------------------------- | | `payload` | The [Payload](/docs/v4/local-api/overview.md) class. | | `i18n` | The [i18n](/docs/v4/configuration/i18n.md) object. | | `user` | The current user with [field read access](#server-principal-vs-read-filtered-user) applied. | > **Reminder:** All Custom Components also receive various other props that are > specific to the component being rendered. See [Root > Components](/docs/v4/custom-components/root-components.md), [Collection > Components](/docs/v4/configuration/collections.md#custom-components), [Global > Components](/docs/v4/configuration/globals.md#custom-components), or [Field > Components](/docs/v4/fields/overview.md#custom-components) for a complete list of all > default props per component. #### Server principal vs. read-filtered user Server Components that render an authenticated user receive two representations of that user, and choosing the wrong one either leaks data or breaks access checks: - **`user`** (the top-level prop, e.g. on views and field components) is the authenticated user with **field read access applied** — read-restricted fields are removed and relationships are populated according to read access. Use it for anything passed toward Client Components or rendered to the page, so restricted data never reaches the client. - **`req.user`** (from the `req` prop, or `initPageResult.req.user` in views) is the **complete server principal** with access bypassed. Use it for access-control checks, Local API queries, and role/authorization gating — `user` may omit fields your access logic relies on (for example a role stored in a read-restricted field). > **Security:** Never pass `req.user` toward a Client Component or into client > React state — use `user`. Conversely, never rely on `user` for authorization > decisions — use `req.user`. ### Custom Props It is also possible to pass custom props to your Custom Components. To do this, you can use either the `clientProps` or `serverProps` properties depending on whether your prop is [serializable](https://react.dev/reference/rsc/use-client#serializable-types), and whether your component is a Server or Client Component. ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { // highlight-line components: { logout: { Button: { path: '/src/components/Logout#MyComponent', clientProps: { myCustomProp: 'Hello, World!', // highlight-line }, }, }, }, }, }) ``` Here is how your component might receive this prop: ```tsx import React from 'react' import { Link } from '@payloadcms/ui' export function MyComponent({ myCustomProp }: { myCustomProp: string }) { return {myCustomProp} } ``` ### Client Components All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, however, it is possible to use [Client Components](https://react.dev/reference/rsc/use-client) by simply adding the `'use client'` directive at the top of your file. Payload will automatically detect and remove all [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) default props before rendering your component. ```tsx // highlight-start 'use client' // highlight-end import React, { useState } from 'react' export function MyClientComponent() { const [count, setCount] = useState(0) return ( ) } ``` > **Reminder:** Client Components cannot be passed [non-serializable > props](https://react.dev/reference/rsc/use-client#serializable-types). If you > are rendering your Client Component _from within_ a Server Component, ensure > that its props are serializable. ### Accessing the Payload Config From any Server Component, the [Payload Config](/docs/v4/configuration/overview.md) can be accessed directly from the `payload` prop: ```tsx import React from 'react' export default async function MyServerComponent({ payload: { config, // highlight-line }, }) { return Go Home } ``` But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions and more. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components. For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](/docs/v4/admin/react-hooks.md#useconfig) hook: ```tsx 'use client' import React from 'react' import { useConfig } from '@payloadcms/ui' export function MyClientComponent() { // highlight-start const { config: { serverURL }, } = useConfig() // highlight-end return Go Home } ``` > **Success** > > See [Using Hooks](#using-hooks) for more details. Similarly, all [Field Components](/docs/v4/fields/overview.md#custom-components) automatically receive their respective Field Config through props. Within Server Components, this prop is named `field`: ```tsx import React from 'react' import type { TextFieldServerProps } from 'payload' export const MyClientFieldComponent: React.FC = ({ field: { name }, }) => { return

{`This field's name is ${name}`}

} ``` Within Client Components, this prop is named `clientField` because its non-serializable props have been removed: ```tsx 'use client' import React from 'react' import type { TextFieldClientProps } from 'payload' export const MyClientFieldComponent: React.FC = ({ clientField: { name }, }) => { return

{`This field's name is ${name}`}

} ``` ### Getting the Current Language All Custom Components can support language translations to be consistent with Payload's [I18n](/docs/v4/configuration/i18n.md). This will allow your Custom Components to display the correct language based on the user's preferences. To do this, first add your translation resources to the [I18n Config](/docs/v4/configuration/i18n.md). Then from any Server Component, you can translate resources using the `getTranslation` function from `@payloadcms/translations`. All Server Components automatically receive the `i18n` object as a prop by default: ```tsx import React from 'react' import { getTranslation } from '@payloadcms/translations' export default async function MyServerComponent({ i18n }) { const translatedTitle = getTranslation(myTranslation, i18n) // highlight-line return

{translatedTitle}

} ``` The best way to do this within a Client Component is to import the `useTranslation` hook from `@payloadcms/ui`: ```tsx 'use client' import React from 'react' import { useTranslation } from '@payloadcms/ui' export function MyClientComponent() { const { t, i18n } = useTranslation() // highlight-line return (
  • {t('namespace1:key', { variable: 'value' })}
  • {t('namespace2:key', { variable: 'value' })}
  • {i18n.language}
) } ``` > **Success** > > See the [Hooks](/docs/v4/admin/react-hooks.md) documentation for a full list of > available hooks. ### Getting the Current Locale All [Custom Views](/docs/v4/custom-components/custom-views.md) can support multiple locales to be consistent with Payload's [Localization](/docs/v4/configuration/localization.md) feature. This can be used to scope API requests, etc. All Server Components automatically receive the `locale` object as a prop by default: ```tsx import React from 'react' export default async function MyServerComponent({ payload, locale, user }) { const localizedPage = await payload.findByID({ collection: 'pages', overrideAccess: false, user, id: '123', locale, }) return

{localizedPage.title}

} ``` The best way to do this within a Client Component is to import the `useLocale` hook from `@payloadcms/ui`. The hook returns `null` when localization is disabled or configured without any locales: ```tsx 'use client' import React from 'react' import { useLocale } from '@payloadcms/ui' function Greeting() { const locale = useLocale() // highlight-line if (!locale) { return null } const trans = { en: 'Hello', es: 'Hola', } return {trans[locale.code]} } ``` > **Success** > > See the [Hooks](/docs/v4/admin/react-hooks.md) documentation for a full list of > available hooks. ### Using Hooks To make it easier to [build your Custom Components](#building-custom-components), you can use [Payload's built-in React Hooks](/docs/v4/admin/react-hooks.md) in any Client Component. For example, you might want to interact with one of Payload's many React Contexts. To do this, you can use one of the many hooks available depending on your needs. ```tsx 'use client' import React from 'react' import { useDocumentInfo } from '@payloadcms/ui' export function MyClientComponent() { const { slug } = useDocumentInfo() // highlight-line return

{`Entity slug: ${slug}`}

} ``` > **Success** > > See the [Hooks](/docs/v4/admin/react-hooks.md) documentation for a full list of > available hooks. ### Adding Styles Payload has a robust [CSS Library](/docs/v4/admin/customizing-css.md) that you can use to style your Custom Components to match to Payload's built-in styling. This will ensure that your Custom Components integrate well into the existing design system. This will make it so they automatically adapt to any theme changes that might occur. To apply custom styles, simply import your own `.css` or `.scss` file into your Custom Component: ```tsx import './index.scss' export function MyComponent() { return
My Custom Component
} ``` Then to colorize your Custom Component's background, for example, you can use the following CSS: ```scss .my-component { background-color: var(--color-bg-secondary); } ``` Payload exposes its design tokens as global [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties), so you can consume them directly without importing anything. For example, using a breakpoint: ```scss .my-component { @media (max-width: 1024px) { background-color: var(--color-bg-inverse); } } ``` > **Note:** You can also drill into Payload's own component styles, or easily > apply global, app-wide CSS. More on that [here](/docs/v4/admin/customizing-css.md). ## Performance An often overlooked aspect of Custom Components is performance. If unchecked, Custom Components can lead to slow load times of the Admin Panel and ultimately a poor user experience. This is different from front-end performance of your public-facing site. > **Success** > > For more performance tips, see the [Performance > documentation](/docs/v4/performance/overview.md). ### Follow React and your host framework's best practices All Custom Components are built using [React](https://react.dev). For this reason, it is important to follow React best practices. This includes using memoization, streaming, caching, optimizing renders, using hooks appropriately, and more. To learn more, see the [React documentation](https://react.dev/learn). The Admin Panel itself is a [Next.js](https://nextjs.org) or [TanStack Start](https://tanstack.com/start/latest) application, depending on how you installed Payload. For this reason, it is _also_ important to follow your host framework's best practices. This includes bundling, when to use layouts vs pages, where to place the server/client boundary, and more. To learn more, see the [Next.js documentation](https://nextjs.org/docs) or the [TanStack Start documentation](https://tanstack.com/start/latest/docs). ### Reducing initial HTML size With Server Components, be aware of what is being sent to through the server/client boundary. All props are serialized and sent through the network. This can lead to large HTML sizes and slow initial load times if too much data is being sent to the client. To minimize this, you must be explicit about what props are sent to the client. Prefer server components and only send the necessary props to the client. This will also offset some of the JS execution to the server. > **Tip:** Use [React Suspense](https://react.dev/reference/react/Suspense) to > progressively load components and improve perceived performance. ### Prevent unnecessary re-renders If subscribing your component to form state, it may be re-rendering more often than necessary. To do this, use the [`useFormFields`](/docs/v4/admin/react-hooks.md) hook instead of `useFields` when you only need to access specific fields. ```ts 'use client' import type React from 'react' import { useFormFields } from '@payloadcms/ui' import type { TextFieldClientProps } from 'payload' const MyComponent: React.FC = ({ path }) => { const value = useFormFields(([fields, dispatch]) => fields[path]) // ... } ``` ### Imports best practices When building Custom Components it's important to be mindful of your bundle sizes sent to the client which are primarily affected by your imports. Generally speaking you can import third party packages as needed though it's best to avoid large packages that bloat your bundle size. The most common issue is importing from our `@payloadcms/ui` package in the wrong context. So here are the simple rules to follow: - In the **admin panel UI** you always want to import everything from `@payloadcms/ui` to ensure there's no package mismatch issues. - In the **frontend application** you must always import components and utilities from `@payloadcms/ui/path/to` for example `import { Button } from '@payloadcms/ui/elements/Button'` to ensure tree shaking is effective and your bundle sizes are minimized otherwise it will include the entire library with your frontend code and greatly bloat your bundle size. > **Success** > > See the [Performance](/docs/v4/performance/overview.md) documentation for more tips and > best practices. ### Troubleshooting #### 'Assignment cannot be destructured' or 'value ... of useConfig is undefined' There's a few variations of this error that hint at the same problem, sometimes it will error on `useConfig` hook or any other Payload UI hook like `useAuth`, `useLocale` with the error message that the value being destructured is `undefined`. See [Troubleshooting Common Issues in Payload](/docs/v4/troubleshooting/troubleshooting.md#dependency-mismatches) for more details on resolving dependency mismatches. > **Note** > > Generally we recommend pinning Payload packages to the exact same version in > order to ensure that your package manager installs the same versions across > all Payload packages. # Root Components Source: https://payloadcms.com/docs/beta/custom-components/root-components Root Components are those that affect the [Admin Panel](/docs/v4/admin/overview.md) at a high-level, such as the logo or the main nav. You can swap out these components with your own [Custom Components](/docs/v4/custom-components/overview.md) to create a completely custom look and feel. When combined with [Custom CSS](/docs/v4/admin/customizing-css.md), you can create a truly unique experience for your users, such as white-labeling the Admin Panel to match your brand. To override Root Components, use the `admin.components` property at the root of your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { // ... }, // highlight-end }, }) ``` ## Config Options The following options are available: | Path | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `actions` | An array of Custom Components to be rendered _within_ the header of the Admin Panel, providing additional interactivity and functionality. [More details](#actions). | | `afterDashboard` | An array of Custom Components to inject into the built-in Dashboard, _after_ the default dashboard contents. [More details](#afterdashboard). | | `afterLogin` | An array of Custom Components to inject into the built-in Login, _after_ the default login form. [More details](#afterlogin). | | `afterNavLinks` | An array of Custom Components to inject into the built-in Nav, _after_ the links. [More details](#afternavlinks). | | `beforeDashboard` | An array of Custom Components to inject into the built-in Dashboard, _before_ the default dashboard contents. [More details](#beforedashboard). | | `beforeLogin` | An array of Custom Components to inject into the built-in Login, _before_ the default login form. [More details](#beforelogin). | | `beforeNavLinks` | An array of Custom Components to inject into the built-in Nav, _before_ the links themselves. [More details](#beforenavlinks). | | `graphics.Icon` | The simplified logo used in contexts like the `Nav` component. [More details](#graphicsicon). | | `graphics.Logo` | The full logo used in contexts like the `Login` view. [More details](#graphicslogo). | | `header` | An array of Custom Components to be injected above the Payload header. [More details](#header). | | `logout.Button` | The button displayed in the sidebar that logs the user out. [More details](#logoutbutton). | | `Nav` | Contains the sidebar / mobile menu in its entirety. [More details](#nav). | | `settingsMenu` | An array of Custom Components to inject into a popup menu accessible via a gear icon above the logout button. [More details](#settingsmenu). | | `providers` | Custom [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context) providers that will wrap the entire Admin Panel. [More details](/docs/v4/custom-components/custom-providers.md). | | `views` | Override or create new views within the Admin Panel. [More details](/docs/v4/custom-components/custom-views.md). | _For details on how to build Custom Components, see [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components)._ > **Note:** You can also set [Collection > Components](/docs/v4/configuration/collections.md#custom-components) and [Global > Components](/docs/v4/configuration/globals.md#custom-components) in their respective > configs. ## Components ### actions Actions are rendered within the header of the Admin Panel. Actions are typically used to display buttons that add additional interactivity and functionality, although they can be anything you'd like. To add an action, use the `actions` property in your `admin.components` config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { actions: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple Action component: ```tsx export default function MyCustomAction() { return ( ) } ``` > **Note:** You can also use add Actions to the [Edit View](/docs/v4/custom-components/edit-view.md) and > [List View](/docs/v4/custom-components/list-view.md) in their respective configs. ### beforeDashboard The `beforeDashboard` property allows you to inject Custom Components into the built-in Dashboard, before the default dashboard contents. To add `beforeDashboard` components, use the `admin.components.beforeDashboard` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { beforeDashboard: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `beforeDashboard` component: ```tsx export default function MyBeforeDashboardComponent() { return
This is a custom component injected before the Dashboard.
} ``` > **Note:** You can also set [Dashboard Widgets](/docs/v4/custom-components/dashboard.md) > in the `admin.dashboard` property, or replace the entire [Dashboard > View](/docs/v4/custom-components/dashboard.md) with your own. ### afterDashboard Similar to `beforeDashboard`, the `afterDashboard` property allows you to inject Custom Components into the built-in Dashboard, _after_ the default dashboard contents. To add `afterDashboard` components, use the `admin.components.afterDashboard` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { afterDashboard: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `afterDashboard` component: ```tsx export default function MyAfterDashboardComponent() { return
This is a custom component injected after the Dashboard.
} ``` > **Note:** You can also set [Dashboard Widgets](/docs/v4/custom-components/dashboard.md) > in the `admin.dashboard` property, or replace the entire [Dashboard > View](/docs/v4/custom-components/dashboard.md) with your own. ### beforeLogin The `beforeLogin` property allows you to inject Custom Components into the built-in Login view, _before_ the default login form. To add `beforeLogin` components, use the `admin.components.beforeLogin` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { beforeLogin: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `beforeLogin` component: ```tsx export default function MyBeforeLoginComponent() { return
This is a custom component injected before the Login form.
} ``` ### afterLogin Similar to `beforeLogin`, the `afterLogin` property allows you to inject Custom Components into the built-in Login view, _after_ the default login form. To add `afterLogin` components, use the `admin.components.afterLogin` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { afterLogin: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `afterLogin` component: ```tsx export default function MyAfterLoginComponent() { return
This is a custom component injected after the Login form.
} ``` ### beforeNavLinks The `beforeNavLinks` property allows you to inject Custom Components into the built-in [Nav Component](#nav), _before_ the nav links themselves. To add `beforeNavLinks` components, use the `admin.components.beforeNavLinks` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { beforeNavLinks: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `beforeNavLinks` component: ```tsx export default function MyBeforeNavLinksComponent() { return
This is a custom component injected before the Nav links.
} ``` ### afterNavLinks Similar to `beforeNavLinks`, the `afterNavLinks` property allows you to inject Custom Components into the built-in Nav, _after_ the nav links. To add `afterNavLinks` components, use the `admin.components.afterNavLinks` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { afterNavLinks: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `afterNavLinks` component: ```tsx export default function MyAfterNavLinksComponent() { return

This is a custom component injected after the Nav links.

} ``` ### settingsMenu The `settingsMenu` property allows you to inject Custom Components into a popup menu accessible via a gear icon in the navigation controls, positioned above the logout button. This is ideal for adding custom actions, utilities, or settings that are relevant at the admin level. To add `settingsMenu` components, use the `admin.components.settingsMenu` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { settingsMenu: ['/path/to/your/component#ComponentName'], }, // highlight-end }, }) ``` Here is an example of a simple `settingsMenu` component: ```tsx 'use client' import { PopupList } from '@payloadcms/ui' export function MySettingsMenu() { return ( console.log('Action triggered')}> Custom Action window.open('/admin/custom-page')}> Custom Page ) } ``` You can pass multiple components to create organized groups of menu items: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { components: { settingsMenu: [ '/components/SystemActions#SystemActions', '/components/DataManagement#DataManagement', ], }, }, }) ``` ### Nav The `Nav` property contains the sidebar / mobile menu in its entirety. Use this property to completely replace the built-in Nav with your own custom navigation. To add a custom nav, use the `admin.components.Nav` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { Nav: '/path/to/your/component', }, // highlight-end }, }) ``` Here is an example of a simple `Nav` component: ```tsx import { Link } from '@payloadcms/ui' export default function MyCustomNav() { return ( ) } ``` ### graphics.Icon The `Icon` property is the simplified logo used in contexts like the `Nav` component. This is typically a small, square icon that represents your brand. To add a custom icon, use the `admin.components.graphics.Icon` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { graphics: { Icon: '/path/to/your/component', }, }, // highlight-end }, }) ``` Here is an example of a simple `Icon` component: ```tsx export default function MyCustomIcon() { return My Custom Icon } ``` ### graphics.Logo The `Logo` property is the full logo used in contexts like the `Login` view. This is typically a larger, more detailed representation of your brand. To add a custom logo, use the `admin.components.graphics.Logo` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { graphics: { Logo: '/path/to/your/component', }, }, // highlight-end }, }) ``` Here is an example of a simple `Logo` component: ```tsx export default function MyCustomLogo() { return My Custom Logo } ``` ### header The `header` property allows you to inject Custom Components above the Payload header. Examples of a custom header components might include an announcements banner, a notifications bar, or anything else you'd like to display at the top of the Admin Panel in a prominent location. To add `header` components, use the `admin.components.header` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { header: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `header` component: ```tsx export default function MyCustomHeader() { return (

My Custom Header

) } ``` ### logout.Button The `logout.Button` property is the button displayed in the sidebar that should log the user out when clicked. To add a custom logout button, use the `admin.components.logout.Button` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { logout: { Button: '/path/to/your/component', }, }, // highlight-end }, }) ``` Here is an example of a simple `logout.Button` component: ```tsx export default function MyCustomLogoutButton() { return } ``` # Swap in your own React Context providers Source: https://payloadcms.com/docs/beta/custom-components/custom-providers As you add more and more [Custom Components](/docs/v4/custom-components/overview.md) to your [Admin Panel](/docs/v4/admin/overview.md), you may find it helpful to add additional [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context)(s) to your app. Payload allows you to inject your own context providers where you can export your own custom hooks, etc. To add a Custom Provider, use the `admin.components.providers` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { components: { providers: ['/path/to/MyProvider'], // highlight-line }, }, }) ``` Then build your Custom Provider as follows: ```tsx 'use client' import React, { createContext, use } from 'react' const MyCustomContext = React.createContext(myCustomValue) export function MyProvider({ children }: { children: React.ReactNode }) { return {children} } export const useMyCustomContext = () => use(MyCustomContext) ``` _For details on how to build Custom Components, see [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components)._ > **Reminder:** React Context exists only within Client Components. This means > they must include the `use client` directive at the top of their files and > cannot contain server-only code. To use a Server Component here, simply _wrap_ > your Client Component with it. # Customizing Views Source: https://payloadcms.com/docs/beta/custom-components/custom-views Views are the individual pages that make up the [Admin Panel](/docs/v4/admin/overview.md), such as the Dashboard, [List View](/docs/v4/custom-components/list-view.md), and [Edit View](/docs/v4/custom-components/edit-view.md). One of the most powerful ways to customize the Admin Panel is to create Custom Views. These are [Custom Components](/docs/v4/custom-components/overview.md) that can either replace built-in views or be entirely new. There are four types of views within the Admin Panel: - [Root Views](#root-views) - [Collection Views](#collection-views) - [Global Views](#global-views) - [Document Views](/docs/v4/custom-components/document-views.md) To swap in your own Custom View, first determine the scope that corresponds to what you are trying to accomplish, consult the list of available components, then [author your React component(s)](#building-custom-views) accordingly. ## Configuration ### Replacing Views To customize views, use the `admin.components.views` property in your [Payload Config](/docs/v4/configuration/overview.md). This is an object with keys for each view you want to customize. Each key corresponds to the view you want to customize. The exact list of available keys depends on the scope of the view you are customizing, depending on whether it's a [Root View](#root-views), [Collection View](#collection-views), or [Global View](#global-views). Regardless of the scope, the principles are the same. Here is an example of how to swap out a built-in view: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { components: { views: { // highlight-start dashboard: { Component: '/path/to/MyCustomDashboard', }, // highlight-end }, }, }, }) ``` > **Note:** The dashboard is a special case, where in addition to replacing the > default view, [you can add widgets modularly](/docs/v4/custom-components/dashboard.md). For more granular control, pass a configuration object instead. Payload exposes the following properties for each view: | Property | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Component` \* | Pass in the component path that should be rendered when a user navigates to this route. | | `path` \* | Any valid URL path or array of paths that [`path-to-regexp`](https://www.npmjs.com/package/path-to-regex) understands. Must begin with a forward slash (`/`). | | `exact` | Boolean. When true, will only match if the path matches the `usePathname()` exactly. | | `strict` | When true, a path that has a trailing slash will only match a `location.pathname` with a trailing slash. This has no effect when there are additional URL segments in the pathname. | | `sensitive` | When true, will match if the path is case sensitive. | | `meta` | Page metadata overrides to apply to this view within the Admin Panel. [More details](/docs/v4/admin/metadata.md). | _\* An asterisk denotes that a property is required._ ### Adding New Views To add a _new_ view to the [Admin Panel](/docs/v4/admin/overview.md), simply add your own key to the `views` object. This is true for all view scopes. New views require at least the `Component` and `path` properties: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { components: { views: { // highlight-start myCustomView: { Component: '/path/to/MyCustomView#MyCustomViewComponent', path: '/my-custom-view', }, // highlight-end }, }, }, }) ``` > **Note:** Routes are cascading, so unless explicitly given the `exact` > property, they will match on URLs that simply _start_ with the route's path. > This is helpful when creating catch-all routes in your application. > Alternatively, define your nested route _before_ your parent route. ## Building Custom Views Custom Views are simply [Custom Components](/docs/v4/custom-components/overview.md) rendered at the page-level. Custom Views can either [replace existing views](#replacing-views) or [add entirely new ones](#adding-new-views). The process is generally the same regardless of the type of view you are customizing. To understand how to build Custom Views, first review the [Building Custom Components](/docs/v4/custom-components/overview.md#building-custom-components) guide. Once you have a Custom Component ready, you can use it as a Custom View. ```ts import type { CollectionConfig } from 'payload' export const MyCollectionConfig: CollectionConfig = { // ... admin: { components: { views: { // highlight-start edit: { Component: '/path/to/MyCustomView', // highlight-line }, // highlight-end }, }, }, } ``` ### Default Props Your Custom Views will be provided with the following props: | Prop | Description | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `initPageResult` | An object containing `req`, `payload`, `permissions`, etc. | | `clientConfig` | The Client Config object. [More details](/docs/v4/custom-components/overview.md#accessing-the-payload-config). | | `importMap` | The import map object. | | `params` | An object containing the [Dynamic Route Parameters](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes). | | `searchParams` | An object containing the [Search Parameters](https://developer.mozilla.org/docs/Learn/Common_questions/What_is_a_URL#parameters). | | `doc` | The document being edited. Only available in Document Views. [More details](/docs/v4/custom-components/document-views.md). | | `i18n` | The [i18n](/docs/v4/configuration/i18n.md) object. | | `payload` | The [Payload](/docs/v4/local-api/overview.md) class. | > **Note:** The props above apply to **Root Views** and any view added at the > root level. Collection List Views and Collection/Global Edit Views receive a > different set of props specific to their context. See [List View > Props](/docs/v4/custom-components/list-view.md#props) and [Document Views](/docs/v4/custom-components/document-views.md) for their > respective prop references. Here is an example of a Custom View component: ```tsx import type { AdminViewServerProps } from 'payload' import { Gutter } from '@payloadcms/ui' import React from 'react' export function MyCustomView(props: AdminViewServerProps) { return (

Custom Default Root View

This view uses the Default Template.

) } ``` > **Tip:** For consistent layout and navigation, you may want to wrap your > Custom View with one of the built-in [Templates](#view-templates). ### View Templates Your Custom Root Views can optionally use one of the templates that Payload provides. The most common of these is the Default Template which provides the basic layout and navigation. Here is an example of how to use the Default Template in your Custom View: ```tsx import type { AdminViewServerProps } from 'payload' import { Gutter } from '@payloadcms/ui' import { DefaultTemplate } from '@payloadcms/ui/rsc' import React from 'react' export function MyCustomView({ initPageResult, params, searchParams, user, }: AdminViewServerProps) { return (

Custom Default Root View

This view uses the Default Template.

) } ``` The top-level `user` has field read access applied and should be used for values passed toward client-facing components, so restricted data never reaches the client. `initPageResult.req.user` retains the complete server-side identity and should be used for access-control checks and Local API queries — do not use it for values sent to the client, and do not rely on the filtered `user` for authorization decisions. See [Server principal vs. read-filtered user](/docs/v4/custom-components/overview.md#server-principal-vs-read-filtered-user) for details. ### Securing Custom Views All Custom Views are public by default. It's up to you to secure your custom views. If your view requires a user to be logged in or to have certain access rights, you should handle that within your view component yourself. Here is how you might secure a Custom View: ```tsx import type { AdminViewServerProps } from 'payload' import { Gutter } from '@payloadcms/ui' import React from 'react' export function MyCustomView({ initPageResult }: AdminViewServerProps) { const { req: { user }, } = initPageResult if (!user) { return

You must be logged in to view this page.

} return (

Custom Default Root View

This view uses the Default Template.

) } ``` ## Root Views Root Views are the main views of the [Admin Panel](/docs/v4/admin/overview.md). These are views that are scoped directly under the `/admin` route, such as the Dashboard or Account views. To [swap out](#replacing-views) Root Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property at the root of your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { components: { views: { // highlight-start dashboard: { Component: '/path/to/Dashboard', }, // highlight-end // Other options include: // - account // - [key: string] // See below for more details }, }, }, }) ``` _For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._ The following options are available: | Property | Description | | ----------- | ----------------------------------------------------------------------------------------------- | | `account` | The Account view is used to show the currently logged in user's Account page. | | `dashboard` | The main landing page of the Admin Panel. | | `[key]` | Any other key can be used to add a completely new Root View. [More details](#adding-new-views). | ## Collection Views Collection Views are views that are scoped under the `/collections` route, such as the Collection List and Document Edit views. To [swap out](#replacing-views) Collection Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property of your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollectionConfig: CollectionConfig = { // ... admin: { components: { views: { // highlight-start edit: { default: { Component: '/path/to/MyCustomCollectionView', }, }, // highlight-end // Other options include: // - list // - [key: string] // See below for more details }, }, }, } ``` > **Reminder:** The `edit` key is comprised of various nested views, known as > Document Views, that relate to the same Collection Document. [More > details](/docs/v4/custom-components/document-views.md). The following options are available: | Property | Description | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `edit` | The Edit View corresponds to a single Document for any given Collection and consists of various nested views. [More details](/docs/v4/custom-components/document-views.md). | | `list` | The List View is used to show a list of Documents for any given Collection. [More details](/docs/v4/custom-components/list-view.md). | | `[key]` | Any other key can be used to add a completely new Collection View. [More details](#adding-new-views). | _For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._ ## Global Views Global Views are views that are scoped under the `/globals` route, such as the Edit View. To [swap out](#replacing-views) Global Views with your own or [create entirely new ones](#adding-new-views), use the `admin.components.views` property in your [Global Config](/docs/v4/configuration/globals.md): ```ts import type { SanitizedGlobalConfig } from 'payload' export const MyGlobalConfig: SanitizedGlobalConfig = { // ... admin: { components: { views: { // highlight-start edit: { default: { Component: '/path/to/MyCustomGlobalView', }, }, // highlight-end // Other options include: // - [key: string] // See below for more details }, }, }, } ``` > **Reminder:** The `edit` key is comprised of various nested views, known as > Document Views, that relate to the same Global Document. [More > details](/docs/v4/custom-components/document-views.md). The following options are available: | Property | Description | | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `edit` | The Edit View represents a single Document for any given Global and consists of various nested views. [More details](/docs/v4/custom-components/document-views.md). | | `[key]` | Any other key can be used to add a completely new Global View. [More details](#adding-new-views). | _For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._ # Dashboard Widgets Source: https://payloadcms.com/docs/beta/custom-components/dashboard > **Warning** > > This new Modular Dashboard is an experimental feature and may change in future > releases. Use at your own risk. The Dashboard is the first page users see when they log into the Payload Admin Panel. By default, it displays cards with the collections and globals. You can customize the dashboard by adding **widgets** - modular components that can display data, analytics, or any other content. [Video: Create a flexible admin panel with the new Modular Dashboard feature](https://www.youtube.com/watch?v=fXF34Ef6G84) One of the coolest things about widgets is that each plugin can define its own. Some examples: - Analytics - Error Reporting - Number of documents that meet a certain filter - Jobs recently executed ### Defining Widgets Define widgets in your Payload config using the `admin.dashboard.widgets` property: ```ts import { buildConfig } from 'payload' export default buildConfig({ admin: { dashboard: { widgets: [ { slug: 'sales-summary', Component: './components/SalesSummary.tsx#default', fields: [ { name: 'title', type: 'text' }, { name: 'timeframe', type: 'select', options: ['daily', 'weekly', 'monthly', 'yearly'], }, { name: 'showTrend', type: 'checkbox' }, ], minWidth: 'small', maxWidth: 'medium', }, ], }, }, }) ``` ### Widget Configuration | Property | Type | Description | | -------------- | ------------- | -------------------------------------------------------------------- | | `slug` \* | `string` | Unique identifier for the widget | | `Component` \* | `string` | Path to the widget component (supports `#` syntax for named exports) | | `fields` | `Field[]` | Optional widget-specific form fields shown in the edit drawer | | `minWidth` | `WidgetWidth` | Minimum width the widget can be resized to (default: `'x-small'`) | | `maxWidth` | `WidgetWidth` | Maximum width the widget can be resized to (default: `'full'`) | **WidgetWidth Values:** `'x-small' | 'small' | 'medium' | 'large' | 'x-large' | 'full'`. ### Creating a Widget Component Widgets are React Server Components that receive `WidgetServerProps`: ```tsx import type { WidgetServerProps } from 'payload' export default async function UserStatsWidget({ req }: WidgetServerProps) { const { payload } = req // Fetch data server-side const userCount = await payload.count({ collection: 'users', overrideAccess: false, req, }) return (

Total Users

{userCount.totalDocs}

) } ``` For visual consistency with the Payload UI, we recommend: 1. Use the `card` class for card-style widget roots. It applies the same theme-aware surface, border, radius, and padding as Payload's built-in dashboard cards; omit it for unframed content. 2. Use our theme variables for backgrounds and text colors. For example, use `var(--color-bg)` for backgrounds and `var(--color-text)` for text colors. ### Default Layout Control the initial dashboard layout with the `defaultLayout` property: ```ts export default buildConfig({ admin: { dashboard: { defaultLayout: ({ req }) => { // Customize layout based on user role or other factors const isAdmin = req.user?.roles?.includes('admin') return [ { widgetSlug: 'collections', width: 'full' }, { widgetSlug: 'sales-summary', data: { timeframe: 'monthly', title: 'Revenue Overview', }, width: isAdmin ? 'medium' : 'small', }, { widgetSlug: 'user-stats', width: isAdmin ? 'medium' : 'full' }, { widgetSlug: 'revenue-chart', width: 'full' }, ] }, widgets: [ // ... widget definitions ], }, }, }) ``` The `defaultLayout` function receives the request object and should return an array of `WidgetInstance` objects. If your widget has `fields`, you can type `widgetData` with generated widget types: ```tsx import type { WidgetServerProps } from 'payload' import type { SalesSummaryWidget } from '../payload-types' export default async function SalesSummaryWidgetComponent({ widgetData, }: WidgetServerProps) { const title = widgetData?.title ?? 'Sales Summary' const timeframe = widgetData?.timeframe ?? 'monthly' return (

{title} ({timeframe})

) } ``` #### WidgetInstance Type | Property | Type | Description | | --------------- | ------------- | ---------------------------------------------------- | | `widgetSlug` \* | `string` | Slug of the widget to display | | `data` | `object` | Optional widget-specific data passed to `widgetData` | | `width` | `WidgetWidth` | Initial width of the widget (default: minWidth) | `width` is constrained by each widget's `minWidth` and `maxWidth` when types are generated. > **Tip:** Users can customize their dashboard layout, which is saved to their > preferences. The `defaultLayout` is only used for first-time visitors or after > a layout reset. ### Built-in Widgets Payload includes built-in `welcome`, `activity`, `collections`, and `collection-query` widgets. The `activity` widget has Recently Viewed and Pinned tabs. It opens on Pinned when the user has pinned documents, and on Recently Viewed otherwise. It loads the visible page on demand and displays a single grid row, with up to four documents per page. Its collection filter applies only to Recently Viewed. The `collections` widget displays collection and global cards. The `collection-query` widget displays documents from a selected collection. If you don't define a `defaultLayout`, `welcome`, `activity`, and `collections` appear in that order, each at full width. The `collection-query` widget is available through the **Add +** menu. ### User Customization Users can customize their dashboard by: 1. Clicking the dashboard dropdown in the breadcrumb 2. Selecting "Edit Dashboard" 3. Adding widgets via the "Add +" button 4. Editing widget data (for widgets with `fields`) via the edit button 5. Resizing widgets using the width dropdown on each widget (if multiple widths are allowed) 6. Reordering widgets via drag-and-drop 7. Deleting widgets using the delete button 8. Saving changes or canceling to revert Users can also reset their dashboard to the default layout using the "Reset Layout" option. # Document Views Source: https://payloadcms.com/docs/beta/custom-components/document-views Document Views consist of multiple, individual views that together represent any single [Collection](/docs/v4/configuration/collections.md) or [Global](/docs/v4/configuration/globals.md) Document. All Document Views are scoped under the `/collections/:collectionSlug/:id` or the `/globals/:globalSlug` route, respectively. There are a number of default Document Views, such as the [Edit View](/docs/v4/custom-components/edit-view.md) and API View, but you can also create [entirely new views](/docs/v4/custom-components/custom-views.md#adding-new-views) as needed. All Document Views share a layout and can be given their own tab-based navigation, if desired. To customize Document Views, use the `admin.components.views.edit[key]` property in your [Collection Config](/docs/v4/configuration/collections.md) or [Global Config](/docs/v4/configuration/globals.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollectionOrGlobalConfig: CollectionConfig = { // ... admin: { components: { views: { // highlight-start edit: { default: { Component: '/path/to/MyCustomEditView', }, // Other options include: // - root // - api // - versions // - version // - [key: string] // See below for more details }, // highlight-end }, }, }, } ``` ## Config Options The following options are available: | Property | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `root` | The Root View overrides all other nested views and routes. No document controls or tabs are rendered when this key is set. [More details](#document-root). | | `default` | The Default View is the primary view in which your document is edited. It is rendered within the "Edit" tab. [More details](/docs/v4/custom-components/edit-view.md). | | `versions` | The Versions View is used to navigate the version history of a single document. It is rendered within the "Versions" tab. [More details](/docs/v4/versions/overview.md). | | `version` | The Version View is used to edit a single version of a document. It is rendered within the "Version" tab. [More details](/docs/v4/versions/overview.md). | | `api` | The API View is used to display the REST API JSON response for a given document. It is rendered within the "API" tab. | | `livePreview` | The LivePreview view is used to display the Live Preview interface. It is rendered within the "Live Preview" tab. [More details](/docs/v4/live-preview/overview.md). | | `[key]` | Any other key can be used to add a completely new Document View. | _For details on how to build Custom Views, including all available props, see [Building Custom Views](/docs/v4/custom-components/custom-views.md#building-custom-views)._ ### Document Root The Document Root is mounted on the top-level route for a Document. Setting this property will completely take over the entire Document View layout, including the title, [Document Tabs](#document-tabs), _and all other nested Document Views_ including the [Edit View](/docs/v4/custom-components/edit-view.md), API View, etc. When setting a Document Root, you are responsible for rendering all necessary components and controls, as no document controls or tabs would be rendered. To replace only the Edit View precisely, use the `edit.default` key instead. To override the Document Root, use the `views.edit.root` property in your [Collection Config](/docs/v4/configuration/collections.md) or [Global Config](/docs/v4/configuration/globals.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { slug: 'my-collection', admin: { components: { views: { edit: { // highlight-start root: { Component: '/path/to/MyCustomRootComponent', // highlight-line }, // highlight-end }, }, }, }, } ``` ### Edit View The Edit View is where users interact with individual Collection and Global Documents. This is where they can view, edit, and save their content. The Edit View is keyed under the `default` property in the `views.edit` object. For more information on customizing the Edit View, see the [Edit View](/docs/v4/custom-components/edit-view.md) documentation. ## Document Tabs Each Document View can be given a tab for navigation, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Document View, use the `views.edit.[key].tab` property in your [Collection Config](/docs/v4/configuration/collections.md) or [Global Config](/docs/v4/configuration/globals.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { slug: 'my-collection', admin: { components: { views: { edit: { myCustomView: { Component: '/path/to/MyCustomView', path: '/my-custom-tab', // highlight-start tab: { Component: '/path/to/MyCustomTabComponent', }, // highlight-end }, anotherCustomView: { Component: '/path/to/AnotherCustomView', path: '/another-custom-view', // highlight-start tab: { label: 'Another Custom View', href: '/another-custom-view', order: '100', }, // highlight-end }, }, }, }, }, } ``` > **Note:** This applies to _both_ Collections _and_ Globals. The following options are available for tabs: | Property | Description | | ----------- | ------------------------------------------------------------------------------------------------------------- | | `label` | The label to display in the tab. | | `href` | The URL to navigate to when the tab is clicked. This is optional and defaults to the tab's `path`. | | `order` | The order in which the tab appears in the navigation. Can be set on default and custom tabs. | | `Component` | The component to render in the tab. This can be a Server or Client component. [More details](#tab-components) | ### Tab Components If changing the label or href is not enough, you can also replace the entire tab component with your own custom component. This can be done by setting the `tab.Component` property to the path of your custom component. Here is an example of how to scaffold a custom Document Tab: #### Server Component ```tsx import React from 'react' import type { DocumentTabServerProps } from 'payload' import { Link } from '@payloadcms/ui' export function MyCustomTabComponent(props: DocumentTabServerProps) { return ( This is a custom Document Tab (Server) ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import type { DocumentTabClientProps } from 'payload' import { Link } from '@payloadcms/ui' export function MyCustomTabComponent(props: DocumentTabClientProps) { return ( This is a custom Document Tab (Client) ) } ``` # Edit View Source: https://payloadcms.com/docs/beta/custom-components/edit-view The Edit View is where users interact with individual [Collection](/docs/v4/configuration/collections.md) and [Global](/docs/v4/configuration/globals.md) Documents within the [Admin Panel](/docs/v4/admin/overview.md). The Edit View contains the actual form that submits the data to the server. This is where they can view, edit, and save their content. It contains controls for saving, publishing, and previewing the document, all of which can be customized to a high degree. The Edit View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view. > **Note:** The Edit View is one of many [Document Views](/docs/v4/custom-components/document-views.md) in > the Payload Admin Panel. Each Document View is responsible for a different > aspect of interacting with a single Document. ## Custom Edit View To swap out the entire Edit View with a [Custom View](/docs/v4/custom-components/custom-views.md), use the `views.edit.default` property in your [Collection Config](/docs/v4/configuration/collections.md) or [Global Config](/docs/v4/configuration/globals.md): ```tsx import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { components: { views: { edit: { // highlight-start default: { Component: '/path/to/MyCustomEditViewComponent', }, // highlight-end }, }, }, }, }) ``` Here is an example of a custom Edit View: #### Server Component ```tsx import React from 'react' import type { DocumentViewServerProps } from 'payload' export function MyCustomServerEditView(props: DocumentViewServerProps) { return
This is a custom Edit View (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { DocumentViewClientProps } from 'payload' export function MyCustomClientEditView(props: DocumentViewClientProps) { return
This is a custom Edit View (Client)
} ``` _For details on how to build Custom Views, including all available props, see [Building Custom Views](/docs/v4/custom-components/custom-views.md#building-custom-views)._ ## Custom Components In addition to swapping out the entire Edit View with a [Custom View](/docs/v4/custom-components/custom-views.md), you can also override individual components. This allows you to customize specific parts of the Edit View without swapping out the entire view. > **Note:** Collection and Globals share the same `admin.components.edit` key, > with slightly different options (e.g., `Upload` is Collection-only). Use the > same key, but be aware of which options apply to your entity. #### Collections To override Edit View components for a Collection, use the `admin.components.edit` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start edit: { // ... }, // highlight-end }, }, } ``` The following options are available: | Path | Description | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). | | `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document control bar. [More details](#editmenuitems). | | `SaveButton` | A button that saves the current document. [More details](#savebutton). | | `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). | | `PublishButton` | A button that publishes the current document. [More details](#publishbutton). | | `UnpublishButton` | A button that unpublishes the current document. [More details](#unpublishbutton). | | `PreviewButton` | A button that previews the current document. [More details](#previewbutton). | | `Status` | A component that represents the status of the current document. [More details](#status). | | `Upload` | A file upload component. [More details](#upload). | #### Globals To override Edit View components for Globals, use the `admin.components.edit` property in your [Global Config](/docs/v4/configuration/globals.md): ```ts import type { GlobalConfig } from 'payload' export const MyGlobal: GlobalConfig = { // ... admin: { components: { // highlight-start edit: { // ... }, // highlight-end }, }, } ``` The following options are available: | Path | Description | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). | | `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document control bar. [More details](#editmenuitems). | | `SaveButton` | A button that saves the current document. [More details](#savebutton). | | `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). | | `PublishButton` | A button that publishes the current document. [More details](#publishbutton). | | `UnpublishButton` | A button that unpublishes the current document. [More details](#unpublishbutton). | | `PreviewButton` | A button that previews the current document. [More details](#previewbutton). | | `Status` | A component that represents the status of the global. [More details](#status). | ### SaveButton The `SaveButton` component allows you to render a custom Save Button in the Edit View. #### Configuration Path - **Collections:** `admin.components.edit.SaveButton` - **Globals:** `admin.components.edit.SaveButton` #### Example for Collections ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', admin: { components: { edit: { // highlight-start SaveButton: '/path/to/CustomSaveButton', // highlight-end }, }, }, } ``` #### Example for Globals ```ts import type { GlobalConfig } from 'payload' export const Header: GlobalConfig = { slug: 'header', admin: { components: { edit: { // highlight-start SaveButton: '/path/to/CustomSaveButton', // highlight-end }, }, }, } ``` Here's an example of a custom `SaveButton` component: #### Server Component ```tsx import React from 'react' import { SaveButton } from '@payloadcms/ui' import type { SaveButtonServerProps } from 'payload' export function MySaveButton(props: SaveButtonServerProps) { return } ``` #### Client Component ```tsx 'use client' import React from 'react' import { SaveButton } from '@payloadcms/ui' import type { SaveButtonClientProps } from 'payload' export function MySaveButton(props: SaveButtonClientProps) { return } ``` ### beforeDocumentControls The `beforeDocumentControls` property allows you to render custom components just before the default document action buttons (like Save, Publish, or Preview). This is useful for injecting custom buttons, status indicators, or any other UI elements before the built-in controls. To add `beforeDocumentControls` components, use the `components.edit.beforeDocumentControls` property in your [Collection Config](/docs/v4/configuration/collections.md) or [Global Config](/docs/v4/configuration/globals.md): #### Collections ``` export const MyCollection: CollectionConfig = { admin: { components: { edit: { // highlight-start beforeDocumentControls: ['/path/to/CustomComponent'], // highlight-end }, }, }, } ``` #### Globals ``` export const MyGlobal: GlobalConfig = { admin: { components: { edit: { // highlight-start beforeDocumentControls: ['/path/to/CustomComponent'], // highlight-end }, }, }, } ``` Here's an example of a custom `beforeDocumentControls` component: #### Server Component ```tsx import React from 'react' import type { BeforeDocumentControlsServerProps } from 'payload' export function MyCustomDocumentControlButton( props: BeforeDocumentControlsServerProps, ) { return
This is a custom beforeDocumentControl button (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { BeforeDocumentControlsClientProps } from 'payload' export function MyCustomDocumentControlButton( props: BeforeDocumentControlsClientProps, ) { return
This is a custom beforeDocumentControl button (Client)
} ``` ### editMenuItems The `editMenuItems` property allows you to inject custom components into the 3-dot menu dropdown located in the document controls bar. This dropdown contains default options including `Create New`, `Duplicate`, `Delete`, and other options when additional features are enabled. Any custom components you add will appear below these default items. To add `editMenuItems`, use the `components.edit.editMenuItems` property in your [Collection Config](/docs/v4/configuration/collections.md): #### Config Example ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', admin: { components: { edit: { // highlight-start editMenuItems: ['/path/to/CustomEditMenuItem'], // highlight-end }, }, }, } ``` Here's an example of a custom `editMenuItems` component: #### Server Component ```tsx import React from 'react' import type { EditMenuItemsServerProps } from 'payload' export const EditMenuItems = async (props: EditMenuItemsServerProps) => { const href = `/custom-action?id=${props.id}` return ( <> Custom Edit Menu Item Another Custom Edit Menu Item - add as many as you need! ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { PopupList } from '@payloadcms/ui' import type { EditMenuItemsClientProps } from 'payload' export const EditMenuItems = (props: EditMenuItemsClientProps) => { const handleClick = () => { console.log('Custom button clicked!') } return ( Custom Edit Menu Item Another Custom Edit Menu Item - add as many as you need! ) } ``` > **Styling:** Use Payload's built-in `PopupList.Button` to ensure your menu > items automatically match the default dropdown styles. If you want a different > look, you can customize the appearance by passing your own `className` to > `PopupList.Button`, or use a completely custom button built with a standard > HTML `button` element or any other component that fits your design > preferences. ### SaveDraftButton The `SaveDraftButton` component allows you to render a custom Save Draft Button in the Edit View. #### Configuration Path - **Collections:** `admin.components.edit.SaveDraftButton` - **Globals:** `admin.components.edit.SaveDraftButton` #### Example for Collections ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', versions: { drafts: true, }, admin: { components: { edit: { // highlight-start SaveDraftButton: '/path/to/CustomSaveDraftButton', // highlight-end }, }, }, } ``` #### Example for Globals ```ts import type { GlobalConfig } from 'payload' export const Header: GlobalConfig = { slug: 'header', versions: { drafts: true, }, admin: { components: { edit: { // highlight-start SaveDraftButton: '/path/to/CustomSaveDraftButton', // highlight-end }, }, }, } ``` Here's an example of a custom `SaveDraftButton` component: #### Server Component ```tsx import React from 'react' import { SaveDraftButton } from '@payloadcms/ui' import type { SaveDraftButtonServerProps } from 'payload' export function MySaveDraftButton(props: SaveDraftButtonServerProps) { return } ``` #### Client Component ```tsx 'use client' import React from 'react' import { SaveDraftButton } from '@payloadcms/ui' import type { SaveDraftButtonClientProps } from 'payload' export function MySaveDraftButton(props: SaveDraftButtonClientProps) { return } ``` ### PublishButton The `PublishButton` component allows you to render a custom Publish Button in the Edit View. #### Configuration Path - **Collections:** `admin.components.edit.PublishButton` - **Globals:** `admin.components.edit.PublishButton` #### Example for Collections ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', versions: { drafts: true, }, admin: { components: { edit: { // highlight-start PublishButton: '/path/to/CustomPublishButton', // highlight-end }, }, }, } ``` #### Example for Globals ```ts import type { GlobalConfig } from 'payload' export const Header: GlobalConfig = { slug: 'header', versions: { drafts: true, }, admin: { components: { edit: { // highlight-start PublishButton: '/path/to/CustomPublishButton', // highlight-end }, }, }, } ``` Here's an example of a custom `PublishButton` component: #### Server Component ```tsx import React from 'react' import { PublishButton } from '@payloadcms/ui' import type { PublishButtonServerProps } from 'payload' export function MyPublishButton(props: PublishButtonServerProps) { return } ``` #### Client Component ```tsx 'use client' import React from 'react' import { PublishButton } from '@payloadcms/ui' export function MyPublishButton() { return } ``` ### UnpublishButton The `UnpublishButton` property allows you to render a custom Unpublish Button in the Edit View. To add an `UnpublishButton` component, use the `components.edit.UnpublishButton` property in your [Collection Config](/docs/v4/configuration/collections.md) or [Global Config](/docs/v4/configuration/globals.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { edit: { // highlight-start UnpublishButton: '/path/to/MyUnpublishButton', // highlight-end }, }, }, } ``` Here's an example of a custom `UnpublishButton` component: #### Server Component ```tsx import React from 'react' import { UnpublishButton } from '@payloadcms/ui' import type { UnpublishButtonServerProps } from 'payload' export function MyUnpublishButton(props: UnpublishButtonServerProps) { return } ``` #### Client Component ```tsx 'use client' import React from 'react' import { UnpublishButton } from '@payloadcms/ui' export function MyUnpublishButton() { return } ``` ### PreviewButton The `PreviewButton` component allows you to render a custom Preview Button in the Edit View. #### Configuration Path - **Collections:** `admin.components.edit.PreviewButton` - **Globals:** `admin.components.edit.PreviewButton` #### Example for Collections ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', admin: { preview: () => 'https://example.com/preview', components: { edit: { // highlight-start PreviewButton: '/path/to/CustomPreviewButton', // highlight-end }, }, }, } ``` #### Example for Globals ```ts import type { GlobalConfig } from 'payload' export const Header: GlobalConfig = { slug: 'header', admin: { preview: () => 'https://example.com/preview', components: { edit: { // highlight-start PreviewButton: '/path/to/CustomPreviewButton', // highlight-end }, }, }, } ``` Here's an example of a custom `PreviewButton` component: #### Server Component ```tsx import React from 'react' import { PreviewButton } from '@payloadcms/ui' import type { PreviewButtonServerProps } from 'payload' export function MyPreviewButton(props: PreviewButtonServerProps) { return } ``` #### Client Component ```tsx 'use client' import React from 'react' import { PreviewButton } from '@payloadcms/ui' import type { PreviewButtonClientProps } from 'payload' export function MyPreviewButton(props: PreviewButtonClientProps) { return } ``` ### Description The `Description` property allows you to render a custom description of the Collection or Global in the Edit View. To add a `Description` component, use the `components.Description` property in your [Collection Config](/docs/v4/configuration/collections.md) or [Global Config](/docs/v4/configuration/globals.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start Description: '/path/to/MyDescriptionComponent', // highlight-end }, }, } ``` > **Note:** The `Description` component is shared between the Edit View and the > [List View](/docs/v4/custom-components/list-view.md). Here's an example of a custom `Description` component: #### Server Component ```tsx import React from 'react' import type { ViewDescriptionServerProps } from 'payload' export function MyDescriptionComponent(props: ViewDescriptionServerProps) { return
This is a custom description component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { ViewDescriptionClientProps } from 'payload' export function MyDescriptionComponent(props: ViewDescriptionClientProps) { return
This is a custom description component (Client)
} ``` ### Status The `Status` component displays the document's draft/published state when versioning with drafts is enabled. #### Configuration Path - **Collections:** `admin.components.edit.Status` - **Globals:** `admin.components.edit.Status` #### Example for Collections ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', versions: { drafts: true, }, admin: { components: { edit: { // highlight-start Status: '/path/to/CustomStatus', // highlight-end }, }, }, } ``` #### Example for Globals ```ts import type { GlobalConfig } from 'payload' export const Header: GlobalConfig = { slug: 'header', versions: { drafts: true, }, admin: { components: { edit: { // highlight-start Status: '/path/to/CustomStatus', // highlight-end }, }, }, } ``` ### Upload The `Upload` property allows you to render a custom file upload component in the Edit View. This is only available for upload-enabled Collections. To add an `Upload` component, use the `components.edit.Upload` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: true, admin: { components: { edit: { // highlight-start Upload: '/path/to/MyUploadComponent#MyUploadServer', // highlight-end }, }, }, } ``` > **Important:** Custom upload components must integrate with Payload's form > system to work correctly. You cannot use a simple `` as > it won't connect to Payload's upload API. Instead, use Payload's built-in > `` component from `@payloadcms/ui` or properly integrate with form > hooks like `useDocumentInfo()`. Here's an example of a custom `Upload` component: #### Server Component ```tsx import React from 'react' import type { PayloadServerReactComponent, SanitizedCollectionConfig, } from 'payload' import { CustomUploadClient } from './MyUploadComponent.client' export const MyUploadServer: PayloadServerReactComponent< SanitizedCollectionConfig['admin']['components']['edit']['Upload'] > = (props) => { return (

Custom Upload Interface

) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { Upload, useDocumentInfo } from '@payloadcms/ui' export const CustomUploadClient = () => { const { collectionSlug, docConfig, initialState } = useDocumentInfo() return ( ) } ``` For more details on customizing upload components, including examples with custom actions and drawers, see the [Upload documentation](/docs/v4/upload/overview.md#customizing-the-upload-ui). # List View Source: https://payloadcms.com/docs/beta/custom-components/list-view The List View is where users interact with a list of [Collection](/docs/v4/configuration/collections.md) Documents within the [Admin Panel](/docs/v4/admin/overview.md). This is where they can view, sort, filter, and paginate their documents to find exactly what they're looking for. This is also where users can perform bulk operations on multiple documents at once, such as deleting, editing, or publishing many. The List View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view. > **Note:** Only [Collections](/docs/v4/configuration/collections.md) have a List View. > [Globals](/docs/v4/configuration/globals.md) do not have a List View as they are single > documents. ## Custom List View To swap out the entire List View with a [Custom View](/docs/v4/custom-components/custom-views.md), use the `admin.components.views.list` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { views: { // highlight-start list: { Component: '/path/to/MyCustomListView', }, // highlight-end }, }, }, } ``` Here is an example of a custom List View: #### Server Component ```tsx import React from 'react' import type { ListViewServerProps } from 'payload' export function MyCustomServerListView(props: ListViewServerProps) { return
This is a custom List View (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { ListViewClientProps } from 'payload' export function MyCustomClientListView(props: ListViewClientProps) { return
This is a custom List View (Client)
} ``` ### Props Custom List Views receive a different set of props than other views because Payload pre-fetches data and pre-renders components before passing the results to your component. The exact props available depend on whether you are building a Server or Client Component. #### Server Component Props (`ListViewServerProps`) Server components receive all [client props](#client-component-props-listviewclientprops) plus the following additional server-only data: | Prop | Description | | ---------------------- | ------------------------------------------------------------------------------------- | | `collectionConfig` | The sanitized [Collection Config](/docs/v4/configuration/collections.md) for this collection. | | `data` | The paginated query result, including `docs`, `totalDocs`, `page`, `totalPages`, etc. | | `limit` | The number of documents displayed per page. | | `listPreferences` | Saved user preferences for this list (columns, sort, etc.). | | `listSearchableFields` | The fields configured as searchable in the collection's admin options. | | `i18n` | The [i18n](/docs/v4/configuration/i18n.md) object. | | `locale` | The active locale, if localization is enabled. | | `params` | Route parameters from the framework's dynamic route. | | `payload` | The [Payload](/docs/v4/local-api/overview.md) class. | | `permissions` | The current user's permissions. | | `searchParams` | URL search parameters (page, sort, where, limit, etc.). | | `user` | The currently authenticated user. | #### Client Component Props (`ListViewClientProps`) | Prop | Description | | ----------------------- | ------------------------------------------------------------------------------------------------- | | `collectionSlug` | The slug of the collection being displayed. | | `columnState` | An array of column definitions describing the current table columns and their state. | | `disableBulkDelete` | When `true`, hides the bulk delete action. | | `disableBulkEdit` | When `true`, hides the bulk edit action. | | `disableQueryPresets` | When `true`, disables the query presets feature. | | `enableRowSelections` | When `true`, enables checkboxes on each row for bulk actions. | | `hasCreatePermission` | Whether the current user has permission to create new documents. | | `hasDeletePermission` | Whether the current user has permission to delete documents. | | `hasTrashPermission` | Whether the current user has permission to use the trash. | | `newDocumentURL` | The URL to navigate to when creating a new document. | | `renderedFilters` | A `Map` of pre-rendered filter components, keyed by field path. | | `resolvedFilterOptions` | A `Map` of resolved options for relationship filter fields. | | `viewType` | The current view type identifier. | | `Table` | A pre-rendered React node of the documents table. Pass this through to render the built-in table. | | `AfterList` | Slot for components rendered after the list. | | `AfterListTable` | Slot for components rendered after the table. | | `BeforeList` | Slot for components rendered before the list. | | `BeforeListTable` | Slot for components rendered before the table. | | `Description` | Slot for the collection description component. | | `listMenuItems` | Slot for custom items rendered in the list controls menu. | > **Note:** Custom List Views are rendered inside a `ListQueryProvider`. This > means any Client Component in the tree can call `useListQuery()` from > `@payloadcms/ui` to read or update the current query state (search term, sort, > page, filters, etc.). ### Using DefaultListView If you want to keep Payload's built-in table and controls but add your own layout around them, import and render `DefaultListView` from `@payloadcms/ui`: ```tsx 'use client' import React from 'react' import type { ListViewClientProps } from 'payload' import { DefaultListView } from '@payloadcms/ui' export function MyCustomClientListView(props: ListViewClientProps) { return (

My Custom Header

) } ``` ## Custom Components In addition to swapping out the entire List View with a [Custom View](/docs/v4/custom-components/custom-views.md), you can also override individual components. This allows you to customize specific parts of the List View without swapping out the entire view for your own. To override List View components for a Collection, use the `admin.components` property in your [Collection Config](/docs/v4/configuration/collections.md): ### Slot Props All slot components (`beforeList`, `beforeListTable`, `afterList`, `afterListTable`) receive the same base set of client props plus additional server-only props in Server Components: **Client Props** (`BeforeListClientProps`, `AfterListClientProps`, etc.): | Prop | Description | | --------------------- | ---------------------------------------------------------------- | | `collectionSlug` | The slug of the collection being displayed. | | `hasCreatePermission` | Whether the current user has permission to create new documents. | | `hasDeletePermission` | Whether the current user has permission to delete documents. | | `hasTrashPermission` | Whether the current user has permission to use the trash. | | `newDocumentURL` | The URL to navigate to when creating a new document. | **Additional Server-Only Props** (`BeforeListServerProps`, `AfterListServerProps`, etc.): | Prop | Description | | ---------------------- | ------------------------------------------------------------------------------------- | | `collectionConfig` | The sanitized [Collection Config](/docs/v4/configuration/collections.md) for this collection. | | `data` | The paginated query result, including `docs`, `totalDocs`, `page`, `totalPages`, etc. | | `limit` | The number of documents displayed per page. | | `listPreferences` | Saved user preferences for this list (columns, sort, etc.). | | `listSearchableFields` | The fields configured as searchable in the collection's admin options. | | `i18n` | The [i18n](/docs/v4/configuration/i18n.md) object. | | `payload` | The [Payload](/docs/v4/local-api/overview.md) class. | | `permissions` | The current user's permissions. | | `user` | The currently authenticated user. | ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { // highlight-start components: { // ... }, // highlight-end }, } ``` The following options are available: | Path | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------- | | `beforeList` | An array of custom components to inject before the list of documents in the List View. [More details](#beforelist). | | `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforelisttable). | | `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterlist). | | `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterlisttable). | | `listMenuItems` | An array of components to render within a menu next to the List Controls (after the Columns and Filters options) | | `Description` | A component to render a description of the Collection. [More details](#description). | ### beforeList The `beforeList` property allows you to inject custom components before the list of documents in the List View. To add `beforeList` components, use the `components.beforeList` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start beforeList: ['/path/to/MyBeforeListComponent'], // highlight-end }, }, } ``` Here's an example of a custom `beforeList` component: #### Server Component ```tsx import React from 'react' import type { BeforeListServerProps } from 'payload' export function MyBeforeListComponent(props: BeforeListServerProps) { return
This is a custom beforeList component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { BeforeListClientProps } from 'payload' export function MyBeforeListComponent(props: BeforeListClientProps) { return
This is a custom beforeList component (Client)
} ``` ### beforeListTable The `beforeListTable` property allows you to inject custom components before the table of documents in the List View. To add `beforeListTable` components, use the `components.beforeListTable` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start beforeListTable: ['/path/to/MyBeforeListTableComponent'], // highlight-end }, }, } ``` Here's an example of a custom `beforeListTable` component: #### Server Component ```tsx import React from 'react' import type { BeforeListTableServerProps } from 'payload' export function MyBeforeListTableComponent(props: BeforeListTableServerProps) { return
This is a custom beforeListTable component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { BeforeListTableClientProps } from 'payload' export function MyBeforeListTableComponent(props: BeforeListTableClientProps) { return
This is a custom beforeListTable component (Client)
} ``` ### afterList The `afterList` property allows you to inject custom components after the list of documents in the List View. To add `afterList` components, use the `components.afterList` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start afterList: ['/path/to/MyAfterListComponent'], // highlight-end }, }, } ``` Here's an example of a custom `afterList` component: #### Server Component ```tsx import React from 'react' import type { AfterListServerProps } from 'payload' export function MyAfterListComponent(props: AfterListServerProps) { return
This is a custom afterList component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { AfterListClientProps } from 'payload' export function MyAfterListComponent(props: AfterListClientProps) { return
This is a custom afterList component (Client)
} ``` ### afterListTable The `afterListTable` property allows you to inject custom components after the table of documents in the List View. To add `afterListTable` components, use the `components.afterListTable` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start afterListTable: ['/path/to/MyAfterListTableComponent'], // highlight-end }, }, } ``` Here's an example of a custom `afterListTable` component: #### Server Component ```tsx import React from 'react' import type { AfterListTableServerProps } from 'payload' export function MyAfterListTableComponent(props: AfterListTableServerProps) { return
This is a custom afterListTable component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { AfterListTableClientProps } from 'payload' export function MyAfterListTableComponent(props: AfterListTableClientProps) { return
This is a custom afterListTable component (Client)
} ``` ### Description The `Description` property allows you to render a custom description of the Collection in the List View. To add a `Description` component, use the `components.Description` property in your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start Description: '/path/to/MyDescriptionComponent', // highlight-end }, }, } ``` > **Note:** The `Description` component is shared between the List View and the > [Edit View](/docs/v4/custom-components/edit-view.md). Here's an example of a custom `Description` component: #### Server Component ```tsx import React from 'react' import type { ViewDescriptionServerProps } from 'payload' export function MyDescriptionComponent(props: ViewDescriptionServerProps) { return
This is a custom Collection description component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { ViewDescriptionClientProps } from 'payload' export function MyDescriptionComponent(props: ViewDescriptionClientProps) { return
This is a custom Collection description component (Client)
} ``` # UI Components Source: https://payloadcms.com/docs/beta/ui-components/overview The `@payloadcms/ui` package contains the React components used throughout the Payload Admin Panel. You can use these components to build Custom Components that match the surrounding Admin Panel interface and behavior. Each component guide includes working examples for the selected version of Payload, common props, accessibility guidance, and any providers or setup the component requires. ## Components - [AnimateHeight](./animate-height): Animate a container as its content changes height. - [Banner](./banner): Display contextual notices, warnings, and errors. - [Button](./button): Trigger actions with Payload's standard button styles. - [Card](./card): Present a title with optional actions or full-surface interaction. - [Checkbox and Select Inputs](./checkbox-and-select-inputs): Collect boolean and option values with Payload field controls. - [Collapsible](./collapsible): Show and hide a section beneath a collapsible header. - [CopyToClipboard](./copy-to-clipboard): Copy text to the clipboard with built-in feedback. - [DatePicker](./date-picker): Select dates and times with Payload's styled calendar control. - [Document Actions](./document-actions): Save, publish, and perform bulk operations with Admin-aware controls. - [Document State](./document-state): Handle document locks, takeovers, and unsaved changes. - [Draggable Sortable](./draggable-sortable): Reorder custom interface content with pointer and keyboard controls. - [Dropzone](./dropzone): Accept files through drag, drop, and paste interactions. - [ErrorPill](./error-pill): Display a compact, localized validation error count. - [Field Labels and Messages](./field-labels-and-messages): Add labels, descriptions, and validation messages to custom fields. - [Gutter](./gutter): Align content with the Admin Panel's responsive gutters. - [Icons](./icons): Use Payload's interface icons in custom Admin Panel UI. - [Link](./link): Navigate between Admin routes through Payload's router adapter. - [List and Pagination Controls](./list-and-pagination-controls): Navigate paginated data and compose common Admin list controls. - [Motion and Loading](./motion-and-loading): Communicate loading and route transitions with Payload's motion helpers. - [Pill](./pill): Display compact metadata, categories, and statuses. - [PillSelector](./pill-selector): Select, deselect, and optionally reorder compact options. - [ReactSelect](./react-select): Select one or more options with Payload's react-select adapter. - [Relationship and Upload Inputs](./relationship-and-upload-inputs): Select related documents and media from custom fields. - [ShimmerEffect](./shimmer-effect): Show a loading placeholder while content resolves. - [Step Navigation](./step-navigation): Set the Admin header breadcrumbs from a Custom View. - [Table Cells](./table-cells): Render field values and build purpose-specific table cells. - [Text Inputs](./text-inputs): Build custom text and textarea field controls. - [Upload Helpers](./upload-helpers): Display file details, generated sizes, and image adjustments. > **Warning** > > UI component APIs can change between major versions of Payload. Use the > version selector to view examples that match the version installed in your > project. ## Using UI components Inside a Payload Admin Panel Custom Component, import components from `@payloadcms/ui`: ```tsx 'use client' import { Button } from '@payloadcms/ui' export function SaveButton() { return } ``` Some components depend on context provided by the Admin Panel. Each guide calls out those requirements when a component cannot be rendered independently. ## Customizing components The Design tab in each component example provides CSS you can use as a starting point. Add those overrides to the stylesheet loaded by your Admin Panel. ```css label="Next.js" /* app/(payload)/custom.css */ @layer payload { .btn--style-primary { --color-bg-brand: #6d5dfc; } } ``` ```css label="TanStack Start" /* src/routes/_payload/custom.css */ @layer payload { .btn--style-primary { --color-bg-brand: #6d5dfc; } } ``` Both frameworks use the same Payload selectors and design tokens. Only the stylesheet entry point differs. # AnimateHeight Source: https://payloadcms.com/docs/beta/ui-components/animate-height The `AnimateHeight` component animates its children between a collapsed height and their natural height. It is useful when building disclosure controls and expandable regions. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { AnimateHeight } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { AnimateHeight } from '@payloadcms/ui/elements/AnimateHeight' ``` ## Interactive example **Implementation** ```tsx const [isOpen, setIsOpen] = useState(true)
Expandable content
``` **Styling** AnimateHeight controls only the height transition. Apply visual styles to its child content so the same surface is used throughout the animation. ```css @layer payload { .expandable-content { padding: var(--spacer-3); color: var(--color-text); background: var(--color-bg-secondary); border: var(--stroke-width-small) solid var(--color-border); border-radius: var(--radius-medium); } } ``` - `--color-bg-secondary`: Expandable content background. - `--color-border`: Expandable content border. - `--radius-medium`: Expandable content corner radius. The control that changes `height` remains your responsibility. Give that control an accessible label and expose its expanded state with `aria-expanded` when it controls a disclosure region. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ------------- | ------------- | ------- | -------------------------------------------- | | `children` \* | `ReactNode` | — | Content whose height is animated. | | `height` | `0 \| 'auto'` | — | Uses `0` to collapse and `'auto'` to expand. | | `duration` | `number` | `300` | Animation duration in milliseconds. | | `id` | `string` | — | Sets the wrapper element ID. | | `className` | `string` | — | Adds a class to the wrapper element. | _\* An asterisk denotes that a prop is required._ # Banner Source: https://payloadcms.com/docs/beta/ui-components/banner The `Banner` component displays a short message that needs more emphasis than surrounding content. Use its type to communicate the purpose of the message. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { Banner } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { Banner } from '@payloadcms/ui/elements/Banner' ``` ## Basic usage **Implementation** ```tsx Review these changes before publishing. ``` **Styling** Override Banner tokens on the component to adjust neutral messages without changing the same semantic colors throughout the Admin Panel. ```css @layer payload { .banner { --color-bg-secondary: #f1f0ff; --color-text: #352f67; --radius-medium: 0.5rem; } } ``` - `--color-bg-secondary`: Default banner background. - `--color-text`: Default banner text and icon color. - `--radius-medium`: Banner corner radius. ## Types Use `brand` for product context, `success` to confirm an action, `warning` to flag a risk, and `danger` when something failed. Use the default type for neutral messages. **Implementation** ```tsx Default message Additional product context Changes saved successfully. Review this setting before continuing. Something went wrong. ``` **Styling** Target the class for each semantic Banner type to customize that message without changing unrelated uses of the same color token. ```css @layer payload { .banner--type-brand { --color-bg-brand-tertiary: #eeeaff; } .banner--type-success { --color-bg-success-tertiary: #e3f7e9; } .banner--type-warning { --color-bg-warning-tertiary: #fff2d8; } .banner--type-danger { --color-bg-danger-tertiary: #fde8e7; } } ``` - `--color-bg-brand-tertiary`: Brand banner background. - `--color-bg-success-tertiary`: Success banner background. - `--color-bg-warning-tertiary`: Warning banner background. - `--color-bg-danger-tertiary`: Danger banner background. Keep banner copy concise and include the next action when the user must resolve something. ## Accessibility - Use text that communicates the message without relying on color alone. - Include an icon only when it reinforces the meaning of the message. - Keep actionable banners keyboard accessible and give their action a clear purpose. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ----------- | ------------------------------------------------------------ | ----------- | ------------------------------------------- | | `children` | `ReactNode` | — | Message displayed inside the banner. | | `type` | `'default' \| 'brand' \| 'success' \| 'warning' \| 'danger'` | `'default'` | Sets the message style. | | `icon` | `ReactNode` | — | Displays an icon beside the message. | | `alignIcon` | `'left' \| 'right'` | `'left'` | Positions the icon. | | `onClick` | `(event: MouseEvent) => void` | — | Makes the banner actionable. | | `to` | `string` | — | Makes the banner navigate to the given URL. | # Button Source: https://payloadcms.com/docs/beta/ui-components/button The `Button` component lets a user perform an action or navigate to another location. Use its style to communicate the importance and consequence of the action. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { Button } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { Button } from '@payloadcms/ui/elements/Button' ``` ## Basic usage Use a primary button for the main action in a section or view. **Implementation** ```tsx ``` **Styling** Scope Payload 4 semantic color tokens to primary Buttons to customize the component without changing the global brand palette. ```css @layer payload { .btn--style-primary { --color-bg-brand: #6d5dfc; --color-bg-brand-hover: #5947e5; --color-bg-brand-pressed: #4938cc; --color-text-onbrand: #ffffff; } } ``` - `--color-bg-brand`: Primary button background. - `--color-bg-brand-hover`: Primary button background on hover. - `--color-bg-brand-pressed`: Primary button background while pressed. - `--color-text-onbrand`: Button label and icon color. ## Styles Use `secondary` for supporting actions. Use `destructive` when an action is destructive or difficult to reverse. The `dashed`, `ghost`, and `pill` variants provide lower-emphasis treatments for controls that need them. **Implementation** ```tsx ``` **Styling** Each Button variant has its own class, so supporting and destructive actions can be customized independently. ```css @layer payload { .btn--style-secondary { --color-bg: #f7f5ff; --color-text: #4938cc; --special-border-translucent: #8b7cf6; } .btn--style-destructive { --color-bg-danger: #c7362f; --color-bg-danger-hover: #a92c27; --color-bg-danger-pressed: #8f2420; --color-text-ondanger: #ffffff; } } ``` - `--color-bg`: Secondary button background. - `--color-text`: Secondary button label and icon color. - `--special-border-translucent`: Secondary button border color. - `--color-bg-danger`: Destructive button background. - `--color-bg-danger-hover`: Destructive button background on hover. - `--color-bg-danger-pressed`: Destructive button background while pressed. - `--color-text-ondanger`: Destructive button label and icon color. Avoid placing multiple primary buttons next to one another. When actions have equal emphasis, use secondary buttons instead. ## Disabled state Use `disabled` when an action is temporarily unavailable. When possible, explain what the user must do before the action becomes available. **Implementation** ```tsx ``` **Styling** Target the disabled state together with a style variant to customize unavailable Buttons without changing enabled actions. ```css @layer payload { .btn--style-primary.btn--disabled { --color-bg-disabled: #e3e1f5; --color-text-ondisabled: #625d82; } } ``` - `--color-bg-disabled`: Disabled button background. - `--color-text-ondisabled`: Label and icon color on a disabled filled button. ## Sizes Buttons support `medium` and `large` sizes. Use `medium` unless the surrounding interface establishes the larger size. **Implementation** ```tsx ``` **Styling** Override the spacing tokens within a size class when a project needs different Button dimensions. ```css @layer payload { .btn--size-medium { --button-height: 1.75rem; --spacer-2: 0.625rem; } .btn--size-large { --spacer-5: 2.25rem; --spacer-3: 0.875rem; } } ``` - `--button-height`: Medium button height. - `--spacer-2`: Medium button inline padding. - `--spacer-5`: Large button height. - `--spacer-3`: Large button inline padding. ## Accessibility - Use a short, action-oriented label that describes what happens next. - Provide `aria-label` when an icon-only button has no visible label. - Do not rely on color alone to explain a destructive or disabled action. - Use `type="submit"` only when the button submits its containing form. - Use a link-style button for navigation rather than handling navigation in `onClick`. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ------------- | ---------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------- | | `buttonStyle` | `'primary' \| 'secondary' \| 'destructive' \| 'dashed' \| 'ghost' \| 'pill'` | `'primary'` | Sets the visual emphasis. | | `children` | `ReactNode` | — | Visible button content. | | `disabled` | `boolean` | `false` | Prevents interaction. | | `loading` | `boolean` | `false` | Shows a spinner, hides the content, and prevents interaction. | | `margin` | `boolean` | `true` | Applies the default outer margin. | | `round` | `boolean` | `false` | Gives an icon button equal width and height. | | `selected` | `boolean` | `false` | Applies the active treatment to supported variants. | | `size` | `'medium' \| 'large'` | `'medium'` | Sets the button height and spacing. | | `type` | `'button' \| 'submit'` | `'button'` | Sets the native button type. | # Card Source: https://payloadcms.com/docs/beta/ui-components/card The `Card` component groups a title with optional actions. It can also make the entire surface interactive. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { Card } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { Card } from '@payloadcms/ui/elements/Card' ``` ## Basic usage **Implementation** ```tsx ``` **Styling** Override Card tokens within the component selector to customize its surface, interactive state, border, and corner radius. ```css @layer payload { .card { --color-bg-secondary: #f4f1ff; --color-bg-secondary-hover: #e9e3ff; --color-border: #c8bdf5; --radius-medium: 0.75rem; } } ``` - `--color-bg-secondary`: Default card background. - `--color-bg-secondary-hover`: Interactive card background on hover. - `--color-border`: Card border color. - `--radius-medium`: Card corner radius. ## Actions Use the `actions` prop for controls that act on the card without activating the full card surface. **Implementation** ```tsx } margin={false} onClick={() => createPost()} round /> } title="Posts" /> ``` **Styling** Override Card tokens within the component selector to customize its surface, interactive state, border, and corner radius. ```css @layer payload { .card { --color-bg-secondary: #f4f1ff; --color-bg-secondary-hover: #e9e3ff; --color-border: #c8bdf5; --radius-medium: 0.75rem; } } ``` - `--color-bg-secondary`: Default card background. - `--color-bg-secondary-hover`: Interactive card background on hover. - `--color-border`: Card border color. - `--radius-medium`: Card corner radius. When using `href` or `onClick`, provide `buttonAriaLabel` so the full-card control has an accessible name. Give controls passed to `actions` their own accessible names. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ----------------- | ------------- | ------- | ----------------------------------------------- | | `title` \* | `string` | — | Visible card title. | | `titleAs` | `ElementType` | `'div'` | Sets the element used for the title. | | `actions` | `ReactNode` | — | Displays controls on the trailing edge. | | `href` | `string` | — | Makes the full card navigate to the given path. | | `onClick` | `() => void` | — | Makes the full card perform an action. | | `buttonAriaLabel` | `string` | — | Labels the full-card interactive control. | _\* An asterisk denotes that a prop is required._ # Checkbox and Select Inputs Source: https://payloadcms.com/docs/beta/ui-components/checkbox-and-select-inputs `CheckboxInput` and `SelectInput` provide the input-level controls used by Payload's Checkbox and Select fields. They are useful when a Custom Field needs standard Admin styling while controlling its own state. ## Import ```tsx import { CheckboxInput, SelectInput } from '@payloadcms/ui' ``` ## CheckboxInput **Implementation** ```tsx const [checked, setChecked] = useState(true) setChecked(event.target.checked)} /> ``` **Styling** Customize the v4 checkbox visuals through their component variables while leaving the native input and label connection intact. ```css .checkbox-input__visuals { --checkbox-border-color: var(--color-border); border-radius: var(--radius-small); } .checkbox-input__control:checked + .checkbox-input__visuals { --checkbox-bg: var(--color-bg-brand); --checkbox-mark-color: var(--color-icon-onbrand); } ``` - `--checkbox-bg`: Checked control surface. - `--checkbox-border-color`: Checkbox outline. - `--checkbox-mark-color`: Check icon color. - `border-radius`: Checkbox corner radius. Use `checked` and `onToggle` to control the value. Set `partialChecked` to display the indeterminate treatment used by list-selection interfaces. ## SelectInput **Implementation** ```tsx const options = [ { label: 'Planned', value: 'planned' }, { label: 'In progress', value: 'in-progress' }, { label: 'Complete', value: 'complete' }, ] const [value, setValue] = useState('in-progress') setValue(option?.value || '')} options={options} path="status" value={value} /> ``` **Styling** Customize the ReactSelect control within SelectInput while retaining Payload’s field label, description, and error structure. ```css .field-type.select .rs__control { background: var(--color-bg); border-color: var(--color-border); border-radius: 6px; } .field-type.select .rs__control--is-focused { border-color: var(--color-border-brand-strong); } ``` - `--color-bg`: Select control surface. - `border-color`: Resting and focused outline color. - `border-radius`: Select control corner radius. `SelectInput` wraps [`ReactSelect`](/docs/v4/ui-components/react-select.md) with Payload's field label, description, and validation structure. Its controlled `value` contains the stored option value rather than the complete option object. ## CheckboxInput props | Prop | Type | Default | Description | | ---------------- | -------------------------------------- | ------- | ------------------------------------------ | | `checked` | `boolean` | `false` | Controls the checked state. | | `onToggle` \* | `ChangeEventHandler` | — | Receives changes from the native checkbox. | | `label` | `StaticLabel` | — | Displays the checkbox label. | | `partialChecked` | `boolean` | `false` | Displays the indeterminate treatment. | | `readOnly` | `boolean` | `false` | Disables interaction. | | `required` | `boolean` | `false` | Marks the native input as required. | ## SelectInput props | Prop | Type | Default | Description | | ------------- | ---------------------- | ------- | --------------------------------------------- | | `name` \* | `string` | — | Names the select input. | | `path` \* | `string` | — | Identifies the field and connects its UI. | | `options` \* | `OptionObject[]` | — | Supplies the available translated options. | | `value` | `string \| string[]` | — | Supplies the selected stored value or values. | | `onChange` | `ReactSelect.onChange` | — | Receives the selected option object. | | `hasMany` | `boolean` | `false` | Enables multiple values. | | `isSortable` | `boolean` | `true` | Enables reordering in multi-value mode. | | `isClearable` | `boolean` | `true` | Allows the selected value to be cleared. | | `readOnly` | `boolean` | `false` | Disables interaction. | | `showError` | `boolean` | Form | Displays the current validation error. | _\* An asterisk denotes that a prop is required._ ## Provider requirements Both inputs use Payload's translation context. `SelectInput` also composes the standard field label, description, and error components, which can read the surrounding form context in a Custom Field. # Collapsible Source: https://payloadcms.com/docs/beta/ui-components/collapsible The `Collapsible` component groups content beneath a header that can be expanded or collapsed. It manages its own state by default and can also be controlled by a parent component. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { Collapsible } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { Collapsible } from '@payloadcms/ui/elements/Collapsible' ``` ## Basic usage **Implementation** ```tsx Collapsible content ``` **Styling** Scope surface and border tokens to Collapsible to customize both its header and expanded content. ```css @layer payload { .collapsible { --color-bg: #ffffff; --color-bg-secondary: #f4f1ff; --color-bg-secondary-hover: #e9e3ff; --color-border: #c8bdf5; --button-radius: 0.5rem; } } ``` - `--color-bg`: Expanded content background. - `--color-bg-secondary`: Header background. - `--color-bg-secondary-hover`: Header background on hover. - `--color-border`: Header and content border color. - `--button-radius`: Outer corner radius. Use a concise header that describes the hidden content. The entire header is used as the toggle target unless `disableHeaderToggle` is enabled. ## Error state **Implementation** ```tsx Correct the invalid fields in this section. ``` **Styling** Target the error state to customize invalid sections without changing the danger palette across the entire Admin Panel. ```css @layer payload { .collapsible--style-error { --color-bg-danger-tertiary: #fde5e3; --color-bg-danger-tertiary-hover: #f8cfcc; --color-border-danger-strong: #b8322c; } } ``` - `--color-bg-danger-tertiary`: Error header background. - `--color-bg-danger-tertiary-hover`: Error header background on hover. - `--color-border-danger-strong`: Error header and content border. Use the `error` style when the section contains invalid fields that require attention. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ------------------------ | ----------------------------------------------- | ----------- | ------------------------------------------------------ | | `children` \* | `ReactNode` | — | Content displayed inside the collapsible section. | | `header` | `ReactNode` | — | Content displayed in the section header. | | `actions` | `ReactNode` | — | Displays controls on the trailing edge of the header. | | `collapsibleStyle` | `'default' \| 'error'` | `'default'` | Sets the visual state. | | `initCollapsed` | `boolean` | `false` | Sets the initial uncontrolled state. | | `isCollapsed` | `boolean` | — | Controls the collapsed state from a parent component. | | `onToggle` | `(collapsed: boolean) => Promise \| void` | — | Runs when the section is toggled. | | `disableHeaderToggle` | `boolean` | `false` | Prevents the header from toggling the section. | | `disableToggleIndicator` | `boolean` | `false` | Hides the trailing chevron. | | `AfterCollapsible` | `ReactNode` | — | Renders content after the collapsible content wrapper. | _\* An asterisk denotes that a prop is required._ # CopyToClipboard Source: https://payloadcms.com/docs/beta/ui-components/copy-to-clipboard The `CopyToClipboard` component renders a compact copy control with tooltip feedback. Use it beside values such as IDs, URLs, and API keys. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { CopyToClipboard } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { CopyToClipboard } from '@payloadcms/ui/elements/CopyToClipboard' ``` ## Basic usage **Implementation** ```tsx post_123456789 ``` **Styling** Override CopyToClipboard tokens on the control to customize its icon, interaction states, focus color, and radius. ```css @layer payload { .copy-to-clipboard { --color-icon: #4938cc; --color-bg-secondary: #f4f1ff; --color-bg-secondary-pressed: #d9d0ff; --color-border-selected: #6d5dfc; --button-radius: 0.375rem; } } ``` - `--color-icon`: Copy icon color. - `--color-bg-secondary`: Control background on hover. - `--color-bg-secondary-pressed`: Control background while pressed. - `--color-border-selected`: Keyboard focus outline color. - `--button-radius`: Control corner radius. The control is not rendered when `value` is empty. Provide surrounding text so users understand what will be copied. ## Accessibility - Place the control beside a visible representation of the value being copied. - Use concise tooltip messages that identify the action and confirm success. - Do not place sensitive values in the clipboard without an explicit user action. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ---------------- | -------- | ---------------------- | ----------------------------------------- | | `value` | `string` | — | Value written to the clipboard. | | `defaultMessage` | `string` | Localized copy label | Tooltip shown before the value is copied. | | `successMessage` | `string` | Localized copied label | Tooltip shown after a successful copy. | # DatePicker Source: https://payloadcms.com/docs/beta/ui-components/date-picker The `DatePicker` component wraps `react-datepicker` with Payload's styles, localization, and date and time display options. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { DatePicker } from '@payloadcms/ui' ``` ## Basic usage **Implementation** ```tsx const [value, setValue] = useState() ``` **Styling** Override field tokens on DatePicker for the input, then scope calendar tokens to react-datepicker for the open calendar surface and selected date. ```css @layer payload { .date-time-picker { --field-color-bg: #f8f7fc; --field-color-border: #8f87bd; --field-color-placeholder: #625d82; --field-border-radius: 0.5rem; } .react-datepicker { --color-bg: #ffffff; --color-bg-selected-strong: #6d5dfc; --color-text-onselected-strong: #ffffff; } } ``` - `--field-color-bg`: Input background. - `--field-color-border`: Input border. - `--field-color-placeholder`: Placeholder text color. - `--field-border-radius`: Input corner radius. - `--color-bg-selected-strong`: Selected date background. Pair the picker with a visible label. `id` is applied to the picker's wrapper element, not to the underlying input, so pass the input ID through `overrides` and point the label's `htmlFor` at that value: ```tsx ``` The Admin Panel supplies the translation context that DatePicker uses for its calendar labels. ## Picker appearances Use `pickerAppearance` to choose between a date, date and time, day, month, or time input. Payload selects a matching display format when `displayFormat` is not provided. | Appearance | Input | | ------------ | ---------------------- | | `default` | Calendar date | | `dayAndTime` | Calendar date and time | | `dayOnly` | Day and month | | `monthOnly` | Month | | `timeOnly` | Time | ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ------------------ | --------------------------------------------------------------------- | ----------- | ------------------------------------------------ | | `id` | `string` | — | Sets the ID of the picker's wrapper element. | | `value` | `Date \| string` | — | Selected date or date string. | | `onChange` | `(value: Date) => void` | — | Runs when the selected value changes. | | `placeholder` | `string` | — | Displays text when no value is selected. | | `pickerAppearance` | `'default' \| 'dayAndTime' \| 'dayOnly' \| 'monthOnly' \| 'timeOnly'` | `'default'` | Selects the date and time controls shown. | | `displayFormat` | `string` | Automatic | Overrides the date-fns display format. | | `minDate` | `Date` | — | Sets the earliest selectable date. | | `maxDate` | `Date` | — | Sets the latest selectable date. | | `monthsToShow` | `1 \| 2` | `1` | Sets the number of visible calendar months. | | `timeIntervals` | `number` | `30` | Sets the number of minutes between time options. | | `readOnly` | `boolean` | `false` | Prevents the value from being changed. | | `overrides` | `ReactDatePickerProps` | — | Passes supported options to `react-datepicker`. | # Document Actions Source: https://payloadcms.com/docs/beta/ui-components/document-actions Payload's document action components connect standard Admin controls to the current form, document, permissions, and selection state. Use them inside the corresponding Edit or List View rather than as general-purpose buttons. ## Import ```tsx import { DeleteMany, EditMany, PublishButton, PublishMany, PreviewButton, SaveButton, SaveDraftButton, UnpublishButton, UnpublishMany, } from '@payloadcms/ui' ``` ## Save When drafts are disabled, `SaveButton` submits the current document form. It is disabled while an upload is in progress and, when editing an existing document, until the form has been modified. **Implementation** ```tsx ``` **Styling** Use the Save action ID together with its Button style class when the document Save control needs treatment distinct from other primary Admin buttons. ```css #action-save.btn--style-primary { color: var(--color-text-onbrand); background-color: var(--color-bg-brand); } #action-save.btn--style-primary:hover:not(.btn--disabled) { background-color: var(--color-bg-brand-hover); } ``` - `background-color`: Save action background. - `color`: Save action label color. Pass `label` to `SaveButton` when a replacement component needs a more specific action label. ## Draft and publish actions When drafts are enabled, Payload uses `PublishButton` instead of `SaveButton`. `SaveDraftButton` is shown by default when autosave is disabled. When autosave is enabled, set `versions.drafts.autosave.showSaveDraftButton` to `true` to keep the manual draft action visible. Payload also shows it for a new Collection document when draft validation is enabled. Saving a draft skips validation; publishing submits the document with a published status. **Implementation** ```tsx ``` **Styling** Use the stable action IDs together with Button style classes when Save and Publish controls need treatment distinct from other Admin buttons. ```css #action-save-draft.btn--style-secondary { color: var(--color-text-brand); border-color: var(--color-border-brand-strong); } #action-save-draft.btn--style-secondary:hover:not(.btn--disabled) { background-color: var(--color-bg-brand-tertiary); } #action-save.btn--style-primary { color: var(--color-text-onbrand); background-color: var(--color-bg-brand); } #action-save.btn--style-primary:hover:not(.btn--disabled) { background-color: var(--color-bg-brand-hover); } ``` - `background-color`: Action background. - `border-color`: Secondary action border. - `color`: Action label color. `UnpublishButton` appears in the document action menu when the current user can publish, the document is published, and drafts are enabled. It opens a confirmation modal before changing the document back to draft status. `PreviewButton` opens the current document's configured preview URL in a new tab. It only renders when the Edit View's live preview context contains a URL. Use these components when replacing the equivalent Edit View control. The surrounding Edit View supplies their form, document information, operation, locale, translation, and modal contexts. ```ts export const Posts = { slug: 'posts', admin: { components: { edit: { SaveDraftButton: '/components/CustomSaveDraftButton', PublishButton: '/components/CustomPublishButton', UnpublishButton: '/components/CustomUnpublishButton', }, }, }, } ``` See [Edit View Custom Components](/docs/v4/custom-components/edit-view.md) for all Collection and Global replacement slots and their server and client prop types. ## Bulk List View actions The bulk action components operate on the documents selected in the current List View. **Implementation** ```tsx ``` **Styling** Bulk actions share the ListSelection button treatment. Override that class to customize selection actions without changing standard Buttons. ```css .list-selection__button { color: #6d5dfc; font-weight: 600; } .list-selection__button:hover, .list-selection__button:focus-visible { color: #5947e5; text-decoration: underline; } ``` - `color`: Bulk action label color. - `font-weight`: Bulk action label emphasis. - `text-decoration`: Interaction feedback for bulk actions. Each component receives the current Collection's client configuration. Selection and authentication providers determine whether the action is visible and which documents it affects. `PublishMany` and `UnpublishMany` only render for Collections with drafts enabled. These controls are intended for List View composition. They do not create selection state when rendered elsewhere. ## Behavior and visibility | Component | Context | When it is used | | ----------------- | --------- | -------------------------------------------------------------- | | `SaveButton` | Edit View | Saves a document when drafts are disabled. | | `SaveDraftButton` | Edit View | Saves a draft without requiring the form to pass validation. | | `PublishButton` | Edit View | Publishes the current document when drafts are enabled. | | `UnpublishButton` | Edit View | Returns a published document to draft status after confirming. | | `PreviewButton` | Edit View | Opens the configured document preview in a new tab. | | `EditMany` | List View | Opens the bulk-edit workflow for selected documents. | | `PublishMany` | List View | Publishes selected documents. | | `UnpublishMany` | List View | Unpublishes selected documents. | | `DeleteMany` | List View | Trashes or permanently deletes selected documents. | ## Accessibility - Keep replacement labels action-oriented and specific to the operation. - Preserve the built-in confirmation step for destructive or difficult-to-reverse actions. - Do not force an action to remain visible when Payload hides it because of permissions or document state. - Keep custom action order consistent with the surrounding Edit or List View. ## Provider requirements These components depend on providers supplied by the Payload Admin Panel. Edit actions require the current form and document contexts. Bulk actions require the current Collection, selection, authentication, and List View contexts. Use [`Button`](/docs/v4/ui-components/button.md) for actions that do not need this behavior. # Document State Source: https://payloadcms.com/docs/beta/ui-components/document-state Payload's document state components communicate editing conflicts and protect users from losing work. The Admin Panel renders these states automatically during its standard document workflow; use the exports when composing or replacing an Edit View experience. ## Import ```tsx import { DocumentLocked, DocumentTakeOver, LeaveWithoutSaving, Locked, } from '@payloadcms/ui' ``` ## Lock indicator `Locked` displays the compact lock icon used when another user is editing a document. Its tooltip identifies that user when their email or ID is available. **Implementation** ```tsx ``` **Styling** Style the compact Locked indicator directly while preserving its built-in editor tooltip. ```css .locked { color: #6d5dfc; background: #f3f1ff; border: 1px solid #c8c0ff; border-radius: 999px; padding: 0.35rem; } ``` - `color`: Lock icon color. - `background`: Indicator surface. - `border`: Indicator outline. - `padding`: Clickable area around the lock icon. ## Locked document `DocumentLocked` interrupts editing and gives the user three appropriate paths: return to the previous view, inspect the document without editing, or take over editing. **Implementation** ```tsx router.back()} isActive={isLocked} onReadOnly={() => setReadOnly(true)} onTakeOver={takeOverDocument} updatedAt={lockedAt} user={lockingUser} /> ``` **Styling** Target the document lock dialog by its stable modal slug to customize the interruption without changing its permission or navigation behavior. ```css dialog[data-payload-modal-slug='document-locked'] { background: rgb(255 255 255 / 88%); backdrop-filter: blur(8px); } dialog[data-payload-modal-slug='document-locked'] > .dialog { border: 1px solid #c8c0ff; border-radius: 8px; } ``` - `background`: Surface displayed over the interrupted Edit View. - `backdrop-filter`: Visual separation from the underlying document. - `border`: Optional outline around the state message. - `border-radius`: State message corner radius. The component opens while `isActive` is true. Supply callbacks for each navigation or takeover outcome; the component coordinates its own modal visibility and clears Payload's route cache when required. ## Editing taken over `DocumentTakeOver` is shown to the previous editor after another user takes control of the document. It lets them return to the dashboard or continue in read-only mode. **Implementation** ```tsx router.push('/admin')} isActive={hasLostEditAccess} onReadOnly={() => setReadOnly(true)} /> ``` **Styling** Target the document takeover dialog by its stable modal slug to customize the interruption without changing its permission or navigation behavior. ```css dialog[data-payload-modal-slug='document-take-over'] { background: rgb(255 255 255 / 88%); backdrop-filter: blur(8px); } dialog[data-payload-modal-slug='document-take-over'] > .dialog { border: 1px solid #c8c0ff; border-radius: 8px; } ``` - `background`: Surface displayed over the interrupted Edit View. - `backdrop-filter`: Visual separation from the underlying document. - `border`: Optional outline around the state message. - `border-radius`: State message corner radius. ## Unsaved changes `LeaveWithoutSaving` watches the current document form and opens a confirmation modal when an authenticated user attempts to navigate away from modified or invalid content. ```tsx { analytics.track('Left document with unsaved changes') }} /> ``` Set `disablePreventLeave` when another part of a custom workflow temporarily owns navigation. Use `onPrevent` to observe the attempted destination and `onConfirm` for work that must happen after the user accepts leaving. ## Common props These are the props most commonly used with this component family. See the exported types in `@payloadcms/ui` for the complete list. ### Locked | Prop | Type | Default | Description | | ----------- | ------------ | ------- | ------------------------------------------- | | `user` \* | `ClientUser` | — | Identifies the user currently editing. | | `className` | `string` | — | Adds a class to the lock indicator wrapper. | ### DocumentLocked | Prop | Type | Default | Description | | ----------------- | -------------------------------- | ------- | ----------------------------------------------- | | `isActive` \* | `boolean` | — | Opens or closes the locked-document modal. | | `handleGoBack` \* | `() => void` | — | Returns the user to the previous safe location. | | `onReadOnly` \* | `() => void` | — | Switches the document into read-only mode. | | `onTakeOver` \* | `() => void` | — | Takes control of editing the document. | | `updatedAt` | `number \| null` | — | Displays the locking editor's last activity. | | `user` | `ClientUser \| string \| number` | — | Identifies the locking user. | ### DocumentTakeOver | Prop | Type | Default | Description | | -------------------------- | ------------ | ------- | ---------------------------------------------- | | `isActive` \* | `boolean` | — | Opens or closes the takeover modal. | | `handleBackToDashboard` \* | `() => void` | — | Returns the previous editor to the dashboard. | | `onReadOnly` \* | `() => void` | — | Continues with the document in read-only mode. | _\* An asterisk denotes that a prop is required._ ## Accessibility - Preserve the built-in modal focus management when composing conflict states. - Identify the other editor when that information can be shown safely. - Keep read-only and takeover actions explicit; they have meaning beyond ordinary navigation. - Do not rely on the lock icon alone when the editing conflict needs an explanation. ## Provider requirements These components depend on the document, form, authentication, translation, modal, route-cache, and route-transition contexts supplied by the Admin Panel. They are intended for Custom Components rendered within Payload's document workflow, not standalone application status messages. # Draggable Sortable Source: https://payloadcms.com/docs/beta/ui-components/draggable-sortable Use `DraggableSortable` with `DraggableSortableItem` to add pointer, touch, and keyboard reordering to custom Admin Panel interfaces. ## Import ```tsx import { DraggableSortable, DraggableSortableItem } from '@payloadcms/ui' ``` ## Basic usage **Implementation** ```tsx 'use client' import { Button, ChevronIcon, DraggableSortable, DraggableSortableItem, DragHandleIcon, } from '@payloadcms/ui' import { useState } from 'react' const initialItems = [ { id: 'overview', label: 'Overview' }, { id: 'details', label: 'Details' }, { id: 'settings', label: 'Settings' }, ] const moveItem = (items, moveFromIndex, moveToIndex) => { const nextItems = [...items] const [movedItem] = nextItems.splice(moveFromIndex, 1) nextItems.splice(moveToIndex, 0, movedItem) return nextItems } export function SortableList() { const [items, setItems] = useState(initialItems) return ( id)} onDragEnd={({ moveFromIndex, moveToIndex }) => { setItems((current) => moveItem(current, moveFromIndex, moveToIndex)) }} > {items.map(({ id, label }, index) => ( {({ attributes, isDragging, listeners, setNodeRef, transform, transition }) => (
{label}
)}
))}
) } ``` **Styling** Style the list and row wrappers while leaving the drag transform and transition inline on each item. Payload theme variables keep the surface consistent across light and dark modes. ```css .sortable-list { display: grid; gap: var(--spacer-2); } .sortable-item { align-items: center; background: var(--color-bg-secondary); border: 1px solid var(--color-border); display: flex; justify-content: space-between; padding: var(--spacer-2) var(--spacer-3); } ``` - `--color-bg-secondary`: Sortable item background. - `--color-border`: Sortable item border. - `--spacer-2`: Spacing within and between items. `DraggableSortable` coordinates the list and reports the original and destination indexes through `onDragEnd`. Your component remains responsible for updating local state and persisting the new order when necessary. Each `DraggableSortableItem` exposes the attributes, listeners, ref, transform, and transition needed to render an item. Apply the listeners to a visible drag handle so the rest of the row can retain its own interactions. ## DraggableSortable props | Prop | Type | Description | | ------------- | ------------------------------------------------- | ---------------------------------------------- | | `children*` | `ReactNode` | Renders the sortable items. | | `ids*` | `string[]` | Lists item IDs in their current display order. | | `onDragEnd*` | `({ moveFromIndex, moveToIndex, event }) => void` | Handles a completed reorder. | | `onDragStart` | `({ id, event }) => void` | Handles the beginning of a drag. | | `className` | `string` | Adds a class to the sortable list container. | ## DraggableSortableItem props | Prop | Type | Description | | ----------- | ------------------------------ | ----------------------------------------------------------- | | `id*` | `string` | Matches an entry in the parent `ids` array. | | `children*` | `(sortableState) => ReactNode` | Renders the item with its drag attributes and visual state. | | `disabled` | `boolean` | Prevents the item from being reordered. | _\* An asterisk denotes that a prop is required._ ## Accessibility - Put the supplied attributes and listeners on a keyboard-focusable drag handle. - Give the handle an accessible name that identifies the item it moves. - Provide move-up and move-down controls so pointer users can reorder without a dragging movement. - Persist the resulting order only after `onDragEnd` supplies a valid destination. These components configure their own drag-and-drop context and require no Payload providers. # Dropzone Source: https://payloadcms.com/docs/beta/ui-components/dropzone The `Dropzone` component adds drag, drop, and paste handling to a container. It passes the selected `FileList` to `onChange` and applies Payload's drop-target styles. ## Import ```tsx import { Dropzone } from '@payloadcms/ui' ``` ## Basic usage **Implementation** ```tsx const inputRef = useRef(null) const [fileNames, setFileNames] = useState([]) const selectFiles = (files: FileList) => { setFileNames(Array.from(files, (file) => file.name)) } event.target.files && selectFiles(event.target.files)} ref={inputRef} type="file" /> {fileNames.length ? fileNames.join(', ') : 'or drag and drop files here'} ``` **Styling** Override semantic surface, border, and radius tokens on Dropzone while preserving its drag-and-drop interaction states. ```css @layer payload { .dropzone { background: var(--color-bg-secondary); border-color: var(--color-border); border-radius: var(--radius-medium); } } ``` - `--color-bg-secondary`: Drop target surface. - `--color-border`: Drop target outline. - `--radius-medium`: Drop target corner radius. `Dropzone` does not render a file input or open the system file picker. Render those controls as children when users should be able to browse for files in addition to dropping them. It also has no awareness of Collections or existing documents, so controls such as **Create New** and **Choose from existing** must come from the surrounding field interface. When `multipleFiles` is disabled, dropping more than one file passes only the first file to `onChange`. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | --------------- | --------------------------- | ----------- | -------------------------------------------------- | | `children` | `ReactNode` | — | Content and file-selection controls in the target. | | `onChange` \* | `(files: FileList) => void` | — | Runs when files are dropped, pasted, or supplied. | | `multipleFiles` | `boolean` | `false` | Allows more than one file to be returned. | | `disabled` | `boolean` | `false` | Disables drag, drop, and paste listeners. | | `dropzoneStyle` | `'default' \| 'none'` | `'default'` | Enables or removes Payload's drop-target styles. | | `className` | `string` | — | Adds a class to the dropzone container. | _\* An asterisk denotes that a prop is required._ # ErrorPill Source: https://payloadcms.com/docs/beta/ui-components/error-pill The `ErrorPill` component displays the number of validation errors associated with a field or group of fields. It returns `null` when the count is zero. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { ErrorPill, useTranslation } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { ErrorPill } from '@payloadcms/ui/elements/ErrorPill' import { useTranslation } from '@payloadcms/ui/providers/Translation' ``` ## Counts **Implementation** ```tsx const { i18n } = useTranslation() ``` **Styling** Override semantic tokens on ErrorPill to customize its validation color and shape without changing danger colors elsewhere in the Admin Panel. ```css @layer payload { .error-pill { --color-bg-danger: #b8322c; --color-text-ondanger: #ffffff; --radius-medium: 999px; } } ``` - `--color-bg-danger`: ErrorPill background. - `--color-text-ondanger`: ErrorPill count and message color. - `--radius-medium`: ErrorPill corner radius. Pass the `i18n` object from `useTranslation`. Add `withMessage` when the count needs a localized “error” or “errors” label. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ------------- | ------------ | ------- | ------------------------------------------------ | | `count` \* | `number` | — | Number of errors. Renders nothing when set to 0. | | `i18n` \* | `I18nClient` | — | Supplies localized error labels. | | `withMessage` | `boolean` | `false` | Displays “error” or “errors” after the count. | | `className` | `string` | — | Adds a custom class to the element. | _\* An asterisk denotes that a prop is required._ # Field Labels and Messages Source: https://payloadcms.com/docs/beta/ui-components/field-labels-and-messages `FieldLabel`, `FieldDescription`, and `FieldError` provide the supporting text and validation treatment used around inputs in the Payload Admin Panel. Use them when a Custom Field needs to match Payload's field structure without rendering one of the complete field components. ## Import ```tsx import { FieldDescription, FieldError, FieldLabel } from '@payloadcms/ui' ``` ## Labels **Implementation** ```tsx
``` **Styling** Style the shared FieldLabel class and its required marker to keep custom inputs aligned with the rest of the Admin Panel. ```css .field-label { color: var(--color-text); font-weight: 600; } .field-label .required { color: var(--color-text-danger); } ``` - `color`: Label and required-marker color. - `font-weight`: Label emphasis. This example places `FieldLabel` above a native input to show how it fits into a complete Custom Field. Connect the label to its input with `htmlFor`. When `htmlFor` is omitted, `FieldLabel` can derive the matching field ID from `path` inside Payload's form context. ## Descriptions and errors **Implementation** ```tsx
``` **Styling** Customize helper and validation messages through their shared classes while preserving the visibility behavior controlled by the field state. ```css .field-description { color: var(--color-text-secondary); font-size: 0.875rem; } .field-error.tooltip { background: var(--color-bg-danger); color: var(--color-text-ondanger); } ``` - `background`: Validation tooltip surface. - `color`: Helper or validation text color. - `font-size`: Helper text size. `FieldDescription` renders helper text below the control, while `FieldError` positions validation feedback against the same field. `FieldError` can receive `message` and `showError` directly, or read the current message and submitted state from Payload's form context. ## Common props ### FieldLabel | Prop | Type | Default | Description | | ------------ | --------------------------- | --------- | ------------------------------------------------- | | `label` | `StaticLabel` | — | Supplies the translated visible label. | | `htmlFor` | `string` | Generated | Associates the label with an input. | | `path` | `string` | — | Generates the input ID when `htmlFor` is omitted. | | `required` | `boolean` | `false` | Displays Payload's required marker. | | `localized` | `boolean` | `false` | Displays the active locale beside the label. | | `hideLocale` | `boolean` | `false` | Hides the active locale label. | | `as` | `'h3' \| 'label' \| 'span'` | `'label'` | Changes the rendered element. | | `unstyled` | `boolean` | `false` | Removes Payload's label presentation. | ### FieldDescription | Prop | Type | Default | Description | | ----------------- | ------------------- | ------- | --------------------------------------------- | | `description` | `StaticDescription` | — | Supplies translated helper content. | | `path` \* | `string` | — | Adds the field-specific description class. | | `className` | `string` | — | Adds a custom class. | | `marginPlacement` | `'bottom' \| 'top'` | — | Applies Payload's standard description space. | ### FieldError | Prop | Type | Default | Description | | ------------ | ------------------------------- | --------- | ------------------------------------------ | | `path` | `string` | — | Reads validation state for this form path. | | `message` | `string` | Form | Overrides the message from form state. | | `showError` | `boolean` | Form | Forces the validation message to render. | | `alignCaret` | `'center' \| 'left' \| 'right'` | `'right'` | Positions the validation tooltip caret. | _\* An asterisk denotes that a prop is required._ ## Accessibility - Associate visible labels with their inputs. - Use descriptions for guidance, not essential validation errors. - Keep validation messages specific and explain how to correct the value. - Preserve `aria-describedby` and error associations when composing a complete custom input. ## Provider requirements These components use Payload's translation context. `FieldLabel` can also read form, locale, and edit-depth context, while `FieldError` reads form validation state when explicit values are not supplied. Those providers are already present inside Admin Panel Custom Components. # Gutter Source: https://payloadcms.com/docs/beta/ui-components/gutter The `Gutter` component applies the standard horizontal spacing used by Payload views. Use it to align custom view content with the surrounding Admin Panel. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { Gutter } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { Gutter } from '@payloadcms/ui/elements/Gutter' ``` ## Basic usage **Implementation** ```tsx
Content aligned to the Admin Panel gutter
``` **Styling** Override the horizontal gutter token on a specific Gutter when custom content needs different spacing from the surrounding Admin view. ```css @layer payload { .gutter { --gutter-h: 3rem; } } ``` - `--gutter-h`: Left and right gutter spacing. Left and right padding are enabled by default. Negative gutters let a child extend through spacing already applied by a parent layout. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | --------------- | ----------- | ------- | ------------------------------------- | | `children` \* | `ReactNode` | — | Content positioned within the gutter. | | `left` | `boolean` | `true` | Applies the standard left padding. | | `right` | `boolean` | `true` | Applies the standard right padding. | | `negativeLeft` | `boolean` | `false` | Applies a negative left margin. | | `negativeRight` | `boolean` | `false` | Applies a negative right margin. | | `className` | `string` | — | Adds a custom class to the wrapper. | _\* An asterisk denotes that a prop is required._ # Icons Source: https://payloadcms.com/docs/beta/ui-components/icons Payload exports the icons used throughout the Admin Panel so custom UI can use the same visual language. ## Import ```tsx import { CopyIcon, EditIcon, PlusIcon, SearchIcon } from '@payloadcms/ui' ``` ## Available icons **Implementation** ```tsx import { CopyIcon, EditIcon, PlusIcon, SearchIcon } from '@payloadcms/ui' export function Toolbar() { return (
) } ``` **Styling** Icons use currentColor, so set color on a wrapper to follow your theme. Target the nested SVG only when the surrounding layout requires a consistent size. ```css .toolbar-icon { color: var(--color-text); display: inline-flex; } .toolbar-icon svg { height: 1.25rem; width: 1.25rem; } .toolbar-icon:hover { color: var(--color-text-secondary); } ``` - `color`: Stroke or fill color inherited by the icon. - `height`: Rendered icon height. - `width`: Rendered icon width. Icons inherit their color from the surrounding text. Their default dimensions vary according to where each icon is used in the Admin Panel; apply a shared class to the surrounding element when your layout requires a consistent size. The client export includes: - `CalendarIcon`, `CheckIcon`, `ChevronIcon`, `CloseMenuIcon`, and `CodeBlockIcon` - `CopyIcon`, `DocumentIcon`, `DragHandleIcon`, `EditIcon`, and `NewTabIcon` - `FolderIcon`, `GearIcon`, `GridViewIcon`, `LineIcon`, and `LinkIcon` - `ListViewIcon`, `LogOutIcon`, `MinimizeMaximizeIcon`, `MoreIcon`, and `Dots` - `MoveFolderIcon`, `PlusIcon`, `SearchIcon`, `SwapIcon`, and `XIcon` - `ErrorIcon`, `InfoIcon`, `SuccessIcon`, `WarningIcon`, and `WarningTriangleIcon` for notification states - `ArrowIcon`, `CirclePlusIcon`, `CircledXIcon`, `ClipboardIcon`, `DuplicateIcon`, and `EyeIcon` - `FilterIcon`, `KeyIcon`, `LockIcon`, `LockOpenIcon`, `PeopleIcon`, `RefreshIcon`, and `ReplaceIcon` - `SortDownIcon`, `TagIcon`, `ThreeDotsIcon`, `TrashIcon`, `VariableColorIcon`, and `WriteIcon` ## Configurable icons Most icons render without props. These exports expose additional options: | Component | Props | Description | | ---------------------- | --------------------------------------------- | -------------------------------------------------- | | `ChevronIcon` | `direction`, `size`, `ariaLabel`, `className` | Sets its direction, preset size, label, or class. | | `GearIcon` | `ariaLabel`, `className`, `size` | Sets its label, class, or 16/24-pixel preset size. | | `MinimizeMaximizeIcon` | `isMinimized`, `className`, `size` | Switches graphics and sets a preset size. | | `DragHandleIcon` | `className`, `size` | Adds a custom class or 16/24-pixel preset size. | | `EditIcon` | `className` | Adds a custom class. | | `NewTabIcon` | `className`, `size` | Adds a custom class or 16/24-pixel preset size. | | `FolderIcon` | `className`, `size` | Adds a custom class or 16/24-pixel preset size. | | `MoreIcon` | `className`, `size` | Adds a custom class or 16/24-pixel preset size. | | `XIcon` | `className`, `size` | Adds a custom class or 16/24-pixel preset size. | ## Accessibility - Treat an icon as decorative when adjacent text already describes the action. - Give an icon-only button or link an accessible name on the interactive element. - Do not use color alone to communicate a state or outcome. The icons use SVG markup and require no Payload providers. # Link Source: https://payloadcms.com/docs/beta/ui-components/link The `Link` component uses Payload's router adapter and route-transition state. Use it for links between Admin Panel routes. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { Link } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { Link } from '@payloadcms/ui/elements/Link' ``` ## Basic usage **Implementation** ```tsx View posts ``` **Styling** Link supplies navigation behavior but no visual treatment. Pass a className and style that class to match the link’s purpose and surrounding Admin content. ```css @layer payload { .custom-admin-link { color: var(--color-text-brand); font-weight: var(--font-weight-strong); text-decoration: underline; text-underline-offset: 0.15em; } .custom-admin-link:hover { color: var(--color-text); } } ``` - `--color-text-brand`: Branded link text color. - `--color-text`: Default text color used here for hover. - `--font-weight-strong`: Emphasized link weight. `Link` supplies navigation behavior but does not add visual styling. Pass a `className` when the link needs its own visual treatment. The Admin Panel supplies the router and route-transition contexts used by the component. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ---------------- | ------------------- | ------- | ---------------------------------------------------------- | | `href` \* | `string` | — | Destination passed to Payload's router adapter. | | `replace` | `boolean` | `false` | Replaces the current history entry instead of adding one. | | `scroll` | `boolean` | `true` | Controls whether navigation scrolls to the new page. | | `prefetch` | `boolean` | — | Controls route prefetching when the adapter supports it. | | `preventDefault` | `boolean` | `true` | Lets Payload manage navigation and route-transition state. | | `forceReload` | `boolean` | `false` | Uses a hard browser navigation instead of client routing. | | `onClick` | `MouseEventHandler` | — | Runs before navigation begins. | _\* An asterisk denotes that a prop is required._ # List and Pagination Controls Source: https://payloadcms.com/docs/beta/ui-components/list-and-pagination-controls Payload's list controls support paginated data, page-size selection, sorting, and row selection. Use standalone controls for custom data views and provider-backed controls when extending an existing Admin list. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { Pagination, PerPage, SelectAll, SelectMany, SelectRow, SortColumn, SortHeader, SortRow, } from '@payloadcms/ui' ``` ## Pagination **Implementation** ```tsx const [page, setPage] = useState(4) const totalPages = 12 1} nextPage={page + 1} onChange={setPage} page={page} prevPage={page - 1} totalPages={totalPages} /> ``` **Styling** Override field and surface tokens on Pagination to customize its page input without changing other Admin controls. ```css @layer payload { .paginator { --field-color-border: #8f87bd; --color-border-selected: #6d5dfc; --color-bg-secondary: #f8f7fc; } } ``` - `--field-color-border`: Page input border. - `--color-border-selected`: Page input border while focused. - `--color-bg-secondary`: Page input background. `Pagination` is controlled. It renders previous and next controls alongside an editable page input. Pass the current page metadata and update your query or local state from `onChange`. The input accepts a page between `1` and `totalPages`. Press Enter or move focus away to apply the value, and press Escape to restore the current page. ## Page size `PerPage` displays a compact selector for the number of rows shown on each page. ```tsx const [limit, setLimit] = useState(10) ``` Reset the current page when changing the limit if the new page size would make that page invalid. ## Sorting - `SortColumn` renders ascending and descending controls for a named column. - `SortHeader` switches an orderable list to its configured order field. - `SortRow` renders the drag handle used by an actively orderable list. These controls read and update Payload's `ListQuery` context. Use them inside an Admin list or a custom view that provides the same list-query state. ## Selection - `SelectAll` toggles selection for the current list. - `SelectRow` toggles one row and respects document locking. - `SelectMany` renders an action pill for the current selection. Selection controls require Payload's selection context. `SelectRow` also reads the authenticated user to determine whether a locked row can be selected. ## Common props These are the props most commonly used. See the exported types in `@payloadcms/ui` for the complete list. ### Pagination props | Prop | Type | Default | Description | | ------------- | ------------------------ | ------- | ----------------------------------------------- | | `page` | `number` | `1` | Sets the current page. | | `totalPages` | `number` | `1` | Sets the total page count. | | `hasNextPage` | `boolean` | `false` | Enables the next-page control. | | `hasPrevPage` | `boolean` | `false` | Enables the previous-page control. | | `nextPage` | `number` | — | Sets the page selected by the next control. | | `prevPage` | `number` | — | Sets the page selected by the previous control. | | `onChange` | `(page: number) => void` | — | Runs when the user selects a different page. | ### PerPage props | Prop | Type | Default | Description | | -------------- | ------------------------- | ------- | ------------------------------------------------ | | `limit` \* | `number` | — | Sets the selected page size. | | `limits` \* | `number[]` | — | Sets the available page sizes. | | `defaultLimit` | `number` | `10` | Provides a fallback when `limit` is not numeric. | | `handleChange` | `(limit: number) => void` | — | Runs when the user chooses a new page size. | _\* An asterisk denotes that a prop is required._ ## Internal list wrappers `PageControls` and `PageControlsComponent` are exported for Payload's own list views but are marked internal. For Custom Components, compose `Pagination` and `PerPage` unless you are intentionally extending Payload's existing list-query implementation. # Motion and Loading Source: https://payloadcms.com/docs/beta/ui-components/motion-and-loading Payload's loading components communicate that work is in progress, while its motion helpers make layout changes easier to follow. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { AnimateHeight, FormLoadingOverlayToggle, LoadingOverlay, LoadingOverlayToggle, ProgressBar, ShimmerEffect, StaggeredShimmers, } from '@payloadcms/ui' ``` ## Loading overlay **Implementation** ```tsx const [show, setShow] = useState(false) ``` **Styling** Override LoadingOverlay tokens to customize its backdrop and spinner while preserving the built-in loading behavior. ```css @layer payload { .loading-overlay { --color-bg: #f8f7fc; --color-icon-secondary: #6d5dfc; } } ``` - `--color-bg`: Loading backdrop color. - `--color-icon-secondary`: Spinner color. `LoadingOverlay` renders a spinner and backdrop directly. Use `LoadingOverlayToggle` inside the Admin Panel to update Payload's shared loading overlay instead. `FormLoadingOverlayToggle` connects that shared overlay to a Payload form's loading and processing states. It must be rendered inside the Admin Panel's form and loading providers. ## Staggered shimmers **Implementation** ```tsx ``` **Styling** Override ShimmerEffect tokens on the component to match a custom surface without changing global background colors. ```css @layer payload { .shimmer-effect { --shine-bg: #ebe9f5; --shine-fg: #f8f7fc; --radius-medium: 0.5rem; } } ``` - `--shine-bg`: Base placeholder color. - `--shine-fg`: Animated highlight color. - `--radius-medium`: Default placeholder corner radius. Use `ShimmerEffect` for one placeholder or `StaggeredShimmers` for a repeated set. Match their dimensions to the content they temporarily replace to reduce layout movement. ## Route progress **Implementation** ```tsx View posts ``` **Styling** Target the progress element to replace its default text-colored fill with a semantic brand color. ```css @layer payload { .progress-bar__progress { background-color: var(--color-bg-brand); } } ``` - `--color-bg-brand`: Route progress fill color in this override. `ProgressBar` displays progress for transitions started within `RouteTransitionProvider`. Payload's `Link` component starts this transition automatically. Use `startRouteTransition` from `useRouteTransition` when navigation begins from another control. The preview simulates a brief route transition so the delayed progress indicator remains visible long enough to inspect. ## Other helpers - [`AnimateHeight`](/docs/v4/ui-components/animate-height.md) animates a container when its content changes height. - [`ShimmerEffect`](/docs/v4/ui-components/shimmer-effect.md) documents the single-placeholder API in more detail. ## Common props These are the props most commonly used. See the exported types in `@payloadcms/ui` for the complete list. ### LoadingOverlay | Prop | Type | Default | Description | | ------------------- | --------- | --------- | ------------------------------------------------ | | `show` | `boolean` | `true` | Selects the entering or exiting animation state. | | `animationDuration` | `string` | `'500ms'` | Sets the CSS animation duration. | | `overlayType` | `string` | — | Adds a type modifier to the overlay. | ### LoadingOverlayToggle | Prop | Type | Default | Description | | --------- | ------------------------------ | -------------- | --------------------------------------------- | | `name` \* | `string` | — | Identifies this source in the shared overlay. | | `show` \* | `boolean` | — | Adds or removes this source's loading state. | | `type` | `'fullscreen' \| 'withoutNav'` | `'fullscreen'` | Selects the shared overlay layout. | ### StaggeredShimmers | Prop | Type | Default | Description | | -------------- | ------------------ | ------- | ------------------------------------------------ | | `count` \* | `number` | — | Sets the number of shimmer placeholders. | | `height` | `number \| string` | — | Sets each placeholder's height. | | `width` | `number \| string` | — | Sets each placeholder's width. | | `renderDelay` | `number` | `500` | Delays rendering to avoid flashing on fast work. | | `shimmerDelay` | `number \| string` | `25` | Staggers the animation between placeholders. | _\* An asterisk denotes that a prop is required._ ## Accessibility Loading visuals should accompany, not replace, meaningful status text. Prevent interaction when an action cannot safely continue, and keep the loading state visible only while work is actually pending. # Pill Source: https://payloadcms.com/docs/beta/ui-components/pill The `Pill` component displays compact metadata, a category, or a status. Keep its label short so it remains easy to scan. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { Pill } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { Pill } from '@payloadcms/ui/elements/Pill' ``` ## Styles **Implementation** ```tsx Default Dark Success Warning Error ``` **Styling** Target a semantic Pill style to customize its background and text without changing the corresponding palette throughout the Admin Panel. ```css @layer payload { .pill--style-success { --color-bg-selected: #dff7e8; --color-text-brand: #176b3a; } .pill--style-warning { --color-bg-warning-tertiary: #fff0cc; --color-text-warning: #714b00; } .pill--style-error { --color-bg-danger-tertiary: #fde5e3; --color-text-danger: #9b2924; } } ``` - `--color-bg-selected`: Success Pill background. - `--color-text-brand`: Success Pill text. - `--color-bg-warning-tertiary`: Warning Pill background. - `--color-text-warning`: Warning Pill text. - `--color-bg-danger-tertiary`: Error Pill background. - `--color-text-danger`: Error Pill text. Use semantic styles only when the label represents that state. For example, use `error` for a failed state rather than decoration. ## Shape **Implementation** ```tsx Default Rounded ``` **Styling** Override the default and rounded radius tokens within Pill selectors to change their shapes without altering other controls. ```css @layer payload { .pill { --button-radius: 0.25rem; } .pill--rounded { --radius-large: 999px; } } ``` - `--button-radius`: Default Pill corner radius. - `--radius-large`: Corner radius used when rounded is enabled. Pills use Payload's standard small corner radius by default. Add `rounded` when the label should use the larger rounded corner treatment. ## Sizes **Implementation** ```tsx Small Medium ``` **Styling** Adjust spacing tokens within each size class to customize Pill height and inline padding independently. ```css @layer payload { .pill--size-small { --spacer-1: 0.375rem; } .pill--size-medium { --spacer-1: 0.375rem; --spacer-2-5: 0.875rem; } } ``` - `--spacer-1`: Small spacing used for vertical and compact inline padding. - `--spacer-2-5`: Medium Pill inline padding. ## Accessibility - Keep labels concise and ensure their meaning does not depend on color alone. - Treat a Pill as presentational metadata. Use a Button or Link when the user needs to perform an action. - Provide an accessible label when an icon communicates information that is not present in the visible text. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ----------- | ----------------------------------------------------------------------------------------------------- | ---------- | ------------------------------------ | | `children` | `ReactNode` | — | Visible pill content. | | `pillStyle` | `'light' \| 'light-gray' \| 'dark' \| 'white' \| 'always-white' \| 'success' \| 'warning' \| 'error'` | `'light'` | Sets the visual style. | | `size` | `'small' \| 'medium'` | `'medium'` | Sets the pill height and spacing. | | `rounded` | `boolean` | `false` | Uses a larger rounded corner radius. | | `icon` | `ReactNode` | — | Displays an icon beside the label. | # PillSelector Source: https://payloadcms.com/docs/beta/ui-components/pill-selector The `PillSelector` component displays a wrapping list of options that can be selected, deselected, and optionally reordered. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { PillSelector } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { PillSelector } from '@payloadcms/ui/elements/PillSelector' ``` ## Interactive example **Implementation** ```tsx const [pills, setPills] = useState([ { name: 'Posts', selected: true }, { name: 'Media', selected: false }, { name: 'Users', selected: true }, ]) { setPills((current) => current.map((item) => item.name === pill.name ? { ...item, selected: !item.selected } : item, ), ) }} pills={pills} /> ``` **Styling** Override tokens on the selector surface and its selected Chips to customize this group without changing the same colors throughout the Admin Panel. ```css @layer payload { .pill-selector { --color-bg-secondary: #f7f5ff; --spacer-4: 1.25rem; } .pill-selector .chip--selected { --color-bg-brand-tertiary: #e8e4ff; --color-border-brand: #6d5dfc; } } ``` - `--color-bg-secondary`: PillSelector surface color. - `--color-bg-brand-tertiary`: Selected Chip background. - `--color-border-brand`: Selected Chip border. - `--spacer-4`: PillSelector padding. The component does not manage selection state. Update the `pills` array in `onClick` and pass the new array back to the component. ## Pill shape Each item in `pills` accepts: | Property | Type | Description | | ------------- | ----------- | -------------------------------------------- | | `name` \* | `string` | Stable value and default visible label. | | `selected` \* | `boolean` | Whether the option is currently selected. | | `Label` | `ReactNode` | Replaces the visible label. | | `key` | `string` | Overrides the React key used for the option. | _\* An asterisk denotes that a property is required._ ## Accessibility - Give each option a short, unique `name`; Payload uses it as the Chip's accessible label. - Do not communicate selection by color alone. Selected items replace the plus icon with a remove icon. - When enabling reordering, provide another way to understand or change the order when drag-and-drop is unavailable. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | ----------- | --------------------------------------------------------- | ------- | ------------------------------------------ | | `pills` \* | `SelectablePill[]` | — | Options displayed by the selector. | | `onClick` | `({ pill }) => Promise \| void` | — | Called when an option is activated. | | `draggable` | `{ onDragEnd: ({ moveFromIndex, moveToIndex }) => void }` | — | Enables reordering and handles the result. | _\* An asterisk denotes that a prop is required._ # ReactSelect Source: https://payloadcms.com/docs/beta/ui-components/react-select The `ReactSelect` component adapts `react-select` to Payload's styles and interaction patterns. It is also exported as `Select`. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { ReactSelect } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { ReactSelect } from '@payloadcms/ui/elements/ReactSelect' ``` ## Basic usage **Implementation** ```tsx import type { ReactSelectOption } from '@payloadcms/ui' const options: ReactSelectOption[] = [ { label: 'Draft', value: 'draft' }, { label: 'Published', value: 'published' }, { label: 'Archived', value: 'archived' }, ] const [value, setValue] = useState(options[0] ?? null) const handleChange = ( nextValue: ReactSelectOption | ReactSelectOption[] | null, ) => { setValue(Array.isArray(nextValue) ? null : nextValue) } ``` **Styling** Override field tokens within ReactSelect, then target its portalled menu to customize the control, focus state, and options without changing other Admin fields. ```css @layer payload { .react-select { --field-color-bg: #ffffff; --field-color-border: #b8b2d8; --field-color-border-focus: #6d5dfc; --field-border-radius: 0.5rem; } .rs__floating-menu-portal .rs__option--is-focused { --color-bg-secondary: #f2efff; } .rs__floating-menu-portal .rs__option--is-selected { --color-bg-selected: #e4dfff; } } ``` - `--field-color-bg`: Select control background. - `--field-color-border`: Select control border. - `--field-color-border-focus`: Select control border while focused. - `--field-border-radius`: Select control corner radius. - `--color-bg-secondary`: Focused option background. - `--color-bg-selected`: Selected option background. Store the selected option object rather than only its value. Use `isMulti` when users can select more than one option. By default, the options menu is rendered in a portal so it can escape drawers and other containers without being clipped. ## Multiple values **Implementation** ```tsx import type { ReactSelectOption } from '@payloadcms/ui' const options: ReactSelectOption[] = [ { label: 'Posts', value: 'posts' }, { label: 'Media', value: 'media' }, { label: 'Pages', value: 'pages' }, { label: 'Users', value: 'users' }, ] const [value, setValue] = useState(options.slice(0, 2)) const handleChange = ( nextValue: ReactSelectOption | ReactSelectOption[] | null, ) => { setValue(Array.isArray(nextValue) ? nextValue : nextValue ? [nextValue] : []) } ``` **Styling** Override field tokens within ReactSelect, then target its portalled menu to customize the control, focus state, and options without changing other Admin fields. ```css @layer payload { .react-select { --field-color-bg: #ffffff; --field-color-border: #b8b2d8; --field-color-border-focus: #6d5dfc; --field-border-radius: 0.5rem; } .rs__floating-menu-portal .rs__option--is-focused { --color-bg-secondary: #f2efff; } .rs__floating-menu-portal .rs__option--is-selected { --color-bg-selected: #e4dfff; } } ``` - `--field-color-bg`: Select control background. - `--field-color-border`: Select control border. - `--field-color-border-focus`: Select control border while focused. - `--field-border-radius`: Select control corner radius. - `--color-bg-secondary`: Focused option background. - `--color-bg-selected`: Selected option background. Enable `isMulti` to store an array of selected options. Add `isSortable` when users should also be able to reorder those values by dragging them. ## Accessibility - Add `aria-label` or associate the select with a visible field label when its purpose is not clear from nearby text. - Keep option labels unique and concise. - Preserve keyboard search and selection behavior when supplying custom `components`. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | -------------- | --------------------------------------------- | --------- | -------------------------------------------------- | | `options` \* | `Option[] \| OptionGroup[]` | — | Options displayed in the menu. | | `value` | `Option \| Option[]` | — | Selected option or options. | | `onChange` | `(value: Option \| Option[] \| null) => void` | — | Runs when the selection changes or is cleared. | | `placeholder` | `string \| LabelFunction` | Localized | Sets the empty-state label. | | `isMulti` | `boolean` | `false` | Allows multiple selected values. | | `isSortable` | `boolean` | `false` | Allows selected multi-value items to be reordered. | | `isClearable` | `boolean` | `true` | Displays a control for clearing the value. | | `isSearchable` | `boolean` | `true` | Allows options to be filtered by typing. | | `isCreatable` | `boolean` | `false` | Allows users to create values not in `options`. | | `disabled` | `boolean` | `false` | Prevents interaction. | | `isLoading` | `boolean` | `false` | Displays the loading state. | | `showError` | `boolean` | `false` | Applies the validation error style. | _\* An asterisk denotes that a prop is required._ # Relationship and Upload Inputs Source: https://payloadcms.com/docs/beta/ui-components/relationship-and-upload-inputs `RelationshipInput` and `UploadInput` are the input-level interfaces used by Payload's relationship-backed fields. Use them when replacing a field component while preserving Payload's document-selection workflows. ## Import ```tsx import { RelationshipInput, UploadInput } from '@payloadcms/ui' ``` ## RelationshipInput **Implementation** ```tsx const [value, setValue] = useState([]) ``` **Styling** Style the RelationshipInput select surface through its field wrapper while preserving document drawer behavior. ```css .field-type.relationship .rs__control { background: var(--color-bg); border-color: var(--color-border); border-radius: 6px; } ``` - `--color-bg`: Relationship control surface. - `border-color`: Relationship control outline. - `border-radius`: Relationship control corner radius. `RelationshipInput` selects documents from one or more Collections. Use `appearance="select"` for searchable inline results or `appearance="drawer"` to choose documents from a List Drawer. ## UploadInput **Implementation** ```tsx const [value, setValue] = useState() ``` **Styling** Customize the UploadInput drop target and its Create New and Choose from existing controls without changing their drawer behavior. ```css .field-type.upload .dropzone { background: var(--color-bg-secondary); border-color: var(--color-border); border-radius: 6px; } .field-type.upload .btn--style-pill { border-color: var(--color-border); } ``` - `background`: Upload selection surface. - `border-color`: Drop target or action outline. - `border-radius`: Upload selection corner radius. `UploadInput` relates documents from an upload-enabled Collection. Its empty state includes **Create New**, **Choose from existing**, and drag-and-drop file intake when the current user has permission to create uploads. This differs from [`Dropzone`](/docs/v4/ui-components/dropzone.md), which only handles incoming files and has no awareness of Collections or existing documents. ## RelationshipInput props | Prop | Type | Default | Description | | --------------- | ------------------------------------------ | ---------- | ----------------------------------------------- | | `relationTo` \* | `string[]` | — | Identifies the selectable Collections. | | `path` \* | `string` | — | Identifies the form field. | | `hasMany` \* | `boolean` | — | Selects one document or multiple documents. | | `value` | `ValueWithRelation \| ValueWithRelation[]` | — | Supplies the controlled relationship value. | | `onChange` \* | `(value) => void` | — | Receives the updated relationship value. | | `appearance` | `'select' \| 'drawer'` | `'select'` | Chooses inline search or List Drawer selection. | | `allowCreate` | `boolean` | `true` | Allows creating related documents. | | `allowEdit` | `boolean` | `true` | Allows opening selected documents for editing. | | `filterOptions` | `FilterOptionsResult` | — | Restricts selectable documents. | | `isSortable` | `boolean` | `true` | Enables reordering when `hasMany` is enabled. | | `readOnly` | `boolean` | `false` | Disables selection and editing. | ## UploadInput props | Prop | Type | Default | Description | | ---------------- | ----------------------------------------- | ------- | ----------------------------------------------- | | `relationTo` \* | `string \| string[]` | — | Identifies the upload-enabled Collections. | | `path` \* | `string` | — | Identifies the form field. | | `value` | `string \| number \| ValueWithRelation[]` | — | Supplies the controlled upload relationship. | | `onChange` | `(value) => void` | — | Receives the updated upload relationship. | | `allowCreate` | `boolean` | `false` | Shows file creation when permissions allow it. | | `hasMany` | `boolean` | `false` | Allows multiple upload documents. | | `displayPreview` | `boolean` | — | Displays thumbnails for selected uploads. | | `maxRows` | `number` | — | Limits selections in multi-value mode. | | `filterOptions` | `FilterOptionsResult` | — | Restricts documents shown in the List Drawer. | | `readOnly` | `boolean` | `false` | Disables selection, removal, and upload intake. | _\* An asterisk denotes that a prop is required._ ## Provider requirements These inputs are intended for Payload Custom Fields. They rely on the Admin Panel's Config, Auth, Translation, Locale, Modal, drawer, and document-event contexts. `UploadInput` also uses Payload's bulk-upload context. Use the complete `RelationshipField` or `UploadField` when Payload should manage the field configuration and form connection for you. # ShimmerEffect Source: https://payloadcms.com/docs/beta/ui-components/shimmer-effect The `ShimmerEffect` component provides a visual placeholder while content is loading. Match its dimensions to the content that will replace it to reduce layout shift. ## Import Inside a Payload Admin Panel Custom Component: ```tsx import { ShimmerEffect } from '@payloadcms/ui' ``` You can also import the component directly: ```tsx import { ShimmerEffect } from '@payloadcms/ui/elements/ShimmerEffect' ``` ## Basic usage **Implementation** ```tsx ``` **Styling** Override ShimmerEffect tokens on the component to match a custom surface without changing global background colors. ```css @layer payload { .shimmer-effect { --shine-bg: #ebe9f5; --shine-fg: #f8f7fc; --radius-medium: 0.5rem; } } ``` - `--shine-bg`: Base placeholder color. - `--shine-fg`: Animated highlight color. - `--radius-medium`: Default placeholder corner radius. ## Composing a skeleton Combine multiple shimmer elements to approximate the final layout. **Implementation** ```tsx ``` **Styling** Override ShimmerEffect tokens on the component to match a custom surface without changing global background colors. ```css @layer payload { .shimmer-effect { --shine-bg: #ebe9f5; --shine-fg: #f8f7fc; --radius-medium: 0.5rem; } } ``` - `--shine-bg`: Base placeholder color. - `--shine-fg`: Animated highlight color. - `--radius-medium`: Default placeholder corner radius. Loading indicators should not replace an accessible loading label or status when users need to know that work is in progress. ## Accessibility - Keep the loading announcement outside the decorative shimmer element. - Avoid changing the placeholder dimensions when the final content loads. - Respect the surrounding interface's reduced-motion behavior. ## Common props These are the props most commonly used with this component. See its exported types in `@payloadcms/ui` for the complete list. | Prop | Type | Default | Description | | --------------------- | ------------------ | -------- | --------------------------------------------------- | | `height` | `number \| string` | `'60px'` | Sets the placeholder height. | | `width` | `number \| string` | `'100%'` | Sets the placeholder width. | | `animationDelay` | `string` | `'0ms'` | Delays the shimmer animation. | | `transparent` | `boolean` | `false` | Allows the underlying background to remain visible. | | `disableInlineStyles` | `boolean` | `false` | Leaves dimensions to a custom class or stylesheet. | # Step Navigation Source: https://payloadcms.com/docs/beta/ui-components/step-navigation Step navigation is the breadcrumb trail rendered in the Admin Panel header. In a Custom View, use `SetStepNav` or `useStepNav` to populate that existing interface—you do not need to render another breadcrumb component. ## Import ```tsx import { SetStepNav, type StepNavItem, useStepNav } from '@payloadcms/ui' ``` ## Custom View usage Use `SetStepNav` when the complete breadcrumb trail is known while rendering the Custom View. **Implementation** ```tsx const stepNav = [ { label: 'Orders', url: '/admin/collections/orders' }, { label: 'Order #1042' }, ] export function OrderView() { return ( <>

Order #1042

) } ``` **Styling** Target the existing StepNav in your Admin Panel stylesheet to adjust breadcrumb spacing and linked-item treatment across Custom Views. ```css .step-nav { gap: 0.75rem; } .step-nav a { color: #6d5dfc; text-decoration-color: #b8adff; } ``` - `gap`: Space between the home link, separators, and breadcrumb items. - `color`: Linked breadcrumb text and inherited icon color. - `text-decoration-color`: Underline color shown when a breadcrumb link is hovered or focused. `SetStepNav` renders no visible element itself. It updates the step navigation provider that surrounds Admin Panel views, and the header displays the resulting items. Define a static `nav` array outside the component when its contents do not change. This keeps the array reference stable and avoids updating the context after every render. ## Dynamic navigation Use `useStepNav` when breadcrumb items depend on state that changes while the view remains mounted. ```tsx 'use client' import { useEffect } from 'react' import { useStepNav } from '@payloadcms/ui' export function OrderView({ orderNumber }: { orderNumber: string }) { const { setStepNav } = useStepNav() useEffect(() => { setStepNav([ { label: 'Orders', url: '/admin/collections/orders' }, { label: `Order #${orderNumber}` }, ]) }, [orderNumber, setStepNav]) return

Order #{orderNumber}

} ``` Prefer `SetStepNav` unless you need direct access to the current `stepNav` value or need to update it in response to local state. ## StepNavItem Each item contains a `label` and may contain a `url`. | Property | Type | Description | | ---------- | --------------------------------------------- | --------------------------------------------------- | | `label` \* | `StaticLabel \| LabelFunction \| JSX.Element` | Displays the translated or custom breadcrumb label. | | `url` | `string` | Renders the item as a link when present. | Items with a `url` are rendered as links. Items without a `url` are rendered as text. The final item is considered current by default, but it remains a link when a `url` is provided. _\* An asterisk denotes that a property is required._ ## SetStepNav props These are the props accepted by `SetStepNav`. | Prop | Type | Default | Description | | -------- | --------------- | ------- | ------------------------------------------------- | | `nav` \* | `StepNavItem[]` | — | Replaces the items displayed in the Admin header. | _\* An asterisk denotes that a prop is required._ ## Accessibility - Use short labels that identify each level in the current hierarchy. - Keep the current location as the final item. - Provide URLs for earlier items when users can safely return to those views. - Avoid duplicating the current page heading in every breadcrumb level. ## Provider requirements `SetStepNav` and `useStepNav` depend on the step navigation provider already included in the Payload Admin Panel. Use them within an Admin Panel Custom Component or Custom View. Outside that context, setting the navigation is a no-op. See [Custom Views](/docs/v4/custom-components/custom-views.md) for configuring a view in the Admin Panel. # Table Cells Source: https://payloadcms.com/docs/beta/ui-components/table-cells Payload's table-cell components format field values inside Admin Panel tables. Use `DefaultCell` when a custom table should follow Payload's built-in field formatting, or register a custom Cell Component when a column needs purpose-specific output. This guide covers the table-cell primitives exported from `@payloadcms/ui`. The Field Config shown below is the registration point for a custom cell, not a general guide to Payload fields. ## Import ```tsx import { DateCell, DefaultCell, RenderDefaultCell, useCellProps, } from '@payloadcms/ui' import type { DefaultCellComponentProps, DefaultServerCellComponentProps, } from 'payload' ``` ## Custom cell A custom Cell Component receives the current value as `cellData` and the complete document as `rowData`. Use the value to apply a distinct visual treatment to each option your field supports. **Implementation** ```tsx 'use client' import type { DefaultCellComponentProps } from 'payload' import { Pill } from '@payloadcms/ui' const variantLabels = { alpha: 'Alpha', beta: 'Beta', gamma: 'Gamma', delta: 'Delta', epsilon: 'Epsilon', } as const type Variant = keyof typeof variantLabels const isVariant = (value: unknown): value is Variant => typeof value === 'string' && value in variantLabels export const CustomCell = ({ cellData }: DefaultCellComponentProps) => { const variant = isVariant(cellData) ? cellData : 'other' const label = isVariant(cellData) ? variantLabels[cellData] : String(cellData ?? 'Other') return ( {label} ) } ``` **Styling** Use one shared cell rule and assign color tokens from a stable modifier class for each known value. Values without a modifier use the neutral fallback colors. ```css .table .custom-cell { background: var(--cell-background, var(--color-bg-secondary)); border: 1px solid var(--cell-border, var(--color-border)); border-radius: 999px; color: var(--cell-text, var(--color-text)); font-weight: 600; } .custom-cell--alpha { --cell-background: #eee8ff; --cell-border: #a99be8; --cell-text: #41317d; } .custom-cell--beta { --cell-background: #dff1ff; --cell-border: #78add1; --cell-text: #174d70; } .custom-cell--gamma { --cell-background: #ffe3f0; --cell-border: #d68eae; --cell-text: #71334f; } .custom-cell--delta { --cell-background: #dff5e8; --cell-border: #7eb996; --cell-text: #245d3b; } .custom-cell--epsilon { --cell-background: #fff0d9; --cell-border: #d2a25d; --cell-text: #704914; } ``` - `--cell-background`: Surface color for a custom value. - `--cell-text`: Text color for a custom value. - `--cell-border`: Outline color for a custom value. - `border-radius`: Shape of the custom value treatment. - `font-weight`: Emphasis applied to the custom value. This example derives a stable `custom-cell--{value}` modifier from an allowlist. A shared CSS rule defines the cell treatment, while each modifier supplies its background, text, and border colors. Unexpected values fall back to the neutral Payload theme colors. Register the component through `admin.components.Cell` on the field whose column it replaces: ```ts import type { Field } from 'payload' export const variantField: Field = { name: 'variant', type: 'select', options: ['alpha', 'beta', 'gamma', 'delta', 'epsilon'], admin: { components: { Cell: '/components/CustomCell', }, }, } ``` Use `DefaultCellComponentProps` for a Client Component and `DefaultServerCellComponentProps` for a Server Component. The prop types come from `payload`, while any visual primitives used by the cell—such as `Pill` in this example—come from `@payloadcms/ui`. ## Default field rendering `DefaultCell` selects Payload's standard renderer for the supplied field type. For types without a specialized renderer, it displays the value directly. **Implementation** ```tsx ``` **Styling** Target the column class generated from each accessor to adjust a built-in cell without replacing its value formatting. ```css .table .cell-title { color: var(--color-text); font-weight: 600; } .table .cell-publishedAt { color: var(--color-text-secondary); } ``` - `color`: Text color for a specific column. - `font-weight`: Text emphasis for a specific column. Pass the same field configuration and row data used to build the surrounding column. Set `link` when the value should navigate to the document, or provide `onClick` when the cell performs a custom selection action. `DateCell` is the specialized date renderer used by `DefaultCell`. It applies the field's `admin.date.displayFormat`, the root Admin date format, and timezone data when configured. Most custom tables should use `DefaultCell` for a date field rather than call `DateCell` directly. ## Advanced cell composition `RenderDefaultCell` is the lower-level wrapper used by Payload's List View. In addition to rendering `DefaultCell`, it coordinates linked cells with `TableColumnsProvider` and a List Drawer. `useCellProps` reads the current `DefaultCellComponentProps` from that wrapper. It is useful when replacing Payload's linked-cell output, but it returns `null` outside `RenderDefaultCell`. For an ordinary custom Cell Component, use the props passed directly to the component instead of adding either API. ## Common props These are the props most commonly used with `DefaultCell`. See `DefaultCellComponentProps` from `payload` for the complete type. | Prop | Type | Description | | ----------------- | ------------- | -------------------------------------------------------- | | `cellData*` | `unknown` | Supplies the value for the current field and row. | | `collectionSlug*` | `string` | Identifies the Collection represented by the table. | | `field*` | `ClientField` | Selects Payload's renderer and formatting configuration. | | `rowData*` | `RowData` | Supplies the complete document represented by the row. | | `link` | `boolean` | Wraps the displayed value in a document link. | | `linkURL` | `string` | Overrides the URL generated for a linked cell. | | `onClick` | `function` | Replaces linked behavior with a cell action. | | `customCellProps` | `object` | Supplies project-specific data to a custom cell. | _\* An asterisk denotes that a prop is required._ ## Accessibility - Render text alongside color when a custom cell communicates status. - Use a link only when the cell navigates and a button only when it performs an action. - Keep custom output concise so the table remains readable at narrow widths. - Preserve an understandable text value when adding icons or visual formatting. ## Provider requirements `DefaultCell` and `DateCell` use the Config and Translation providers supplied by the Payload Admin Panel. `RenderDefaultCell` additionally expects Table Columns and List Drawer context. Custom Cell Components registered through a Field Config receive their props from Payload automatically. See [Fields](/docs/v4/fields/overview.md#cell) for the complete Cell Component extension point. Use the exported `Table` component to compose rows and columns around these cells. # Text Inputs Source: https://payloadcms.com/docs/beta/ui-components/text-inputs `TextInput` and `TextareaInput` combine native text controls with Payload's label, description, validation, and read-only field presentation. Use them when building a Custom Field that manages its own value. ## Import ```tsx import { TextareaInput, TextInput } from '@payloadcms/ui' ``` ## TextInput **Implementation** ```tsx const [value, setValue] = useState('Quarterly report') setValue(event.target.value)} path="title" required value={value} /> ``` **Styling** Target the TextInput field wrapper and native input to customize its surface, outline, and focus treatment. ```css .field-type.text input { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 6px; } .field-type.text input:focus { border-color: var(--color-border-brand-strong); } ``` - `--color-bg`: Input surface inherited from the theme. - `border`: Input outline. - `border-radius`: Input corner radius. `TextInput` is controlled. Pass the current string through `value` and update it from the native change event. Set `hasMany` to render Payload's sortable multi-value text control instead of a native input. ## TextareaInput **Implementation** ```tsx const [value, setValue] = useState( 'A concise summary for the project dashboard.', ) setValue(event.target.value)} path="summary" rows={4} value={value} /> ``` **Styling** Style the native textarea inside Payload’s field wrapper while retaining its label, description, and validation layout. ```css .field-type.textarea textarea { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 6px; resize: vertical; } .field-type.textarea textarea:focus { border-color: var(--color-border-brand-strong); } ``` - `--color-bg`: Textarea surface inherited from the theme. - `border`: Textarea outline. - `border-radius`: Textarea corner radius. - `resize`: Allowed manual resize direction. Use `rows` to establish the initial height. The value remains controlled by the surrounding Custom Field. ## Common props These props are shared by both inputs unless noted otherwise. | Prop | Type | Default | Description | | ------------- | ------------------------- | ------- | ---------------------------------------------- | | `path` \* | `string` | — | Identifies the field and connects its UI. | | `value` | `string` | `''` | Supplies the controlled value. | | `onChange` | `ChangeEventHandler` | — | Receives native input changes. | | `label` | `StaticLabel` | — | Displays Payload's field label. | | `description` | `StaticDescription` | — | Displays helper text beneath the input. | | `placeholder` | `string \| LabelFunction` | — | Displays translated placeholder text. | | `required` | `boolean` | `false` | Displays the required marker. | | `readOnly` | `boolean` | `false` | Disables editing and applies read-only styles. | | `showError` | `boolean` | Form | Displays the current validation error. | | `BeforeInput` | `ReactNode` | — | Inserts content before the native control. | | `AfterInput` | `ReactNode` | — | Inserts content after the native control. | | `rows` | `number` | — | Sets the TextareaInput row count. | | `hasMany` | `boolean` | `false` | Enables TextInput's multi-value mode. | _\* An asterisk denotes that a prop is required._ ## Custom field usage Inside a Custom Field, connect the input to Payload's form state with `useField`: ```tsx 'use client' import { TextInput, useField } from '@payloadcms/ui' export function CustomTextField({ field, path }) { const { setValue, showError, value } = useField({ path }) return ( setValue(event.target.value)} path={path} required={field.required} showError={showError} value={value} /> ) } ``` ## Provider requirements The Admin Panel supplies the translation and form contexts used by the built-in label, description, and validation components. Use these inputs inside a Payload Custom Field, or provide equivalent contexts when rendering them elsewhere. # Upload Helpers Source: https://payloadcms.com/docs/beta/ui-components/upload-helpers Payload's upload helpers support the file-management interface around an upload-enabled Collection. Use them when extending an upload workflow that already has a document, upload configuration, and the surrounding Admin context. ## Import ```tsx import { EditUpload, FileDetails, PreviewSizes } from '@payloadcms/ui' ``` ## File details `FileDetails` presents a file thumbnail, URL, dimensions, size, and MIME type. It can also display removal, image-adjustment, generated-variant, and custom upload actions. **Implementation** ```tsx ``` **Styling** Customize the FileDetails surface and thumbnail while preserving its metadata, copy, adjustment, and removal behavior. ```css .file-details { background: #f9f8ff; border: 1px solid #c8c0ff; border-radius: 8px; } .file-details__thumbnail { border-radius: 6px; } ``` - `background`: File summary surface. - `border`: File summary outline. - `border-radius`: Summary and thumbnail corner radius. The component renders a static summary by default. Pass the sortable row props when it represents one item in a multi-upload field. ## Generated image variants `PreviewSizes` lets users compare the original upload with image variants generated by the Collection's upload configuration. **Implementation** ```tsx ``` **Styling** Target the selected size and structural regions to align the PreviewSizes browser with a project’s media treatment. ```css .preview-sizes__sizeOption:hover, .preview-sizes--selected { background: #f3f1ff; } .preview-sizes__imageWrap { border-color: #c8c0ff; } ``` - `background`: Selected and hovered size surface. - `border-color`: Divider between the preview and size list. The available options come from `uploadConfig.variants` and matching entries in `doc.variants`. Selecting an option updates the main preview and metadata. ## Image adjustments `EditUpload` provides Payload's crop and focal-point controls. Use `showCrop` and `showFocalPoint` to expose the adjustments enabled by the upload Collection. `EditUpload` renders inside Payload's shared upload-edit modal; rendering the component does not open it. Mount it as part of an existing upload workflow and open it through the image-adjustment action supplied by `Upload` or `FileDetails`. ```tsx updateUploadEdits(uploadEdits)} showCrop showFocalPoint /> ``` `onSave` receives the crop, focal point, and resulting pixel dimensions. The component closes Payload's upload-edit modal after applying the changes. ## Common props These are the props most commonly used with this component family. See the exported types in `@payloadcms/ui` for the complete list. ### FileDetails | Prop | Type | Default | Description | | --------------------- | ------------------------------------- | ------- | ------------------------------------------------ | | `collectionSlug` \* | `string` | — | Identifies the upload-enabled Collection. | | `doc` \* | `Data & { variants?: FileSizes }` | — | Supplies file metadata and generated variants. | | `uploadConfig` \* | `SanitizedCollectionConfig['upload']` | — | Controls previews and available upload behavior. | | `enableAdjustments` | `boolean` | `false` | Shows the image-adjustment action. | | `hasImageSizes` | `boolean` | `false` | Shows the generated-variant preview action. | | `hideRemoveFile` | `boolean` | `false` | Hides the file-removal control. | | `customUploadActions` | `ReactNode[]` | — | Adds project-specific upload actions. | ### PreviewSizes | Prop | Type | Default | Description | | ----------------- | ------------------------------------------------ | ------- | ----------------------------------------------- | | `doc` \* | `Data & { variants?: Record }` | — | Supplies original and generated image metadata. | | `uploadConfig` \* | `SanitizedCollectionConfig['upload']` | — | Defines the configured image-variant order. | | `imageCacheTag` | `string` | — | Appends a cache-busting tag to preview URLs. | ### EditUpload | Prop | Type | Default | Description | | ------------------- | ------------------------------ | ------- | ------------------------------------------- | | `fileName` \* | `string` | — | Labels the image being edited. | | `fileSrc` \* | `string` | — | Supplies the image URL. | | `onSave` | `(edits: UploadEdits) => void` | — | Receives the applied image adjustments. | | `showCrop` | `boolean` | `false` | Enables crop controls. | | `showFocalPoint` | `boolean` | `false` | Enables focal-point controls. | | `initialCrop` | `UploadEdits['crop']` | — | Sets the initial crop region. | | `initialFocalPoint` | `{ x: number; y: number }` | — | Sets the initial focal point by percentage. | _\* An asterisk denotes that a prop is required._ ## Accessibility - Provide meaningful upload `alt` data so generated-variant previews describe the underlying image. - Keep file names and metadata available alongside visual thumbnails. - Preserve keyboard access when adding custom upload actions. - Label destructive removal actions clearly and confirm them when removal cannot be reversed. ## Provider requirements `FileDetails` and `EditUpload` rely on upload, translation, and modal behavior supplied by the Admin Panel. `PreviewSizes` expects the normalized document and upload configuration produced by an upload-enabled Collection. For complete form and persistence behavior, use the exported `UploadField` component. # Authentication Overview Source: https://payloadcms.com/docs/beta/authentication/overview [Video: Simplified Authentication for Headless CMS: Unlocking Reusability in One Line](https://www.youtube.com/watch?v=CT4KafeJjTI) Authentication is a critical part of any application. Payload provides a secure, portable way to manage user accounts out of the box. Payload Authentication is designed to be used in both the [Admin Panel](/docs/v4/admin/overview.md), as well as your own external applications, completely eliminating the need for paid, third-party platforms and services. Here are some common use cases of Authentication in your own applications: - Customer accounts for an e-commerce app - User accounts for a SaaS product - P2P apps or social sites where users need to log in and manage their profiles - Online games where players need to track their progress over time When Authentication is enabled on a [Collection](/docs/v4/configuration/collections.md), Payload injects all necessary functionality to support the entire user flow. This includes all [auth-related operations](/docs/v4/authentication/operations.md) like account creation, logging in and out, and resetting passwords, all [auth-related emails](/docs/v4/authentication/email.md) like email verification and password reset, as well as any necessary UI to manage users from the Admin Panel. To enable Authentication on a Collection, use the `auth` property in the [Collection Config](/docs/v4/configuration/collections.md#config-options): ```ts import type { CollectionConfig } from 'payload' export const Users: CollectionConfig = { // ... auth: true, // highlight-line } ``` ![Authentication Admin Panel functionality](https://payloadcms.com/images/docs/auth-overview.jpg) _Admin Panel screenshot depicting an Admins Collection with Auth enabled_ ## Config Options Any [Collection](/docs/v4/configuration/collections.md) can opt-in to supporting Authentication. Once enabled, each Document that is created within the Collection can be thought of as a "user". This enables a complete authentication workflow on your Collection, such as logging in and out, resetting their password, and more. > **Note:** By default, Payload provides an auth-enabled `User` Collection which > is used to access the Admin Panel. [More > details](/docs/v4/admin/overview.md#the-admin-user-collection). To enable Authentication on a Collection, use the `auth` property in the [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const Admins: CollectionConfig = { // ... // highlight-start auth: { tokenExpiration: 7200, // How many seconds to keep the user logged in verify: true, // Require email verification before being allowed to authenticate maxLoginAttempts: 5, // Automatically lock a user out after X amount of failed logins lockTime: 600 * 1000, // Time period to allow the max login attempts // More options are available }, // highlight-end } ``` > **Tip:** For default auth behavior, set `auth: true`. This is a good starting > point for most applications. > **Note:** Auth-enabled Collections will be automatically injected with the > `hash`, `salt`, and `email` fields. [More > details](/docs/v4/fields/overview.md#field-names). The `hash` field stores a PBKDF2-SHA256 derived key prefixed with the scheme it was created with, for example `pbkdf2-sha256-v1:`. Values stored before that prefix was introduced stay valid and are rewritten to the current scheme after the next successful login, which also assigns a new `salt`. Treat the stored format as internal and read these fields through Payload rather than deriving them yourself. New hashes use 600,000 iterations by default. Some environments, such as Cloudflare Workers, do not support more than 100,000 iterations. To support them, set `auth.passwordHashIterations` in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ auth: { passwordHashIterations: 100000, }, // ... }) ``` Each iteration count uses its own scheme prefix, so hashes created with either count can be verified. After a successful login, a hash with a different count is rehashed with the configured count. > **Note:** An environment limited to 100,000 iterations cannot verify existing > 600,000-iteration hashes. Those users cannot log in there until they reset > their passwords. A user who logs in one time through a Node.js deployment with > `passwordHashIterations: 100000` also gets a new hash that the environment can > verify. The following options are available: | Option | Description | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`cookies`** | Set cookie options, including `secure`, `sameSite`, and `domain`. For advanced users. | | **`depth`** | How many levels deep a `user` document should be populated when creating the JWT and binding the `user` to the `req`. Defaults to `0` and should only be modified if absolutely necessary, as this will affect performance. | | **`disableLocalStrategy`** | Set to `true` to disable local authentication, `false` (default) to keep it enabled, or an object to retain auth fields while disabling it. [More details](#disable-local-strategy). | | **`forgotPassword`** | Customize the way that the `forgotPassword` operation functions. [More details](/docs/v4/authentication/email.md#forgot-password). | | **`lockTime`** | Set the time (in milliseconds) that a user should be locked out if they fail authentication more times than `maxLoginAttempts` allows for. | | **`loginWithUsername`** | Ability to allow users to login with username/password. [More](/docs/v4/authentication/overview.md#login-with-username) | | **`maxLoginAttempts`** | Only allow a user to attempt logging in X amount of times. Automatically locks out a user from authenticating if this limit is passed. Set to `0` to disable. | | **`refreshTokens`** | Set to `true` to also return a refresh token from login, which keeps clients logged in after their token expires. [More details](#refresh-tokens). | | **`removeTokenFromResponses`** | Set to true if you want to remove the token from the returned authentication API responses such as login or refresh. | | **`strategies`** | Advanced - an array of custom authentication strategies to extend this collection's authentication with. [More details](/docs/v4/authentication/custom-strategies.md). | | **`tokenExpiration`** | How long (in seconds) to keep the user logged in. JWTs and HTTP-only cookies will both expire at the same time. | | **`useAPIKey`** | Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection. [More details](/docs/v4/authentication/api-keys.md). | | **`useSessions`** | True by default. Set to `false` to use stateless JWTs for authentication instead of sessions. Stateless JWTs cannot be revoked, so they stay valid until `tokenExpiration` even after a password change. | | **`verify`** | Set to `true` or pass an object with verification options to require users to verify by email before they are allowed to log into your app. [More details](/docs/v4/authentication/email.md#email-verification). | ### Disable Local Strategy The built-in local strategy authenticates users with an email or username and password. It is enabled by default. Set `disableLocalStrategy` to `true` to disable it, or to `false` to explicitly keep it enabled. Only disable the local strategy if you have configured another authentication method, such as [API keys](/docs/v4/authentication/api-keys.md) or a [custom strategy](/docs/v4/authentication/custom-strategies.md). You can also pass an object to disable the local strategy while retaining its auth fields: ```ts { slug: 'users', auth: { disableLocalStrategy: { enableFields: true, optionalPassword: true, }, }, } ``` - `enableFields` retains the local auth fields in the database and generated types. - `optionalPassword` makes the password field optional when auth fields are retained. ### Login With Username You can allow users to login with their username instead of their email address by setting the `loginWithUsername` property to `true`. Example: ```ts { slug: 'customers', auth: { loginWithUsername: true, }, } ``` Or, you can pass an object with additional options: ```ts { slug: 'customers', auth: { loginWithUsername: { allowEmailLogin: true, // default: false requireEmail: false, // default: false }, }, } ``` **`allowEmailLogin`** If set to `true`, users can log in with either their username or email address. If set to `false`, users can only log in with their username. **`requireEmail`** If set to `true`, an email address is required when creating a new user. If set to `false`, email is not required upon creation. ## Auto-Login For testing and demo purposes you may want to skip forcing the user to login in order to access your application. Typically, all users should be required to login, however, you can speed up local development time by enabling auto-login. To enable auto-login, set the `autoLogin` property in the [Payload Config](/docs/v4/admin/overview.md#admin-options): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... // highlight-start admin: { autoLogin: process.env.NODE_ENV === 'development' ? { email: 'test@example.com', password: 'test', prefillOnly: true, } : false, }, // highlight-end }) ``` > **Warning:** The recommended way to use this feature is behind an [Environment > Variable](/docs/v4/configuration/environment-vars.md). This will ensure it is > _disabled_ in production. The following options are available: | Option | Description | | ----------------- | --------------------------------------------------------------------------------------------------------------- | | **`username`** | The username of the user to login as | | **`email`** | The email address of the user to login as | | **`password`** | The password of the user to login as. This is only needed if `prefillOnly` is set to true | | **`prefillOnly`** | If set to true, the login credentials will be prefilled but the user will still need to click the login button. | ## Auto-Refresh Turning this property on will allow users to stay logged in indefinitely while their browser is open and on the admin panel, by automatically refreshing their authentication token before it expires. To enable auto-refresh for user tokens, set `autoRefresh: true` in the [Payload Config](/docs/v4/admin/overview.md#admin-options) to: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... // highlight-start admin: { autoRefresh: true, }, // highlight-end }) ``` ## Refresh Tokens Apps that aren't always running, like mobile apps or CLIs, can't refresh their token before it expires. With `refreshTokens`, login also returns a refresh token, which the [refresh operation](/docs/v4/authentication/operations.md#refresh) trades for a new token and a new refresh token, even after the token expired: ```ts import type { CollectionConfig } from 'payload' export const Users: CollectionConfig = { slug: 'users', auth: { // highlight-start refreshTokens: { // Optional. How many seconds the session lasts without a refresh, which is how long its // refresh token can be used. Defaults to 30 days. expiration: 60 * 60 * 24 * 7, }, // highlight-end }, } ``` ```ts const res = await fetch('http://localhost:3000/api/users/refresh-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken }), }) // Store the new refresh token for next time const { refreshedToken, refreshToken: nextRefreshToken } = await res.json() ``` Refresh tokens don't expire on their own. They work as long as their session, which lasts `expiration` from the last refresh instead of `tokenExpiration`. The token itself still expires after `tokenExpiration`. Each refresh token is meant to be used once, so store the new one after every refresh. For 30 seconds after a refresh, the refresh token from right before it still works, so clients that refresh twice at once stay logged in. If a used refresh token comes back otherwise, Payload treats it as copied and logs out its session. Logging out ends refresh tokens too. They require `useSessions`, and aren't returned with `removeTokenFromResponses`. ## Operations All auth-related operations are available via Payload's REST, Local, and GraphQL APIs. These operations are automatically added to your Collection when you enable Authentication. [More details](/docs/v4/authentication/operations.md). ## Strategies Out of the box Payload ships with three powerful Authentication strategies: - [HTTP-Only Cookies](/docs/v4/authentication/cookies.md) - [JSON Web Tokens (JWT)](/docs/v4/authentication/jwt.md) - [API-Keys](/docs/v4/authentication/api-keys.md) Each of these strategies can work together or independently. You can also create your own custom strategies to fit your specific needs. [More details](/docs/v4/authentication/custom-strategies.md). ### HTTP-Only Cookies [HTTP-only cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) are a highly secure method of storing identifiable data on a user's device so that Payload can automatically recognize a returning user until their cookie expires. They are totally protected from common XSS attacks and **cannot be read by JavaScript in the browser**, unlike JWT's. [More details](/docs/v4/authentication/cookies.md). ### JSON Web Tokens JWT (JSON Web Tokens) can also be utilized to perform authentication. Tokens are generated on `login`, `refresh` and `me` operations and can be attached to future requests to authenticate users. [More details](/docs/v4/authentication/jwt.md). ### API Keys API Keys can be enabled on auth collections. These are particularly useful when you want to authenticate against Payload from a third party service. [More details](/docs/v4/authentication/api-keys.md). ### Custom Strategies There are cases where these may not be enough for your application. Payload is extendable by design so you can wire up your own strategy when you need to. [More details](/docs/v4/authentication/custom-strategies.md). ### Access Control Default auth fields including `email`, `username`, and `password` can be overridden by defining a custom field with the same name in your collection config. This allows you to customize the field — including access control — while preserving the underlying auth functionality. For example, you might want to restrict the `email` field from being updated once it is created, or only allow it to be read by certain user roles. You can achieve this by redefining the field and setting access rules accordingly. Here's an example of how to restrict access to default auth fields: ```ts import type { CollectionConfig } from 'payload' export const Auth: CollectionConfig = { slug: 'users', auth: true, fields: [ { name: 'email', // or 'username' type: 'text', access: { create: () => true, read: () => false, update: () => false, }, }, { name: 'password', // this will be applied to all password-related fields including new password, confirm password. type: 'text', hidden: true, // needed only for the password field to prevent duplication in the Admin panel access: { update: () => false, }, }, ], } ``` **Note:** - Access functions will apply across the application — I.e. if `read` access is disabled on `email`, it will not appear in the Admin panel UI or API. - Restricting `read` on the `email` or `username` disables the **Unlock** action in the Admin panel as this function requires access to a user-identifying field. - When overriding the `password` field, you may need to include `hidden: true` to prevent duplicate fields being displayed in the Admin panel. # Authentication Operations Source: https://payloadcms.com/docs/beta/authentication/operations Enabling [Authentication](/docs/v4/authentication/overview.md) on a [Collection](/docs/v4/configuration/collections.md) automatically exposes additional auth-based operations in the [Local API](/docs/v4/local-api/overview.md), [REST API](/docs/v4/rest-api/overview.md), and [GraphQL API](/docs/v4/graphql/overview.md). ## Access The Access operation returns what a logged in user can and can't do with the collections and globals that are registered via your config. This data can be immensely helpful if your app needs to show and hide certain features based on [Access Control](/docs/v4/access-control/overview.md), just as the [Admin Panel](/docs/v4/admin/overview.md) does. **REST API endpoint**: `GET http://localhost:3000/api/access` Example response: ```ts { canAccessAdmin: true, collections: { pages: { create: { permission: true, }, read: { permission: true, }, update: { permission: true, }, delete: { permission: true, }, fields: { title: { create: { permission: true, }, read: { permission: true, }, update: { permission: true, }, } } } } } ``` **Example GraphQL Query**: ```graphql query { Access { pages { read { permission } } } } ``` Document access can also be queried on a collection/global basis. Access on a global can be queried like `http://localhost:3000/api/global-slug/access`, Collection document access can be queried like `http://localhost:3000/api/collection-slug/access/:id`. ## Me Returns either a logged in user with token or null when there is no logged in user. **REST API endpoint**: `GET http://localhost:3000/api/[collection-slug]/me` Example response: ```ts { user: { // The JWT "payload" ;) from the logged in user email: 'dev@payloadcms.com', createdAt: "2020-12-27T21:16:45.645Z", updatedAt: "2021-01-02T18:37:41.588Z", id: "5ae8f9bde69e394e717c8832" }, token: '34o4345324...', // The token that can be used to authenticate the user exp: 1609619861, // Unix timestamp representing when the user's token will expire } ``` **Example GraphQL Query**: ```graphql query { me[collection-singular-label] { user { email } exp } } ``` ## Login Accepts an `email` and `password`. On success, it will return the logged in user as well as a token that can be used to authenticate. With [refresh tokens](/docs/v4/authentication/overview.md#refresh-tokens), it also returns a `refreshToken`. In the GraphQL and REST APIs, this operation also automatically sets an HTTP-only cookie including the user's token. If you pass a `res` to the Local API operation, Payload will set a cookie there as well. **Example REST API login**: ```ts const res = await fetch('http://localhost:3000/api/[collection-slug]/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'dev@payloadcms.com', password: 'this-is-not-our-password...or-is-it?', }), }) const json = await res.json() // JSON will be equal to the following: /* { user: { email: 'dev@payloadcms.com', createdAt: "2020-12-27T21:16:45.645Z", updatedAt: "2021-01-02T18:37:41.588Z", id: "5ae8f9bde69e394e717c8832" }, token: '34o4345324...', exp: 1609619861 } */ ``` **Example GraphQL Mutation**: ```graphql mutation { login[collection-singular-label](email: "dev@payloadcms.com", password: "yikes") { user { email } exp token } } ``` **Example Local API login**: ```ts const result = await payload.login({ collection: 'collection-slug', overrideAccess: false, // intentionally run the collection's login access control data: { email: 'dev@payloadcms.com', password: 'get-out', }, }) ``` > **Server Functions:** Payload offers a ready-to-use `login` server function > that utilizes the Local API. For integration details and examples, check out > the [Server Function docs](/docs/v4/authentication/server-functions.md). ## Logout As Payload sets HTTP-only cookies, logging out cannot be done by just removing a cookie in JavaScript, as HTTP-only cookies are inaccessible by JS within the browser. So, Payload exposes a `logout` operation to delete the token in a safe way. **Example REST API logout**: ```ts const res = await fetch( 'http://localhost:3000/api/[collection-slug]/logout?allSessions=false', { method: 'POST', headers: { 'Content-Type': 'application/json', }, }, ) ``` **Example GraphQL Mutation**: ``` mutation { logoutUser(allSessions: false) } ``` > **Server Functions:** Payload provides a ready-to-use `logout` server function > that manages the user's cookie for a seamless logout. For integration details > and examples, check out the [Server Function docs](/docs/v4/authentication/server-functions.md). #### Logging out with sessions enabled By default, logging out will only end the session pertaining to the JWT that was used to log out with. However, you can pass `allSessions: true` to the logout operation in order to end all sessions for the user logging out. #### Password changes and sessions With sessions enabled, changing a user's password ends that user's other sessions, so tokens that were issued before the change stop working. - Updating the password of the user making the request - through the admin Account view, the REST and GraphQL update endpoints, or a Local API update that receives the authenticated user - keeps the session that the request was made with and ends the rest. - Updating a user's password on their behalf, such as an admin updating another user, ends all of that user's sessions. - The `resetPassword` operation ends all existing sessions and returns a token for the new session that it creates. A Local API update that runs without an authenticated user has no session to keep, so it ends all of the user's sessions. Pass the `user` returned by `payload.auth` when the user making the request should stay logged in: ```ts const { user } = await payload.auth({ headers }) await payload.update({ collection: 'users', overrideAccess: false, id: user.id, data: { password: 'new-password', }, user, }) ``` ## Refresh Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user. This operation requires a non-expired token to send back a new one. If the user's token has already expired, you will need to allow them to log in again to retrieve a new token, unless you use [refresh tokens](/docs/v4/authentication/overview.md#refresh-tokens). Then send `{ refreshToken }` in the request body, or as the `refreshToken` argument in GraphQL, and the response also includes the next `refreshToken`. If successful, this operation will automatically renew the user's HTTP-only cookie and will send back the updated token in JSON. **Example REST API token refresh**: ```ts const res = await fetch( 'http://localhost:3000/api/[collection-slug]/refresh-token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, }, ) const json = await res.json() // JSON will be equal to the following: /* { user: { email: 'dev@payloadcms.com', createdAt: "2020-12-27T21:16:45.645Z", updatedAt: "2021-01-02T18:37:41.588Z", id: "5ae8f9bde69e394e717c8832" }, refreshedToken: '34o4345324...', exp: 1609619861 } */ ``` **Example GraphQL Mutation**: ``` mutation { refreshToken[collection-singular-label] { user { email } refreshedToken } } ``` > **Server Functions:** Payload exports a ready-to-use `refresh` server function > that automatically renews the user's token and updates the associated cookie. > For integration details and examples, check out the [Server Function > docs](/docs/v4/authentication/server-functions.md). ## Verify by Email If your collection supports email verification, the Verify operation will be exposed which accepts a verification token and sets the user's `_verified` property to `true`, thereby allowing the user to authenticate with the Payload API. **Example REST API user verification**: ```ts const res = await fetch( `http://localhost:3000/api/[collection-slug]/verify/${TOKEN_HERE}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }, ) ``` **Example GraphQL Mutation**: ```graphql mutation { verifyEmail[collection-singular-label](token: "TOKEN_HERE") } ``` **Example Local API verification**: ```ts const result = await payload.verifyEmail({ collection: 'collection-slug', token: 'TOKEN_HERE', }) ``` **Note:** the token you need to pass to the `verifyEmail` function is unique to verification and is not the same as the token that you can retrieve from the `forgotPassword` operation. It can be found on the user document, as a hidden `_verificationToken` field. If you'd like to retrieve this token, you can use the Local API's `find` or `findByID` methods, setting `showHiddenFields: true`. **Note:** if you do not have a `config.serverURL` set, Payload will attempt to create one for you if the user was created via REST or GraphQL by looking at the incoming `req`. But this is not supported if you are creating the user via the Local API's `payload.create()` method. If this applies to you, and you do not have a `serverURL` set, you may want to override your `verify.generateEmailHTML` function to provide a full URL to link the user to a proper verification page. ## Unlock If a user locks themselves out and you wish to deliberately unlock them, you can utilize the Unlock operation. The [Admin Panel](/docs/v4/admin/overview.md) features an Unlock control automatically for all collections that feature max login attempts, but you can programmatically unlock users as well by using the Unlock operation. To restrict who is allowed to unlock users, you can utilize the [`unlock`](/docs/v4/access-control/collections.md#unlock) access control function. **Example REST API unlock**: ```ts const res = await fetch(`http://localhost:3000/api/[collection-slug]/unlock`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }) ``` **Example GraphQL Mutation**: ``` mutation { unlock[collection-singular-label] } ``` **Example Local API unlock**: ```ts const result = await payload.unlock({ collection: 'collection-slug', overrideAccess: true, data: { email: 'dev@payloadcms.com', }, }) ``` ## Forgot Password Payload comes with built-in forgot password functionality. Submitting an email address to the Forgot Password operation will generate an email and send it to the respective email address with a link to reset their password. The link to reset the user's password contains a token which is what allows the user to securely reset their password. By default, the Forgot Password operations send users to the [Admin Panel](/docs/v4/admin/overview.md) to reset their password, but you can customize the generated email to send users to the frontend of your app instead by [overriding the email HTML](/docs/v4/authentication/email.md#forgot-password). **Example REST API Forgot Password**: ```ts const res = await fetch( `http://localhost:3000/api/[collection-slug]/forgot-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'dev@payloadcms.com', }), }, ) ``` **Example GraphQL Mutation**: ``` mutation { forgotPassword[collection-singular-label](email: "dev@payloadcms.com") } ``` **Example Local API forgot password**: ```ts const token = await payload.forgotPassword({ collection: 'collection-slug', overrideAccess: true, data: { email: 'dev@payloadcms.com', }, disableEmail: false, // you can disable the auto-generation of email via Local API }) ``` > **Note:** if you do not have a `config.serverURL` set, Payload will attempt to > create one for you if the `forgot-password` operation was triggered via REST > or GraphQL by looking at the incoming `req`. But this is not supported if you > are calling `payload.forgotPassword()` via the Local API. If you do not have a > `serverURL` set, you may want to override your > `auth.forgotPassword.generateEmailHTML` function to provide a full URL to link > the user to a proper reset-password page. > **Tip:** > > You can stop the reset-password email from being sent via using the Local API. This is helpful if > you need to create user accounts programmatically, but not set their password for them. This > effectively generates a reset password token which you can then use to send to a page you create, > allowing a user to "complete" their account by setting their password. In the background, you'd > use the token to "reset" their password. ## Reset Password After a user has "forgotten" their password and a token is generated, that token can be used to send to the reset password operation along with a new password which will allow the user to reset their password securely. **Example REST API Reset Password**: ```ts const res = await fetch(`http://localhost:3000/api/[collection-slug]/reset-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: 'TOKEN_GOES_HERE' password: 'not-today', }), }); const json = await res.json(); // JSON will be equal to the following: /* { user: { email: 'dev@payloadcms.com', createdAt: "2020-12-27T21:16:45.645Z", updatedAt: "2021-01-02T18:37:41.588Z", id: "5ae8f9bde69e394e717c8832" }, token: '34o4345324...', exp: 1609619861 } */ ``` **Example GraphQL Mutation**: ```graphql mutation { resetPassword[collection-singular-label](token: "TOKEN_GOES_HERE", password: "not-today") } ``` # Authentication Server Functions Source: https://payloadcms.com/docs/beta/authentication/server-functions Payload provides `login`, `logout`, and `refresh` server functions that manage authentication cookies for you. Each function requires your Payload config, so it must run on the server. The examples below expose the same call signatures for Next.js and TanStack Start: - `loginAction({ email, password })` - `logoutAction()` - `refreshAction()` ## Next.js Import the helpers from `@payloadcms/next/auth` in a file marked with `'use server'`: ```ts 'use server' import { login, logout, refresh } from '@payloadcms/next/auth' import config from '@payload-config' type LoginArgs = { email: string password: string } export function loginAction({ email, password }: LoginArgs) { return login({ collection: 'users', config, email, password, }) } export function logoutAction() { return logout({ config }) } export function refreshAction() { return refresh({ config }) } ``` ## TanStack Start Import the helpers from `@payloadcms/tanstack-start/server` inside each `createServerFn` handler: ```ts import { createServerFn } from '@tanstack/react-start' type LoginArgs = { email: string password: string } const loginServerFn = createServerFn({ method: 'POST' }) .validator((data: LoginArgs) => data) .handler(async ({ data }) => { const [{ login }, { default: config }] = await Promise.all([ import('@payloadcms/tanstack-start/server'), import('@payload-config'), ]) await login({ collection: 'users', config, email: data.email, password: data.password, }) }) const logoutServerFn = createServerFn({ method: 'POST' }).handler(async () => { const [{ logout }, { default: config }] = await Promise.all([ import('@payloadcms/tanstack-start/server'), import('@payload-config'), ]) return logout({ config }) }) const refreshServerFn = createServerFn({ method: 'POST' }).handler(async () => { const [{ refresh }, { default: config }] = await Promise.all([ import('@payloadcms/tanstack-start/server'), import('@payload-config'), ]) return refresh({ config }) }) export function loginAction(data: LoginArgs) { return loginServerFn({ data }) } export function logoutAction() { return logoutServerFn() } export function refreshAction() { return refreshServerFn() } ``` ## Client Usage The normalized functions can be called the same way from event handlers in either framework: ```tsx 'use client' import { loginAction } from './actions' async function handleLogin({ email, password, }: { email: string password: string }) { try { await loginAction({ email, password }) } catch (error) { // Display the error to the user } } ``` All three functions reject when the operation fails. Use the same `try`/`catch` pattern for `refreshAction` and `logoutAction` so failures do not become unhandled promise rejections. # Authentication Emails Source: https://payloadcms.com/docs/beta/authentication/email [Authentication](/docs/v4/authentication/overview.md) ties directly into the [Email](/docs/v4/email/overview.md) functionality that Payload provides. This allows you to send emails to users for verification, password resets, and more. While Payload provides default email templates for these actions, you can customize them to fit your brand. ## Email Verification Email Verification forces users to prove they have access to the email address they can authenticate. This will help to reduce spam accounts and ensure that users are who they say they are. To enable Email Verification, use the `auth.verify` property on your [Collection Config](/docs/v4/configuration/collections.md): ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { verify: true, // highlight-line }, } ``` > **Tip:** Verification emails are fully customizable. [More > details](#generateemailhtml). The following options are available: | Option | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`generateEmailHTML`** | Allows for overriding the HTML within emails that are sent to users indicating how to validate their account. [More details](#generateemailhtml). | | **`generateEmailSubject`** | Allows for overriding the subject of the email that is sent to users indicating how to validate their account. [More details](#generateemailsubject). | #### generateEmailHTML Function that accepts one argument, containing `{ req, token, user }`, that allows for overriding the HTML within emails that are sent to users indicating how to validate their account. The function should return a string that supports HTML, which can optionally be a full HTML email. ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { verify: { // highlight-start generateEmailHTML: ({ req, token, user }) => { // Use the token provided to allow your user to verify their account const url = `https://yourfrontend.com/verify?token=${token}` return `Hey ${user.email}, verify your email by clicking here: ${url}` }, // highlight-end }, }, } ``` > **Important:** If you specify a different URL to send your users to for email > verification, such as a page on the frontend of your app or similar, you need > to handle making the call to the Payload REST or GraphQL verification > operation yourself on your frontend, using the token that was provided for > you. Above, it was passed via query parameter. #### generateEmailSubject Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function arguments are the same but you can only return a string - not HTML. ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { verify: { // highlight-start generateEmailSubject: ({ req, user }) => { return `Hey ${user.email}, reset your password!` }, // highlight-end }, }, } ``` ## Forgot Password You can customize how the Forgot Password workflow operates with the following options on the `auth.forgotPassword` property: ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { forgotPassword: { // highlight-line // ... }, }, } ``` The following options are available: | Option | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **`expiration`** | Configure how long password reset tokens remain valid, specified in milliseconds. | | **`generateEmailHTML`** | Allows for overriding the HTML within emails that are sent to users attempting to reset their password. [More details](#generateEmailHTML). | | **`generateEmailSubject`** | Allows for overriding the subject of the email that is sent to users attempting to reset their password. [More details](#generateEmailSubject). | | **`minRequestInterval`** | Minimum time between reset emails for the same user, in milliseconds. Defaults to `15000` (15 seconds). Set to `0` to disable. | > **Tip:** Payload provides a built-in password reset page. If you don't need a > custom frontend, you can link directly to `${serverURL}/admin/reset/${token}`. > The admin and reset routes are configurable via `config.routes.admin` and > `config.admin.routes.reset` respectively. #### generateEmailHTML This function allows for overriding the HTML within emails that are sent to users attempting to reset their password. The function should return a string that supports HTML, which can be a full HTML email. ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { forgotPassword: { // highlight-start generateEmailHTML: ({ req, token, user }) => { // Use the token provided to allow your user to reset their password const resetPasswordURL = `https://yourfrontend.com/reset-password?token=${token}` return `

Here is my custom email template!

Hello, ${user.email}!

Click below to reset your password.

${resetPasswordURL}

` }, // highlight-end }, }, } ``` > **Important:** If you specify a different URL to send your users to for > resetting their password, such as a page on the frontend of your app or > similar, you need to handle making the call to the Payload REST or GraphQL > reset-password operation yourself on your frontend, using the token that was > provided for you. Above, it was passed via query parameter. > **Tip:** HTML templating can be used to create custom email templates, inline > CSS automatically, and more. You can make a reusable function that > standardizes all email sent from Payload, which makes sending custom emails > more DRY. Payload doesn't ship with an HTML templating engine, so you are free > to choose your own. The following arguments are passed to the `generateEmailHTML` function: | Argument | Description | | -------- | ----------------------------------------------------------------- | | `req` | The request object. | | `token` | The token that is generated for the user to reset their password. | | `user` | The user document that is attempting to reset their password. | #### generateEmailSubject Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function arguments are the same but you can only return a string - not HTML. ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { forgotPassword: { // highlight-start generateEmailSubject: ({ req, user }) => { return `Hey ${user.email}, reset your password!` }, // highlight-end }, }, } ``` The following arguments are passed to the `generateEmailSubject` function: | Argument | Description | | -------- | ------------------------------------------------------------- | | `req` | The request object. | | `user` | The user document that is attempting to reset their password. | # Cookie Strategy Source: https://payloadcms.com/docs/beta/authentication/cookies Payload offers the ability to [Authenticate](/docs/v4/authentication/overview.md) via HTTP-only cookies. These can be read from the responses of `login`, `logout`, `refresh`, and `me` auth operations. > **Tip:** You can access the logged-in user from within [Access > Control](/docs/v4/access-control/overview.md) and [Hooks](/docs/v4/hooks/overview.md) through > the `req.user` argument. [More details](/docs/v4/authentication/token-data.md). ### Automatic browser inclusion Modern browsers automatically include `http-only` cookies when making requests directly to URLs—meaning that if you are running your API on `https://example.com`, and you have logged in and visit `https://example.com/test-page`, your browser will automatically include the Payload authentication cookie for you. ### HTTP Authentication However, if you use `fetch` or similar APIs to retrieve Payload resources from its REST or GraphQL API, you must specify to include credentials (cookies). Fetch example, including credentials: ```ts const response = await fetch('http://localhost:3000/api/pages', { credentials: 'include', }) const pages = await response.json() ``` For more about including cookies in requests from your app to your Payload API, [read the MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#including_credentials). > **Tip:** To make sure you have a Payload cookie set properly in your browser > after logging in, you can use the browsers Developer Tools > Application > > Cookies > [your-domain-here]. The Developer tools will still show HTTP-only > cookies. ### CSRF Attacks CSRF (cross-site request forgery) attacks are common and dangerous. By using an HTTP-only cookie, Payload removes many XSS vulnerabilities, however, CSRF attacks can still be possible. For example, let's say you have a popular app `https://payload-finances.com` that allows users to manage finances, send and receive money. As Payload is using HTTP-only cookies, that means that browsers automatically will include cookies when sending requests to your domain - **no matter what page created the request**. So, if a user of `https://payload-finances.com` is logged in and is browsing around on the internet, they might stumble onto a page with malicious intent. Let's look at an example: ```ts // malicious-intent.com // makes an authenticated request as on your behalf const maliciousRequest = await fetch(`https://payload-finances.com/api/me`, { credentials: 'include', }).then((res) => await res.json()) ``` In this scenario, if your cookie was still valid, malicious-intent.com would be able to make requests like the one above on your behalf. This is a CSRF attack. ### CSRF Prevention Define domains that you trust and are willing to accept Payload HTTP-only cookie based requests from. Use the `csrf` option on the base Payload Config to do this: ```ts // payload.config.ts import { buildConfig } from 'payload' const config = buildConfig({ serverURL: 'https://my-payload-instance.com', // highlight-start csrf: [ // whitelist of domains to allow cookie auth from 'https://your-frontend-app.com', 'https://your-other-frontend-app.com', // `config.serverURL` is added by default if defined ], // highlight-end collections: [ // collections here ], }) export default config ``` #### Cross domain authentication If your frontend is on a different domain than your Payload API then you will not be able to use HTTP-only cookies for authentication by default as they will be considered third-party cookies by the browser. There are a few strategies to get around this: ##### 1. Use subdomains Cookies can cross subdomains without being considered third party cookies, for example if your API is at api.example.com then you can authenticate from example.com. ##### 2. Configure cookies If option 1 isn't possible, then you can get around this limitation by [configuring your cookies](/docs/v4/authentication/overview.md#config-options) on your authentication collection to achieve the following setup: ``` SameSite: None // allows the cookie to cross domains Secure: true // ensures it's sent over HTTPS only HttpOnly: true // ensures it's not accessible via client side JavaScript ``` Configuration example: ```ts { slug: 'users', auth: { cookies: { sameSite: 'None', secure: true, } }, fields: [ // your auth fields here ] }, ``` If you're configuring [cors](/docs/v4/production/preventing-abuse.md#cross-origin-resource-sharing-cors) in your Payload config, you won't be able to use a wildcard anymore, you'll need to specify the list of allowed domains. > **Good to know:** Setting up `secure: true` will not work if you're developing > on `http://localhost` or any non-https domain. For local development you > should conditionally set this to `false` based on the environment. # JWT Strategy Source: https://payloadcms.com/docs/beta/authentication/jwt Payload offers the ability to [Authenticate](/docs/v4/authentication/overview.md) via JSON Web Tokens (JWT). These can be read from the responses of `login`, `logout`, `refresh`, and `me` auth operations. > **Tip:** You can access the logged-in user from within [Access > Control](/docs/v4/access-control/overview.md) and [Hooks](/docs/v4/hooks/overview.md) through > the `req.user` argument. [More details](/docs/v4/authentication/token-data.md). ### Identifying Users Via The Authorization Header In addition to authenticating via an HTTP-only cookie, you can also identify users via the `Authorization` header on an HTTP request. Example: ```ts const user = await fetch('http://localhost:3000/api/users/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'dev@payloadcms.com', password: 'password', }), }).then((req) => await req.json()) const request = await fetch('http://localhost:3000', { headers: { Authorization: `JWT ${user.token}`, }, }) ``` ### Omitting The Token In some cases you may want to prevent the token from being returned from the auth operations. You can do that by setting `removeTokenFromResponses` to `true` like so: ```ts import type { CollectionConfig } from 'payload' export const UsersWithoutJWTs: CollectionConfig = { slug: 'users-without-jwts', auth: { removeTokenFromResponses: true, // highlight-line }, } ``` ## External JWT Validation When validating Payload-generated JWT tokens in external services, use the processed secret rather than your original secret key: ```ts import crypto from 'node:crypto' const secret = crypto .createHash('sha256') .update(process.env.PAYLOAD_SECRET) .digest('hex') .slice(0, 32) ``` > **Note:** Payload processes your secret using SHA-256 hash and takes the first > 32 characters. This processed value is what's used for JWT operations, not > your original secret. # API Key Strategy Source: https://payloadcms.com/docs/beta/authentication/api-keys To integrate with third-party APIs or services, you might need the ability to generate API keys that can be used to identify as a certain user within Payload. API keys are generated on a user-by-user basis, similar to email and passwords, and are meant to represent a single user. For example, if you have a third-party service or external app that needs to be able to perform protected actions against Payload, first you need to create a user within Payload, i.e. `dev@thirdparty.com`. From your external application you will need to authenticate with that user, you have two options: 1. Log in each time with that user and receive an expiring token to request with. 1. Generate a non-expiring API key for that user to request with. > **Tip:** > > This is particularly useful as you can create a "user" that reflects an integration with a specific external service and assign a "role" or specific access only needed by that service/integration. Technically, both of these options will work for third-party integrations but the second option with API key is simpler, because it reduces the amount of work that your integrations need to do to be authenticated properly. To enable API keys on a collection, set the `useAPIKey` auth option to `true`. The [Admin Panel](/docs/v4/admin/overview.md) then allows users accepted by the admin collection's `access.admin` function to generate, regenerate, or revoke keys. All other users are denied by default. By default, Payload does not return generated API keys from its built-in read operations, including admins. See [Revealing API keys](#revealing-api-keys) for more details. ```ts import type { CollectionConfig } from 'payload' export const ThirdPartyAccess: CollectionConfig = { slug: 'third-party-access', auth: { useAPIKey: true, // highlight-line }, fields: [], } ``` Payload displays a generated API key only once. Copy it when it is generated. Payload omits the key from all reads and displays only a masked preview ending in its final four characters. An API key is independent of the user's password. Changing or resetting a password does not disable an existing API key, so remove or regenerate the key when its access should end. > **Important:** > If you change your `PAYLOAD_SECRET`, you will need to regenerate your API keys. > > The secret key is used to encrypt the API keys, so if you change the secret, existing API keys will > no longer be valid. ### Restricting API key management API key management defaults to users accepted by the configured admin collection. You can further restrict generation, regeneration, and revocation by overriding the generated `apiKey` field's `create` and `update` access. See [Fields Generated by Payload](/docs/v4/fields/default-fields.md#example) for a complete example. ### Revealing API keys By default, API keys are hidden after generation. To allow users who are granted admin access to reveal stored keys, set `auth.useAPIKey.reveal` to `true`: ```ts import type { CollectionConfig } from 'payload' export const Users: CollectionConfig = { slug: 'users', auth: { useAPIKey: { reveal: true, // highlight-line }, }, fields: [], } ``` > **Note** > > Revealing a key also requires read and update access to its document, plus > update access to the generated `apiKey` field. ### HTTP Authentication To authenticate REST or GraphQL API requests using an API key, set the `Authorization` header. The header is case-sensitive and needs the slug of the `auth.useAPIKey` enabled collection, then " API-Key ", followed by the `apiKey` that has been assigned. Payload's built-in middleware will then assign the user document to `req.user` and handle requests with the proper [Access Control](/docs/v4/access-control/overview.md). By doing this, Payload recognizes the request being made as a request by the user associated with that API key. **For example, using Fetch:** ```ts import Users from '../collections/Users' const response = await fetch('http://localhost:3000/api/pages', { headers: { Authorization: `${Users.slug} API-Key ${YOUR_API_KEY}`, }, }) ``` Payload ensures that the same, uniform [Access Control](/docs/v4/access-control/overview.md) is used across all authentication strategies. This enables you to utilize your existing Access Control configurations with both API keys and the standard email/password authentication. This consistency can aid in maintaining granular control over your API keys. ### API Key Only Auth If you want to use API keys as the only authentication method for a collection, you can disable the default local strategy by setting `disableLocalStrategy` to `true` on the collection's `auth` property. Setting it to `false` keeps local authentication enabled. Disabling the local strategy prevents authentication with email and password, leaving API keys as the collection's authentication method. ```ts import type { CollectionConfig } from 'payload' export const ThirdPartyAccess: CollectionConfig = { slug: 'third-party-access', auth: { useAPIKey: true, disableLocalStrategy: true, // highlight-line }, } ``` # Custom Strategies Source: https://payloadcms.com/docs/beta/authentication/custom-strategies > **Warning** > > This is an advanced feature, so only attempt this if you are an experienced > developer. Otherwise, just let Payload's built-in authentication handle user > auth for you. > > **Development Note:** Changes to custom authentication strategies require a full server restart to take effect. Auth strategies are not hot-reloaded during development. Consider using tools like `nodemon` for auto-restart functionality. ### Creating a strategy At the core, a strategy is a way to authenticate a user making a request. As of `3.0` we moved away from [Passport](https://www.passportjs.org) in favor of pulling back the curtain and putting you in full control. A strategy is made up of the following: | Parameter | Description | | --------------------- | ------------------------------------------------------------------------- | | **`name`** \* | The name of your strategy | | **`authenticate`** \* | A function that takes in the parameters below and returns a user or null. | The `authenticate` function is passed the following arguments: | Argument | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------- | | **`canSetHeaders`** \* | Whether or not the strategy is being executed from a context where response headers can be set. Default is `false`. | | **`headers`** \* | The headers on the incoming request. Useful for retrieving identifiable information on a request. | | **`payload`** \* | The Payload class. Useful for authenticating the identifiable information against Payload. | | **`isGraphQL`** | Whether or not the strategy is being executed within the GraphQL endpoint. Default is `false`. | | **`req`** | The request that initiated authentication, when available. | ### Example Strategy At its core a strategy simply takes information from the incoming request and returns a user. This is exactly how Payload's built-in strategies function. The example below sets `disableLocalStrategy` to `true` because the custom strategy replaces local authentication. Set it to `false` or omit it to keep email and password authentication enabled alongside your custom strategy. Your `authenticate` method should return an object containing a Payload user document and any optional headers that you'd like Payload to set for you when we return a response. ```ts import type { CollectionConfig } from 'payload' export const Users: CollectionConfig = { slug: 'users', auth: { disableLocalStrategy: true, // highlight-start strategies: [ { name: 'custom-strategy', authenticate: async ({ payload, headers }) => { const usersQuery = await payload.find({ collection: 'users', overrideAccess: true, where: { code: { equals: headers.get('code'), }, secret: { equals: headers.get('secret'), }, }, }) return { // Send the user with the collection slug back to authenticate, // or send null if no user should be authenticated user: usersQuery.docs[0] ? { collection: 'users', ...usersQuery.docs[0], } : null, // Optionally, you can return headers // that you'd like Payload to set here when // it returns the response responseHeaders: new Headers({ 'some-header': 'my header value', }), } }, }, ], // highlight-end }, fields: [ { name: 'code', type: 'text', index: true, unique: true, }, { name: 'secret', type: 'text', }, ], } ``` # Token Data Source: https://payloadcms.com/docs/beta/authentication/token-data During the lifecycle of a request you will be able to access the data you have configured to be stored in the JWT by accessing `req.user`. The user object is automatically appended to the request for you. ### Defining Token Data You can specify what data gets encoded to the Cookie/JWT-Token by setting `saveToJWT` property on fields within your auth collection. ```ts import type { CollectionConfig } from 'payload' export const Users: CollectionConfig = { slug: 'users', auth: true, fields: [ { // will be stored in the JWT saveToJWT: true, type: 'select', name: 'role', options: ['super-admin', 'user'], }, { // the entire object will be stored in the JWT // tab fields can do the same thing! saveToJWT: true, type: 'group', name: 'group1', fields: [ { type: 'text', name: 'includeField', }, { // will be omitted from the JWT saveToJWT: false, type: 'text', name: 'omitField', }, ], }, { type: 'group', name: 'group2', fields: [ { // will be stored in the JWT // but stored at the top level saveToJWT: true, type: 'text', name: 'includeField', }, { type: 'text', name: 'omitField', }, ], }, ], } ``` > **Tip:** > > If you wish to use a different key other than the field `name`, you can define `saveToJWT` as a string. Decoding a token produces the same flat token data shape used by earlier Payload versions: ```ts { id: 'user-id', collection: 'users', email: 'user@example.com', sid: 'session-id', // when sessions are enabled role: 'super-admin', // fields configured with saveToJWT iat: 1750000000, exp: 1750007200, } ``` Payload reserves `id`, `collection`, `email`, `sid`, `iat`, and `exp`; fields configured with `saveToJWT` cannot replace these values. All other fields selected by `saveToJWT`, including fields that use a custom string key, are stored at the top level with the trusted authentication values. Payload also signs `authVersion: 1` into the token's protected JWT header. This marker identifies tokens issued by the fixed authentication format and is checked during authentication. It is not a decoded payload field, because an existing `saveToJWT` alias could use that name. ### Using Token Data This is especially helpful when writing [Hooks](/docs/v4/hooks/overview.md) and [Access Control](/docs/v4/access-control/overview.md) that depend on user defined fields. ```ts import type { CollectionConfig } from 'payload' export const Invoices: CollectionConfig = { slug: 'invoices', access: { read: ({ req, data }) => { if (!req?.user) return false // highlight-start if ({ req.user?.role === 'super-admin'}) { return true } // highlight-end return data.owner === req.user.id } } fields: [ { name: 'owner', relationTo: 'users' }, // ... other fields ], } ``` # Rotating your PAYLOAD_SECRET Source: https://payloadcms.com/docs/beta/authentication/rotating-secret Your `PAYLOAD_SECRET` is used to encrypt sensitive data at rest (such as API keys), to index API keys for lookup, and to sign authentication tokens. Rotating it — after a leak, or as part of a periodic key-rotation policy — requires re-keying the data that was encrypted under the previous secret. Payload provides the [`rotateSecret`](#rotatesecret) utility and the [`encrypt`/`decrypt`/`reencrypt`](#lower-level-utilities) primitives to do this safely. The [`previousSecrets`](#previoussecrets) config keeps the old secret accepted for reads during the rotation so it can be done with **zero downtime**. ## What a rotation affects > **Success** > > **Password logins keep working.** Passwords are hashed with a random per-user > salt and never involve `PAYLOAD_SECRET`, so a rotation does not affect the > ability to log in with a username/email and password. - **JWT sessions** are signed with the secret. While the old secret is listed in `previousSecrets`, tokens signed under it still verify, so active sessions survive the rotation. Once you remove the old secret, those tokens stop verifying and users log in again. - **Encrypted API keys** are stored as ciphertext plus an `apiKeyIndex` lookup value, both derived from the secret at write time. While the old secret is in `previousSecrets`, keys indexed under it still authenticate; `rotateSecret` re-keys them to the current secret. - **Encrypted values at rest** (API keys and any field you encrypt with `payload.encrypt`) can only be read while a secret that can decrypt them is in the keyring. A value whose secret is no longer present throws on read, so keep the old secret in `previousSecrets` until `rotateSecret` has re-keyed everything. ## Procedure (zero-downtime) 1. Add the previous secret to `previousSecrets` while setting the new secret as the active `secret`, and deploy: ```ts export default buildConfig({ secret: process.env.PAYLOAD_SECRET, // the new secret previousSecrets: [process.env.OLD_PAYLOAD_SECRET], // still accepted for reads // ... }) ``` New data is written under the new secret; existing sessions, API keys, and encrypted values keep working because the old secret is still in the keyring. 2. Create a migration that calls `rotateSecret` to re-key stored data: ```bash payload migrate:create rotate-secret ``` ```ts import type { MigrateUpArgs } from '@payloadcms/db-mongodb' import { rotateSecret } from 'payload' export async function up({ payload }: MigrateUpArgs): Promise { const { migrated, skipped } = await rotateSecret({ payload, oldSecret: process.env.OLD_PAYLOAD_SECRET, }) payload.logger.info( `rotateSecret: migrated ${migrated}, skipped ${skipped}`, ) } ``` 3. Verify with a dry run first — this reads and validates every row against the old and current secrets without writing anything. If `oldSecret` is wrong, it throws before making any changes: ```ts await rotateSecret({ payload, oldSecret: process.env.OLD_PAYLOAD_SECRET, dryRun: true, }) ``` 4. Run the migration for real: ```bash payload migrate ``` 5. Once everything is re-keyed, **remove the old secret from `previousSecrets`** and deploy. Retiring it is what actually contains a leaked secret — do not keep old secrets in the keyring indefinitely. `rotateSecret` is **idempotent** — running it again skips rows that are already on the current secret — and **fail-closed**: a row that matches neither the old nor the current secret aborts the run before that row is written. Rows migrated earlier in an aborted run remain correct, so after fixing the secret you can simply re-run. ## previousSecrets `previousSecrets` is an array of prior `secret` values that Payload still accepts for **reads** — verifying JWTs, matching API keys, and decrypting stored values — during a bounded rotation. New data is always written with the active `secret`. > **Warning** > > A secret in `previousSecrets` is still accepted for auth: a leaked one can > **forge tokens** and act as a user, and still decrypts data written under it. > Run `rotateSecret`, then remove it promptly. ### Expiring a previous secret Payload does not track an expiration for entries in `previousSecrets` — the array is read as-is when the config is loaded. If you want a previous secret to stop being accepted after a set window, gate it in your own config using a date you control, for example an `OLD_PAYLOAD_SECRET_EXPIRATION` environment variable holding the date the secret was put into rotation: ```ts const oneMonth = 1000 * 60 * 60 * 24 * 30 // The date the previous secret was put into rotation, e.g. "2026-08-06". const previousSecretAddedAt = process.env.OLD_PAYLOAD_SECRET_EXPIRATION ? new Date(process.env.OLD_PAYLOAD_SECRET_EXPIRATION).getTime() : 0 // Accept the previous secret only for one month after it was added. const previousSecrets = process.env.OLD_PAYLOAD_SECRET && previousSecretAddedAt > Date.now() - oneMonth ? [process.env.OLD_PAYLOAD_SECRET] : [] export default buildConfig({ secret: process.env.PAYLOAD_SECRET, previousSecrets, // ... }) ``` > **Warning** > > This check runs **once, when the config is loaded** (at startup / build), not > continuously at runtime. A long-running server started while the secret was > still valid keeps it in the keyring until the process restarts — the > expiration only takes effect on the next boot or deploy. Trigger a restart / > redeploy after the window if you need the old secret to actually drop out, and > always run `rotateSecret` before the window closes so no data is left readable > only by the expired secret. ## rotateSecret Re-keys the built-in `apiKey`/`apiKeyIndex` fields for every auth collection configured with [`useAPIKey`](/docs/v4/authentication/api-keys.md). | Option | Description | | ------------- | ----------------------------------------------------------------------------------------------------- | | `payload` | The Payload instance (available as `args.payload` inside a migration). | | `oldSecret` | The previous raw `PAYLOAD_SECRET` that existing data was encrypted under. **Required.** | | `collections` | Optional array of collection slugs to limit the rotation to. Defaults to all `useAPIKey` collections. | | `batchSize` | Number of documents processed per batch. Defaults to `100`. | | `dryRun` | When `true`, verifies every row without writing. Defaults to `false`. | Returns `{ migrated, skipped }` counts. ## Lower-level utilities For data your own project encrypted with `payload.encrypt` (for example a custom encrypted field), re-key it with `payload.reencrypt`, which decrypts with the old secret and re-encrypts with the current one: ```ts const next = payload.reencrypt(storedValue, { oldSecret: process.env.OLD_PAYLOAD_SECRET, }) ``` `payload.encrypt` and `payload.decrypt` also accept an explicit `{ secret }` to override the key, where `secret` is the raw `PAYLOAD_SECRET` value: ```ts const raw = payload.decrypt(storedValue, { secret: process.env.OLD_PAYLOAD_SECRET, }) const next = payload.encrypt(raw) ``` > **Warning** > > When migrating your own encrypted fields, read and write the stored values at > the database layer (`payload.db.*`) rather than through the Local API. Field > hooks that encrypt on write and decrypt on read would otherwise run with the > current secret and corrupt data encrypted under the old one. Unlike [`rotateSecret`](#rotatesecret) — which verifies each row against the stored `apiKeyIndex`, so it can be re-run safely — a hand-written `reencrypt` loop is **not idempotent**, and has no equivalent way to tell an already-migrated value from a wrong `oldSecret`. On a v1 value already re-keyed to the active secret it throws; on a legacy AES-256-CTR value a wrong `oldSecret` does _not_ throw — it decrypts to garbage and silently re-encrypts it. So don't guard by "decrypt under the current secret first" (unreliable for legacy values): track which rows you have migrated, run the loop once, and pass the correct `oldSecret`. ## Encryption format New encrypted values use an authenticated envelope: `v1::::` (AES-256-GCM). The `keyId` is a non-secret fingerprint used to select the right key from the keyring; the auth tag means a value encrypted under an unknown or wrong key **throws** on decrypt rather than returning garbage. Values written by older versions of Payload (AES-256-CTR, no prefix) are still read transparently and are upgraded to the v1 envelope whenever they are re-encrypted (for example by `rotateSecret`). # Rich Text Editor Source: https://payloadcms.com/docs/beta/rich-text/overview Payload's rich text editor powers the [rich text field](/docs/v4/fields/rich-text.md). It is built on top of [Lexical](https://lexical.dev/), Meta's rich text framework, which provides the underlying foundation we extend with Payload-specific features. We aim to offer the best editing experience you can imagine: healthy defaults out of the box, paired with the flexibility to customize every detail — from the “/” menu and toolbars (inline or fixed) to inserting any component or subfield you need. To use the rich text editor, first install it: ```bash pnpm install @payloadcms/richtext-lexical ``` Once you have it installed, you can pass it to your top-level Payload Config as follows: ```ts import { buildConfig } from 'payload' import { lexicalEditor } from '@payloadcms/richtext-lexical' export default buildConfig({ collections: [ // your collections here ], // Pass the Lexical editor to the root config editor: lexicalEditor({}), }) ``` You can also override Lexical settings on a field-by-field basis as follows: ```ts import type { CollectionConfig } from 'payload' import { lexicalEditor } from '@payloadcms/richtext-lexical' export const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'content', type: 'richText', // Pass the Lexical editor here and override base settings as necessary editor: lexicalEditor({}), }, ], } ``` ## Extending the lexical editor with Features Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life. ### Features: The Building Blocks At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed. If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch. ### Integrating New Features To weave in your custom features, utilize the `features` prop when initializing the Lexical Editor. Here's a basic example of how this is done: ```ts import { BlocksFeature, LinkFeature, UploadFeature, lexicalEditor, } from '@payloadcms/richtext-lexical' import { Banner } from '../blocks/Banner' import { CallToAction } from '../blocks/CallToAction' { editor: lexicalEditor({ features: ({ defaultFeatures, rootFeatures }) => [ ...defaultFeatures, LinkFeature({ // Example showing how to customize the built-in fields // of the Link feature fields: ({ defaultFields }) => [ ...defaultFields, { name: 'rel', label: 'Rel Attribute', type: 'select', hasMany: true, options: ['noopener', 'noreferrer', 'nofollow'], admin: { description: 'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.', }, }, ], }), UploadFeature({ collections: { uploads: { // Example showing how to customize the built-in fields // of the Upload feature fields: [ { name: 'caption', type: 'richText', editor: lexicalEditor(), }, ], }, }, }), // This is incredibly powerful. You can reuse your Payload blocks // directly in the Lexical editor as follows: BlocksFeature({ blocks: [Banner, CallToAction], }), ], }) } ``` `features` can be both an array of features, or a function returning an array of features. The function provides the following props: | Prop | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`defaultFeatures`** | This opinionated array contains all "recommended" default features. You can see which features are included in the default features in the table below. | | **`rootFeatures`** | This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty. | ## Official Features You can find more information about the official features in our [official features docs](/docs/v4/rich-text/official-features.md). ## Creating your own, custom Feature You can find more information about creating your own feature in our [building custom feature docs](/docs/v4/rich-text/custom-features.md). ## TypeScript Every single piece of saved data is 100% fully typed within lexical. It provides a type for every single node, which can be imported from `@payloadcms/richtext-lexical` - each type is prefixed with `Serialized`, e.g., `SerializedUploadNode`. If you have [generated types](#automatic-type-generation), each `richText` field is already fully typed - reference it directly (e.g. `Post['richText']`) instead of assembling a union by hand. To type editor state without generated types, use our `TypedEditorState` helper type, which accepts a union of all possible node types as a generic. We don't provide a type that already contains all possible node types because they depend on which features you have enabled in your editor. Here is an example: ```ts import type { SerializedAutoLinkNode, SerializedBlockNode, SerializedHorizontalRuleNode, SerializedLinkNode, SerializedListItemNode, SerializedListNode, SerializedParagraphNode, SerializedQuoteNode, SerializedRelationshipNode, SerializedTextNode, SerializedUploadNode, TypedEditorState, SerializedHeadingNode, } from '@payloadcms/richtext-lexical' const editorState: TypedEditorState< | SerializedAutoLinkNode | SerializedBlockNode | SerializedHorizontalRuleNode | SerializedLinkNode | SerializedListItemNode | SerializedListNode | SerializedParagraphNode | SerializedQuoteNode | SerializedRelationshipNode | SerializedTextNode | SerializedUploadNode | SerializedHeadingNode > = { root: { type: 'root', direction: 'ltr', format: '', indent: 0, version: 1, children: [ { children: [ { detail: 0, format: 0, mode: 'normal', style: '', text: 'Some text. Every property here is fully-typed', type: 'text', version: 1, }, ], direction: 'ltr', format: '', indent: 0, type: 'paragraph', textFormat: 0, version: 1, }, ], }, } ``` Alternatively, you can use the `DefaultTypedEditorState` type, which includes all types for all nodes included in the `defaultFeatures`: ```ts import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical' const editorState: DefaultTypedEditorState = { root: { type: 'root', direction: 'ltr', format: '', indent: 0, version: 1, children: [ { children: [ { detail: 0, format: 0, mode: 'normal', style: '', text: 'Some text. Every property here is fully-typed', type: 'text', version: 1, }, ], direction: 'ltr', format: '', indent: 0, type: 'paragraph', textFormat: 0, version: 1, }, ], }, } ``` Just like `TypedEditorState`, the `DefaultTypedEditorState` also accepts an optional node type union as a generic. Here, this would **add** the specified node types to the default ones. Example: ```ts DefaultTypedEditorState ``` This is a type-safe representation of the editor state. If you look at the auto suggestions of a node's `type` property, you will see all the possible node types you can use. Make sure to only use types exported from `@payloadcms/richtext-lexical`, not from the lexical core packages. We only have control over the types we export and can make sure they're correct, even though the lexical core may export types with identical names. ### Automatic type generation When you [generate types](/docs/v4/typescript/generating-types.md), every `richText` field gets an accurate, fully-typed definition based on the exact features enabled on _that_ editor. Each field's nodes are narrowed to what the editor actually allows - including your custom blocks and the specific collections its relationship and upload nodes can point to. You can reference a field's editor state type straight from your generated types - no manual `TypedEditorState` union, no type assertions: ```ts import type { Post } from './payload-types' // Fully typed and narrowed to this editor's nodes - use it wherever you need the field's type type PostRichText = Post['richText'] ``` This data is also directly assignable to the `SerializedEditorState` that every [converter](/docs/v4/rich-text/converting-html.md) accepts, so you can pass it straight through: ```ts import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html' const html = convertLexicalToHTML({ data: post.richText }) ``` ### Building editor state Use the `buildEditorState` helper to construct editor state JSON from text and/or nodes. Pass your generated field type as the generic and the result is exactly that field's type - with the `nodes` you pass checked against that editor's allowed nodes: ```ts import { buildEditorState } from '@payloadcms/richtext-lexical' import type { Post } from './payload-types' post.richText = buildEditorState({ text: 'Hello world' }) ``` If you don't have a generated type, you can pass a node union instead (e.g. `buildEditorState({ text: 'Hello world' })`). To type an individual node you build by hand (for example a single block, or a `.map()` of nodes), use the `RichTextNodes` helper to pull a field's node union out of its generated type: ```ts import type { RichTextNodes } from '@payloadcms/richtext-lexical' import type { Post } from './payload-types' type PostNode = RichTextNodes ``` ## Admin customization The Rich Text Field editor configuration has an `admin` property with the following options: | Property | Description | | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | | **`placeholder`** | Set this property to define a placeholder string for the field. | | **`hideGutter`** | Set this property to `true` to hide this field's gutter within the Admin Panel. | | **`hideInsertParagraphAtEnd`** | Set this property to `true` to hide the "+" button that appears at the end of the editor. | | **`hideDraggableBlockElement`** | Set this property to `true` to hide the draggable element that appears when you hover a node in the editor. | | **`hideAddBlockButton`** | Set this property to `true` to hide the "+" button that appears when you hover a node in the editor. | ### Disable the gutter You can disable the gutter (the vertical line padding between the editor and the left edge of the screen) by setting the `hideGutter` prop to `true`: ```ts { name: 'richText', type: 'richText', editor: lexicalEditor({ admin: { hideGutter: true }, }), } ``` ### Customize the placeholder You can customize the placeholder (the text that appears in the editor when it's empty) by setting the `placeholder` prop: ```ts { name: 'richText', type: 'richText', editor: lexicalEditor({ admin: { placeholder: 'Type your content here...' }, }), } ``` ## Views Views let you define shared rendering logic for Lexical nodes that works in both the admin panel and JSX converters. You can define multiple named views (like `default`, `preview`, `debug`) and switch between them using the built-in view selector, then pass the same node maps to JSX converters for consistent rendering outside the editor. ```ts import { lexicalEditor } from '@payloadcms/richtext-lexical' export const Posts = { slug: 'posts', fields: [ { name: 'content', type: 'richText', editor: lexicalEditor({ // Import path with #exportName to specify which view map to use views: './views.js#postViews', }), }, ], } ``` Learn more about [Views](/docs/v4/rich-text/views.md). ## Detecting empty editor state When you first type into a rich text field and subsequently delete everything through the admin panel, its value changes from `null` to a JSON object containing an empty paragraph. If needed, you can reset the field value to `null` programmatically - for example, by using a custom hook to detect when the editor is empty. This also applies to fields like `text` and `textArea`, which could be stored as either `null` or an empty value in the database. Since the empty value for richText is a JSON object, checking for emptiness is a bit more involved - so Payload provides a utility for it: ```ts import { hasText } from '@payloadcms/richtext-lexical/shared' hasText(richtextData) ``` # Lexical Converters Source: https://payloadcms.com/docs/beta/rich-text/converters Richtext fields save data in JSON - this is great for storage and flexibility and allows you to easily to convert it to other formats: - [Converting JSX](/docs/v4/rich-text/converting-jsx.md) - [Converting HTML](/docs/v4/rich-text/converting-html.md) - [Converting Plaintext](/docs/v4/rich-text/converting-plaintext.md) - [Converting Markdown and MDX](/docs/v4/rich-text/converting-markdown.md) ## Retrieving the Editor Config Some converters require access to the Lexical editor config, which defines available features and behaviors. Payload provides multiple ways to obtain the editor config through the `editorConfigFactory` from `@payloadcms/richtext-lexical`. ### Importing the Factory First, import the necessary utilities: ```ts import type { SanitizedConfig } from 'payload' import { editorConfigFactory } from '@payloadcms/richtext-lexical' // Your Payload Config needs to be available in order to retrieve the default editor config const config: SanitizedConfig = {} as SanitizedConfig ``` ### Option 1: Default Editor Config If you require the default editor config: ```ts const defaultEditorConfig = editorConfigFactory.default({ config }) ``` ### Option 2: Extract from a Lexical Field When a lexical field config is available, you can extract the editor config directly: ```ts const fieldEditorConfig = editorConfigFactory.fromField({ field: config.collections[0].fields[1], }) ``` ### Option 3: Create a Custom Editor Config You can create a custom editor configuration by specifying additional features: ```ts import { FixedToolbarFeature } from '@payloadcms/richtext-lexical' const customEditorConfig = editorConfigFactory.fromFeatures({ config, features: ({ defaultFeatures }) => [ ...defaultFeatures, FixedToolbarFeature(), ], }) ``` ### Option 4: Extract from an Instantiated Editor If you've created a global or reusable Lexical editor instance, you can access its configuration. This method is typically less efficient and not recommended: ```ts const editor = lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, FixedToolbarFeature(), ], }) const instantiatedEditorConfig = editorConfigFactory.fromEditor({ config, editor, }) ``` For better efficiency, consider extracting the `features` into a separate variable and using `fromFeatures` instead of this method. ### Example - Retrieving the editor config from an existing field If you have access to the sanitized collection config, you can access the lexical sanitized editor config, as every lexical richText field returns it. Here is an example how you can retrieve it from another field's afterRead hook: ```ts import type { CollectionConfig, RichTextField } from 'payload' import { editorConfigFactory, getEnabledNodes, lexicalEditor, } from '@payloadcms/richtext-lexical' export const MyCollection: CollectionConfig = { slug: 'slug', fields: [ { name: 'text', type: 'text', hooks: { afterRead: [ ({ siblingFields, value }) => { const field: RichTextField = siblingFields.find( (field) => 'name' in field && field.name === 'richText', ) as RichTextField const editorConfig = editorConfigFactory.fromField({ field, }) // Now you can use the editor config return value }, ], }, }, { name: 'richText', type: 'richText', editor: lexicalEditor(), }, ], } ``` # Converting JSX Source: https://payloadcms.com/docs/beta/rich-text/converting-jsx ## Richtext to JSX To convert richtext to JSX, import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the richtext content to it: ```tsx import React from 'react' import { RichText } from '@payloadcms/richtext-lexical/react' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' export const MyComponent = ({ data }: { data: SerializedEditorState }) => { return } ``` The `RichText` component includes built-in converters for common Lexical nodes. You can add or override converters via the `converters` prop for custom blocks, custom nodes, or any modifications you need. See the [website template](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) for a working example. If you want to share the same node rendering logic between the admin panel and your frontend, consider using [Views](/docs/v4/rich-text/views.md) instead. Views let you define a single node map that works in both contexts. > **Default** > > When fetching data, ensure your `depth` setting is high enough to fully > populate Lexical nodes such as uploads. The JSX converter requires fully > populated data to work correctly. ### Internal Links By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links. To fix this, you need to pass the `internalDocToHref` prop to `LinkJSXConverter`. This prop is a function that receives the link node and returns the URL of the document. ```tsx import type { DefaultNodeTypes, SerializedLinkNode, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { type JSXConvertersFunction, LinkJSXConverter, RichText, } from '@payloadcms/richtext-lexical/react' import React from 'react' const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => { const { relationTo, value } = linkNode.fields.doc! if (typeof value !== 'object') { throw new Error('Expected value to be an object') } const slug = value.slug switch (relationTo) { case 'posts': return `/posts/${slug}` case 'categories': return `/category/${slug}` case 'pages': return `/${slug}` default: return `/${relationTo}/${slug}` } } const jsxConverters: JSXConvertersFunction = ({ defaultConverters, }) => ({ ...defaultConverters, ...LinkJSXConverter({ internalDocToHref }), }) export const MyComponent: React.FC<{ lexicalData: SerializedEditorState }> = ({ lexicalData }) => { return } ``` ### Lexical Blocks If your rich text includes custom Blocks or Inline Blocks, you must supply custom converters that match each block's slug. This converter is not included by default, as Payload doesn't know how to render your custom blocks. For example: ```tsx 'use client' import type { MyInlineBlock, MyNumberBlock, MyTextBlock } from '@/payload-types' import type { SerializedBlockNode, SerializedInlineBlockNode, WithDefaultNodes, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { type JSXConvertersFunction, RichText, } from '@payloadcms/richtext-lexical/react' import React from 'react' // The default nodes plus your custom blocks for full type safety type NodeTypes = WithDefaultNodes< | SerializedBlockNode | SerializedInlineBlockNode > const jsxConverters: JSXConvertersFunction = ({ defaultConverters, }) => ({ ...defaultConverters, blocks: { // Each key should match your block's slug myNumberBlock: ({ node }) =>
{node.fields.number}
, myTextBlock: ({ node }) => (
{node.fields.text}
), }, inlineBlocks: { // Each key should match your inline block's slug myInlineBlock: ({ node }) => {node.fields.text}, }, }) export const MyComponent: React.FC<{ lexicalData: SerializedEditorState }> = ({ lexicalData }) => { return } ``` ### Overriding Converters You can override any of the default JSX converters by passing your custom converter, keyed to the node type, to the `converters` prop / the converters function. Example - overriding the upload node converter to use next/image: ```tsx 'use client' import type { DefaultNodeTypes, SerializedUploadNode, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { type JSXConvertersFunction, RichText, } from '@payloadcms/richtext-lexical/react' import Image from 'next/image' import React from 'react' type NodeTypes = DefaultNodeTypes // Custom upload converter component that uses next/image const CustomUploadComponent: React.FC<{ node: SerializedUploadNode }> = ({ node }) => { if (node.relationTo === 'uploads') { const uploadDoc = node.value if (typeof uploadDoc !== 'object') { return null } const { alt, height, url, width } = uploadDoc return {alt} } return null } const jsxConverters: JSXConvertersFunction = ({ defaultConverters, }) => ({ ...defaultConverters, // Override the default upload converter upload: ({ node }) => { return }, }) export const MyComponent: React.FC<{ lexicalData: SerializedEditorState }> = ({ lexicalData }) => { return } ``` # Converting HTML Source: https://payloadcms.com/docs/beta/rich-text/converting-html ## Rich Text to HTML There are two main approaches to convert your Lexical-based rich text to HTML: 1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand. 2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API. ### On-demand To convert JSON to HTML on-demand, use the `convertLexicalToHTML` function from `@payloadcms/richtext-lexical/html`. Here's an example of how to use it in a React component in your frontend: ```tsx 'use client' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html' import React from 'react' export const MyComponent = ({ data }: { data: SerializedEditorState }) => { const html = convertLexicalToHTML({ data }) return
} ``` #### Dynamic Population (Advanced) By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function: ```tsx 'use client' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client' import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async' import React, { useEffect, useState } from 'react' export const MyComponent = ({ data }: { data: SerializedEditorState }) => { const [html, setHTML] = useState(null) useEffect(() => { async function convert() { const html = await convertLexicalToHTMLAsync({ data, populate: getRestPopulateFn({ apiURL: `http://localhost:3000/api`, }), }) setHTML(html) } void convert() }, [data]) return html &&
} ``` Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function: ```tsx import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical' import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async' import { getPayload } from 'payload' import React from 'react' import config from '../../config.js' export const MyRSCComponent = async ({ data, }: { data: SerializedEditorState }) => { const payload = await getPayload({ config, }) const html = await convertLexicalToHTMLAsync({ data, populate: await getPayloadPopulateFn({ currentDepth: 0, depth: 1, payload, }), }) return html &&
} ``` ### HTML field The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended, as it creates a column with duplicate content in another format. Consider using the [on-demand HTML converter above](/docs/v4/rich-text/converting-html.md#on-demand) or the [JSX converter](/docs/v4/rich-text/converting-jsx.md) unless you have a good reason. ```ts import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html' import type { MyTextBlock } from '@/payload-types.js' import type { CollectionConfig } from 'payload' import { BlocksFeature, lexicalEditor, lexicalHTMLField, type SerializedBlockNode, type WithDefaultNodes, } from '@payloadcms/richtext-lexical' const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'nameOfYourRichTextField', type: 'richText', editor: lexicalEditor(), }, lexicalHTMLField({ htmlFieldName: 'nameOfYourRichTextField_html', lexicalFieldName: 'nameOfYourRichTextField', }), { name: 'customRichText', type: 'richText', editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, BlocksFeature({ blocks: [ { interfaceName: 'MyTextBlock', slug: 'myTextBlock', fields: [ { name: 'text', type: 'text', }, ], }, ], }), ], }), }, lexicalHTMLField({ htmlFieldName: 'customRichText_html', lexicalFieldName: 'customRichText', // can pass in additional converters or override default ones converters: (({ defaultConverters }) => ({ ...defaultConverters, blocks: { myTextBlock: ({ node, providedCSSString }) => `
${node.fields.text}
`, }, })) as HTMLConvertersFunction< WithDefaultNodes> >, }), ], } ``` ## Blocks to HTML If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example: ```tsx 'use client' import type { MyInlineBlock, MyTextBlock } from '@/payload-types' import type { SerializedBlockNode, SerializedInlineBlockNode, WithDefaultNodes, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { convertLexicalToHTML, type HTMLConvertersFunction, } from '@payloadcms/richtext-lexical/html' import React from 'react' type NodeTypes = WithDefaultNodes< SerializedBlockNode | SerializedInlineBlockNode > const htmlConverters: HTMLConvertersFunction = ({ defaultConverters, }) => ({ ...defaultConverters, blocks: { // Each key should match your block's slug myTextBlock: ({ node, providedCSSString }) => `
${node.fields.text}
`, }, inlineBlocks: { // Each key should match your inline block's slug myInlineBlock: ({ node, providedStyleTag }) => `${node.fields.text}`, }, }) export const MyComponent = ({ data }: { data: SerializedEditorState }) => { const html = convertLexicalToHTML({ converters: htmlConverters, data, }) return
} ``` ## HTML to Richtext If you need to convert raw HTML into a Lexical editor state, use `convertHTMLToLexical` from `@payloadcms/richtext-lexical`, along with the [editorConfigFactory to retrieve the editor config](/docs/v4/rich-text/converters.md#retrieving-the-editor-config): > **Important:** When converting HTML to Lexical, `` tags are NOT automatically uploaded. This is intentional because we don't know which uploads-enabled collection to add them to, and that collection may have required fields that cannot be auto-filled. Images will be omitted from the conversion unless you provide the proper data attributes. See [Converting HTML with Images](#converting-html-with-images) for guidance on handling images. ```ts import { convertHTMLToLexical, editorConfigFactory, } from '@payloadcms/richtext-lexical' // Make sure you have jsdom and @types/jsdom installed import { JSDOM } from 'jsdom' const lexicalJSON = convertHTMLToLexical({ editorConfig: editorConfigFactory.default({ config, // Your Payload Config }), html: '

text

', JSDOM, // Pass in the JSDOM import; it's not bundled to keep package size small }) ``` ## Converting HTML with Images When converting HTML to Lexical, `` tags require special handling because Payload does not automatically upload images to prevent unintended database modifications. Here are three approaches to handle images during HTML-to-Lexical conversion: ### Approach 1: Pre-upload Images and Use Data Attributes (Recommended) The most reliable approach is to upload images to Payload first, then reference them in your HTML using special data attributes before conversion. ```ts import { convertHTMLToLexical, editorConfigFactory, } from '@payloadcms/richtext-lexical' import { getPayload } from 'payload' import { JSDOM } from 'jsdom' import config from '@payload-config' const payload = await getPayload({ config }) // Step 1: Upload the image to Payload const uploadedMedia = await payload.create({ collection: 'media', // Your upload collection slug overrideAccess: true, data: { alt: 'My image description', }, filePath: '/path/to/local/image.jpg', // or file: fileData for file uploads }) // Step 2: Construct HTML with the proper data attributes const htmlWithImage = `

Some text content

My image description

More content

` // Step 3: Convert to Lexical const lexicalJSON = convertHTMLToLexical({ editorConfig: editorConfigFactory.default({ config }), html: htmlWithImage, JSDOM, }) // The lexicalJSON will now contain a proper upload node ``` **Required data attributes:** - `data-lexical-upload-id`: The ID of the uploaded document - `data-lexical-upload-relation-to`: The collection slug (e.g., 'media') ### Approach 2: Parse and Upload Images Before Conversion For bulk content migration or when dealing with external image URLs, parse the HTML first, upload images, then replace the image tags with proper attributes. ```ts import { convertHTMLToLexical, editorConfigFactory, } from '@payloadcms/richtext-lexical' import { getPayload } from 'payload' import { JSDOM } from 'jsdom' import config from '@payload-config' async function convertHTMLWithImageUpload(htmlString: string) { const payload = await getPayload({ config }) // Step 1: Parse HTML to find all images const dom = new JSDOM(htmlString) const images = dom.window.document.querySelectorAll('img') // Step 2: Upload each image and update the DOM for (const img of Array.from(images)) { const imageUrl = img.getAttribute('src') if (!imageUrl) continue try { // Download the image (if external URL) const response = await fetch(imageUrl) const arrayBuffer = await response.arrayBuffer() const buffer = Buffer.from(arrayBuffer) // Upload to Payload const uploadedMedia = await payload.create({ collection: 'media', overrideAccess: true, data: { alt: img.getAttribute('alt') || '', }, file: { data: buffer, mimetype: response.headers.get('content-type') || 'image/jpeg', name: imageUrl.split('/').pop() || 'image.jpg', size: buffer.length, }, }) // Update the img tag with data attributes img.setAttribute('data-lexical-upload-id', String(uploadedMedia.id)) img.setAttribute('data-lexical-upload-relation-to', 'media') img.setAttribute('src', uploadedMedia.url) } catch (error) { console.error(`Failed to upload image: ${imageUrl}`, error) // Optionally remove the img tag if upload fails img.remove() } } // Step 3: Convert the updated HTML to Lexical const updatedHTML = dom.window.document.body.innerHTML return convertHTMLToLexical({ editorConfig: editorConfigFactory.default({ config }), html: updatedHTML, JSDOM, }) } // Usage const lexicalJSON = await convertHTMLWithImageUpload( '

Text

', ) ``` ### Approach 3: Construct Upload Nodes with buildEditorState If you already have upload IDs and want to build the Lexical JSON structure, use the `buildEditorState` helper for a type-safe, simplified approach. This helper requires less boilerplate and eliminates the need to manually construct the root node. ```ts import { buildEditorState } from '@payloadcms/richtext-lexical' import { v4 as uuid } from 'uuid' // Build Lexical JSON with upload nodes using the helper const lexicalJSON = buildEditorState({ text: 'Some text content', nodes: [ { type: 'upload', format: '', version: 3, relationTo: 'media', value: 'your-upload-id-here', // ID of the uploaded document fields: {}, // Any additional fields configured for the upload feature id: uuid(), // Unique ID for this node instance (not the upload document ID) }, ], }) // Save to your collection await payload.create({ collection: 'pages', overrideAccess: true, data: { title: 'My Page', content: lexicalJSON, }, }) ``` ### Troubleshooting **Images disappear during conversion:** - Ensure images have both `data-lexical-upload-id` and `data-lexical-upload-relation-to` attributes - Verify the upload ID exists in your upload collection - Check that the `relationTo` value matches your upload collection slug **"Cannot read property 'id' of undefined" errors:** - The upload document may not exist - verify the ID is correct - Ensure the upload collection is properly configured - Check that the referenced upload hasn't been deleted **Images work in the admin UI but not in conversion:** - The admin UI handles pending uploads differently than server-side conversion - Server-side conversion requires existing upload IDs, not pending uploads - Always upload images before converting HTML > **Tip:** For large-scale content migrations, consider creating a migration > script that processes HTML in batches, uploads images in parallel with rate > limiting, and handles errors gracefully. # Converting Markdown Source: https://payloadcms.com/docs/beta/rich-text/converting-markdown ## Editor adapter converters Lexical provides `converters.fromMarkdown` and `converters.toMarkdown` using the editor's configured features: ```ts const converters = payload.config.editor?.converters if (converters?.fromMarkdown && converters.toMarkdown) { const data = converters.fromMarkdown({ markdown: '## Hello world\n\nThis is **bold** text.', }) const markdown = converters.toMarkdown({ data }) } ``` For a field with its own editor, use the converters on that field's initialized `editor`. ## Richtext to Markdown If you have access to the Payload Config and the [lexical editor config](/docs/v4/rich-text/converters.md#retrieving-the-editor-config), you can convert the lexical editor state to Markdown with the following: ```ts import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { convertLexicalToMarkdown, editorConfigFactory, } from '@payloadcms/richtext-lexical' // Your richtext data here const data: SerializedEditorState = {} const markdown = convertLexicalToMarkdown({ data, editorConfig: editorConfigFactory.default({ config, // <= make sure you have access to your Payload Config }), }) ``` ### Example - outputting Markdown from the Collection ```ts import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import type { CollectionConfig, RichTextField } from 'payload' import { convertLexicalToMarkdown, editorConfigFactory, lexicalEditor, } from '@payloadcms/richtext-lexical' const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'nameOfYourRichTextField', type: 'richText', editor: lexicalEditor(), }, { name: 'markdown', type: 'textarea', admin: { hidden: true, }, hooks: { afterRead: [ ({ siblingData, siblingFields }) => { const data: SerializedEditorState = siblingData['nameOfYourRichTextField'] if (!data) { return '' } const markdown = convertLexicalToMarkdown({ data, editorConfig: editorConfigFactory.fromField({ field: siblingFields.find( (field) => 'name' in field && field.name === 'nameOfYourRichTextField', ) as RichTextField, }), }) return markdown }, ], beforeChange: [ ({ siblingData }) => { // Ensure that the markdown field is not saved in the database delete siblingData['markdown'] return null }, ], }, }, ], } ``` ## Markdown to Richtext If you have access to the Payload Config and the [lexical editor config](/docs/v4/rich-text/converters.md#retrieving-the-editor-config), you can convert Markdown to the lexical editor state with the following: ```ts import { convertMarkdownToLexical, editorConfigFactory, } from '@payloadcms/richtext-lexical' const lexicalJSON = convertMarkdownToLexical({ editorConfig: editorConfigFactory.default({ config, // <= make sure you have access to your Payload Config }), markdown: '# Hello world\n\nThis is a **test**.', }) ``` ## Converting Uploads When converting Markdown to Lexical, standard image syntax `![alt] (url)` is **not** automatically converted to Upload nodes, because Payload has no way to look up a Media document from a URL alone. The `UploadFeature` includes a built-in transformer that recognizes a special placeholder format: ```ts ![media:6507f7b9a4d3c2e1f0ab1234]() ``` Where `media` is your upload collection slug and the second part is the document ID. When `convertMarkdownToLexical` processes this, it creates a proper Upload node with the correct `relationTo` and `value`. When converting **from** Lexical to Markdown, Upload nodes are serialized back into this same placeholder format (unless the document is populated, in which case the URL and alt text are used directly as a standard markdown image). ### Migrating existing Markdown content If you're migrating content that contains standard `![alt] (url)` image references, the recommended approach is: 1. Upload your images to the Media collection first to get their document IDs 2. Pre-process your Markdown to replace `![alt] (url)` with `![media:]()` 3. Then run `convertMarkdownToLexical` ## Converting MDX Payload supports serializing and deserializing MDX content. While Markdown converters are stored on the features, MDX converters are stored on the blocks that you pass to the `BlocksFeature`. ### Defining a Custom Block Here is an example of a `Banner` block. This block: - Renders in the admin UI as a normal Lexical block with specific fields (e.g. type, content). - Converts to an MDX `Banner` component. - Can parse that MDX `Banner` back into a Lexical state. ![Shows the Banner field in a lexical editor and the MDX output](https://payloadcms.com/images/docs/mdx-example-light.png) _Banner field in a lexical editor and the MDX output_ ```ts import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import type { Block, CollectionConfig, RichTextField } from 'payload' import { BlocksFeature, convertLexicalToMarkdown, editorConfigFactory, lexicalEditor, } from '@payloadcms/richtext-lexical' const BannerBlock: Block = { slug: 'Banner', fields: [ { name: 'type', type: 'select', defaultValue: 'info', options: [ { label: 'Info', value: 'info' }, { label: 'Warning', value: 'warning' }, { label: 'Error', value: 'error' }, ], }, { name: 'content', type: 'richText', editor: lexicalEditor(), }, ], jsx: { /** * Convert from Lexical -> MDX: * child content */ export: ({ fields, lexicalToMarkdown }) => { const props: any = {} if (fields.type) { props.type = fields.type } return { children: lexicalToMarkdown({ editorState: fields.content }), props, } }, /** * Convert from MDX -> Lexical: */ import: ({ children, markdownToLexical, props }) => { return { type: props?.type, content: markdownToLexical({ markdown: children }), } }, }, } const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'nameOfYourRichTextField', type: 'richText', editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, BlocksFeature({ blocks: [BannerBlock], }), ], }), }, { name: 'markdown', type: 'textarea', hooks: { afterRead: [ ({ siblingData, siblingFields }) => { const data: SerializedEditorState = siblingData['nameOfYourRichTextField'] if (!data) { return '' } const markdown = convertLexicalToMarkdown({ data, editorConfig: editorConfigFactory.fromField({ field: siblingFields.find( (field) => 'name' in field && field.name === 'nameOfYourRichTextField', ) as RichTextField, }), }) return markdown }, ], beforeChange: [ ({ siblingData }) => { // Ensure that the markdown field is not saved in the database delete siblingData['markdown'] return null }, ], }, }, ], } ``` The conversion is done using the `jsx` property of the block. The `export` function is called when converting from lexical to MDX, and the `import` function is called when converting from MDX to lexical. ### Export The `export` function takes the block field data and the `lexicalToMarkdown` function as arguments. It returns the following object: | Property | Type | Description | | ---------- | ------ | ------------------------------------------------------------------ | | `children` | string | This will be in between the opening and closing tags of the block. | | `props` | object | This will be in the opening tag of the block. | ### Import The `import` function provides data extracted from the MDX. It takes the following arguments: | Argument | Type | Description | | ---------- | ------ | ------------------------------------------------------------------------------------ | | `children` | string | This will be the text between the opening and closing tags of the block. | | `props` | object | These are the props passed to the block, parsed from the opening tag into an object. | The returning object is equal to the block field data. # Converting Plaintext Source: https://payloadcms.com/docs/beta/rich-text/converting-plaintext ## Richtext to Plaintext Here's how you can convert richtext data to plaintext using `@payloadcms/richtext-lexical/plaintext`. ```ts import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { convertLexicalToPlaintext } from '@payloadcms/richtext-lexical/plaintext' // Your richtext data here const data: SerializedEditorState = {} const plaintext = convertLexicalToPlaintext({ data }) ``` ### Custom Converters The `convertLexicalToPlaintext` functions accepts a `converters` object that allows you to customize how specific nodes are converted to plaintext. ```ts import type { SerializedBlockNode, WithDefaultNodes, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import type { MyTextBlock } from '@/payload-types' import { convertLexicalToPlaintext, type PlaintextConverters, } from '@payloadcms/richtext-lexical/plaintext' // Your richtext data here const data: SerializedEditorState = {} const converters: PlaintextConverters< WithDefaultNodes> > = { blocks: { textBlock: ({ node }) => { return node.fields.text ?? '' }, }, link: ({ node }) => { return node.fields.url ?? '' }, } const plaintext = convertLexicalToPlaintext({ converters, data, }) ``` Unlike other converters, there are no default converters for plaintext. If a node does not have a converter defined, the following heuristics are used to convert it to plaintext: - If the node has a `text` field, it will be used as the plaintext. - If the node has a `children` field, the children will be recursively converted to plaintext. - If the node has neither, it will be ignored. - Paragraph, text and tab nodes insert newline / tab characters. # Official Features Source: https://payloadcms.com/docs/beta/rich-text/official-features Below are all the Rich Text Features Payload offers. Everything is customizable; you can [create your own features](/docs/v4/rich-text/custom-features.md), modify ours and share them with the community. ## Features Overview | Feature Name | Included by default | Description | | --------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`BoldFeature`** | Yes | Adds support for bold text formatting. | | **`ItalicFeature`** | Yes | Adds support for italic text formatting. | | **`UnderlineFeature`** | Yes | Adds support for underlined text formatting. | | **`StrikethroughFeature`** | Yes | Adds support for strikethrough text formatting. | | **`SubscriptFeature`** | Yes | Adds support for subscript text formatting. | | **`SuperscriptFeature`** | Yes | Adds support for superscript text formatting. | | **`InlineCodeFeature`** | Yes | Adds support for inline code formatting. | | **`ParagraphFeature`** | Yes | Provides entries in both the slash menu and toolbar dropdown for explicit paragraph creation or conversion. | | **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) | | **`AlignFeature`** | Yes | Adds support for text alignment (left, center, right, justify) | | **`IndentFeature`** | Yes | Adds support for text indentation with toolbar buttons | | **`UnorderedListFeature`** | Yes | Adds support for unordered lists (ul) | | **`OrderedListFeature`** | Yes | Adds support for ordered lists (ol) | | **`ChecklistFeature`** | Yes | Adds support for interactive checklists | | **`LinkFeature`** | Yes | Allows you to create internal and external links | | **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents | | **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes | | **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images | | **`HorizontalRuleFeature`** | Yes | Adds support for horizontal rules / separators. Basically displays an `
` element | | **`InlineToolbarFeature`** | Yes | Provides a floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text | | **`FixedToolbarFeature`** | No | Provides a persistent toolbar pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. | | **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](/docs/v4/fields/blocks.md) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. | | **`TreeViewFeature`** | No | Provides a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging | | **`TableFeature`** | No | Adds support for tables, with a toolbar button and slash menu entry for creation and editing. | | **`TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. | ## In depth ### BoldFeature - Description: Adds support for bold text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Markdown Support: `**bold**` or `__bold__` - Keyboard Shortcut: Ctrl/Cmd + B ### ItalicFeature - Description: Adds support for italic text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Markdown Support: `*italic*` or `_italic_` - Keyboard Shortcut: Ctrl/Cmd + I ### UnderlineFeature - Description: Adds support for underlined text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Keyboard Shortcut: Ctrl/Cmd + U ### StrikethroughFeature - Description: Adds support for strikethrough text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Markdown Support: `~~strikethrough~~` ### SubscriptFeature - Description: Adds support for subscript text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes ### SuperscriptFeature - Description: Adds support for superscript text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes ### InlineCodeFeature - Description: Adds support for inline code formatting with distinct styling, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Markdown Support: \`code\` ### ParagraphFeature - Description: Provides entries in both the slash menu and toolbar dropdown for explicit paragraph creation or conversion. - Included by default: Yes ### HeadingFeature - Description: Adds support for heading nodes (H1-H6) with toolbar dropdown and slash menu entries for each enabled heading size. - Included by default: Yes - Markdown Support: `#`, `##`, `###`, ..., at start of line. - Types: ```ts type HeadingFeatureProps = { enabledHeadingSizes?: HeadingTagType[] // ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } ``` - Usage example: ```ts HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3'], // Default: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] }) ``` ### AlignFeature - Description: Allows text alignment (left, center, right, justify), along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Keyboard Shortcut: Ctrl/Cmd + Shift + L/E/R/J (left/center/right/justify) ### IndentFeature - Description: Adds support for text indentation, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Keyboard Shortcut: Tab (increase), Shift + Tab (decrease) - Types: ```ts type IndentFeatureProps = { /** * The nodes that should not be indented. "type" * property of the nodes you don't want to be indented. * These can be: "paragraph", "heading", "listitem", * "quote" or other indentable nodes if they exist. */ disabledNodes?: string[] /** * If true, pressing Tab in the middle of a block such * as a paragraph or heading will not insert a tabNode. * Instead, Tab will only be used for block-level indentation. * @default false */ disableTabNode?: boolean } ``` - Usage example: ```ts // Allow block-level indentation only IndentFeature({ disableTabNode: true, }) ``` ### UnorderedListFeature - Description: Adds support for unordered lists (bullet points) with toolbar dropdown and slash menu entries. - Included by default: Yes - Markdown Support: `-`, `*`, or `+` at start of line ### OrderedListFeature - Description: Adds support for ordered lists (numbered lists) with toolbar dropdown and slash menu entries. - Included by default: Yes - Markdown Support: `1.` at start of line ### ChecklistFeature - Description: Adds support for interactive checklists with toolbar dropdown and slash menu entries. - Included by default: Yes - Markdown Support: `- [ ]` (unchecked) or `- [x]` (checked) ### LinkFeature - Description: Allows creation of internal and external links with toolbar buttons and automatic URL conversion. - Included by default: Yes - Markdown Support: `[anchor](url)` - Types: ```ts type LinkFeatureServerProps = { /** * Disables the automatic creation of links * from URLs typed or pasted into the editor, * @default false */ disableAutoLinks?: 'creationOnly' | true /** * A function or array defining additional * fields for the link feature. * These will be displayed in the link editor drawer. */ fields?: | ((args: { config: SanitizedConfig defaultFields: FieldAffectingData[] }) => (Field | FieldAffectingData)[]) | Field[] /** * Sets a maximum population depth for the internal * doc default field of link, regardless of the * remaining depth when the field is reached. */ maxDepth?: number } & ExclusiveLinkCollectionsProps type ExclusiveLinkCollectionsProps = | { disabledCollections?: CollectionSlug[] enabledCollections?: never } | { disabledCollections?: never enabledCollections?: CollectionSlug[] } ``` - Usage example: ```ts LinkFeature({ fields: ({ defaultFields }) => [ ...defaultFields, { name: 'rel', type: 'select', options: ['noopener', 'noreferrer', 'nofollow'], }, ], enabledCollections: ['pages', 'posts'], // Collections for internal links maxDepth: 2, // Population depth for internal links disableAutoLinks: false, // Allow auto-conversion of URLs }) ``` ### RelationshipFeature - Description: Allows creation of block-level relationships to other documents with toolbar button and slash menu entry. - Included by default: Yes - Types: ```ts type RelationshipFeatureProps = { /** * Sets a maximum population depth for this relationship, * regardless of the remaining depth when the respective * field is reached. */ maxDepth?: number } & ExclusiveRelationshipFeatureProps type ExclusiveRelationshipFeatureProps = | { disabledCollections?: CollectionSlug[] enabledCollections?: never } | { disabledCollections?: never enabledCollections?: CollectionSlug[] } ``` - Usage example: ```ts RelationshipFeature({ disabledCollections: ['users'], // Collections to exclude maxDepth: 2, // Population depth for relationships }) ``` ### UploadFeature - Description: Allows creation of upload/media nodes with toolbar button and slash menu entry, supports all file types. - Included by default: Yes - Types: ```ts type UploadFeatureProps = { collections?: { [collection: UploadCollectionSlug]: { fields: Field[] } } /** * Sets a maximum population depth for this upload (not the fields for this upload), regardless of the remaining depth when the respective field is reached. * This behaves exactly like the maxDepth properties of relationship and upload fields. * * {@link ../getting-started/concepts#field-level-max-depth} */ maxDepth?: number } & ExclusiveUploadFeatureProps type ExclusiveUploadFeatureProps = | { /** * The collections that should be disabled. Overrides the `enableRichTextRelationship` property in the collection config. * When this property is set, `enabledCollections` will not be available. **/ disabledCollections?: UploadCollectionSlug[] // Ensures that enabledCollections is not available when disabledCollections is set enabledCollections?: never } | { // Ensures that disabledCollections is not available when enabledCollections is set disabledCollections?: never /** * The collections that should be enabled. Overrides the `enableRichTextRelationship` property in the collection config * When this property is set, `disabledCollections` will not be available. **/ enabledCollections?: UploadCollectionSlug[] } ``` - Usage example: ```ts UploadFeature({ collections: { uploads: { fields: [ { name: 'caption', type: 'text', label: 'Caption', }, { name: 'alt', type: 'text', label: 'Alt Text', }, ], }, }, maxDepth: 1, // Population depth for uploads disabledCollections: ['specialUploads'], // Collections to exclude }) ``` ### BlockquoteFeature - Description: Allows creation of blockquotes with toolbar button and slash menu entry. - Included by default: Yes - Markdown Support: `> quote text` ### HorizontalRuleFeature - Description: Adds support for horizontal rules/separators with toolbar button and slash menu entry. - Included by default: Yes - Markdown Support: `---` ### InlineToolbarFeature - Description: Provides a floating toolbar that appears when text is selected, containing formatting options relevant to selected text. - Included by default: Yes ### FixedToolbarFeature - Description: Provides a persistent toolbar pinned to the top of the editor that's always visible. - Included by default: No - Types: ```ts type FixedToolbarFeatureProps = { /** * @default false * If this is enabled, the toolbar will apply * to the focused editor, not the editor with * the FixedToolbarFeature. */ applyToFocusedEditor?: boolean /** * Custom configurations for toolbar groups * Key is the group key (e.g. 'format', 'indent', 'align') * Value is a partial ToolbarGroup object that will * be merged with the default configuration */ customGroups?: CustomGroups /** * @default false * If there is a parent editor with a fixed toolbar, * this will disable the toolbar for this editor. */ disableIfParentHasFixedToolbar?: boolean } ``` - Usage example: ```ts FixedToolbarFeature({ applyToFocusedEditor: false, // Apply to focused editor customGroups: { format: { // Custom configuration for format group }, }, }) ``` ### BlocksFeature - Description: Allows use of Payload's [Blocks Field](/docs/v4/fields/blocks.md) directly in the editor with toolbar buttons and slash menu entries for each block type. Supports both block-level and inline blocks. - Included by default: No For complete documentation including custom block components, the pre-built CodeBlock, and rendering blocks on the frontend, see the dedicated [Blocks documentation](/docs/v4/rich-text/blocks.md). ### TreeViewFeature - Description: Provides a debug panel below the editor showing the editor's internal state, DOM tree, and time travel debugging. - Included by default: No ### TableFeature - Description: Adds support for tables with toolbar button and slash menu entry for creation and editing. - Included by default: No ### TextStateFeature - Description: Allows storing key-value attributes in text nodes with inline styles and toolbar dropdown for style selection. - Included by default: No - Types: ```ts type TextStateFeatureProps = { /** * The keys of the top-level object (stateKeys) represent the attributes that the textNode can have (e.g., color). * The values of the top-level object (stateValues) represent the values that the attribute can have (e.g., red, blue, etc.). * Within the stateValue, you can define inline styles and labels. */ state: { [stateKey: string]: StateValues } } type StateValues = { [stateValue: string]: { css: StyleObject label: string } } type StyleObject = { [K in keyof PropertiesHyphenFallback]?: Extract | undefined } ``` - Usage example: ```ts // We offer default colors that have good contrast and look good in dark and light mode. import { defaultColors, TextStateFeature } from '@payloadcms/richtext-lexical' TextStateFeature({ // prettier-ignore state: { color: { ...defaultColors, // fancy gradients! galaxy: { label: 'Galaxy', css: { background: 'linear-gradient(to right, #0000ff, #ff0000)', color: 'white' } }, sunset: { label: 'Sunset', css: { background: 'linear-gradient(to top, #ff5f6d, #6a3093)' } }, }, // You can have both colored and underlined text at the same time. // If you don't want that, you should group them within the same key. // (just like I did with defaultColors and my fancy gradients) underline: { 'solid': { label: 'Solid', css: { 'text-decoration': 'underline', 'text-underline-offset': '4px' } }, // You'll probably want to use the CSS light-dark() utility. 'yellow-dashed': { label: 'Yellow Dashed', css: { 'text-decoration': 'underline dashed', 'text-decoration-color': 'light-dark(#EAB308,yellow)', 'text-underline-offset': '4px' } }, }, }, }), ``` This is what the example above will look like: ![Example usage in light and dark mode for TextStateFeature with defaultColors and some custom styles](https://payloadcms.com/images/docs/text-state-feature.png) #### Rendering on the frontend When Lexical serializes a text node that has state applied, the state is stored under a `"$"` key in the node object. To apply the styles when rendering rich text on your frontend, you need a custom `text` JSX converter that reads from this key and maps the values back to your CSS config. **Step 1 — Share your state config** Extract your `TextStateFeature` state into a file with no package imports so it can be safely imported in both server and client contexts: ```ts // src/fields/textStateConfig.ts export const textStateConfig = { color: { 'text-red': { label: 'Red', css: { color: 'light-dark(oklch(0.577 0.245 27.325), oklch(0.704 0.191 22.216))', }, }, 'text-blue': { label: 'Blue', css: { color: 'light-dark(oklch(0.546 0.245 262.881), oklch(0.707 0.165 254.624))', }, }, // ...other colors }, } as const ``` Then reference it in your field config: ```ts // src/blocks/Content/config.ts import { TextStateFeature, lexicalEditor } from '@payloadcms/richtext-lexical' import { textStateConfig } from '@/fields/textStateConfig' { name: 'richText', type: 'richText', editor: lexicalEditor({ features: ({ rootFeatures }) => [ ...rootFeatures, TextStateFeature({ state: { color: textStateConfig.color, }, }), ], }), } ``` **Step 2 — Add a custom text converter** When using [`RichText` from `@payloadcms/richtext-lexical/react`](/docs/v4/rich-text/converting-jsx.md#richtext-to-jsx), override the default `text` converter to read the `"$"` key and apply the corresponding CSS as inline styles: ```tsx // src/components/RichText/index.tsx import { textStateConfig } from '@/fields/textStateConfig' import { JSXConvertersFunction, RichText as ConvertRichText, } from '@payloadcms/richtext-lexical/react' // Lexical serializes node state under the "$" key. const NODE_STATE_KEY = '$' // React's style prop requires camelCase, but TextStateFeature CSS uses hyphen-case. function hyphenToCamel(str: string): string { return str.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase()) } const jsxConverters: JSXConvertersFunction = ({ defaultConverters }) => ({ ...defaultConverters, text: (args) => { const { node } = args // Render standard formatting (bold, italic, etc.) using the default converter let text = typeof defaultConverters.text === 'function' ? defaultConverters.text(args) : node.text // Apply TextStateFeature styles from the "$" key in the serialized node const nodeState = (node as any)[NODE_STATE_KEY] as Record | undefined if (nodeState) { const styles: React.CSSProperties = {} for (const [stateKey, stateValue] of Object.entries(nodeState)) { const css = (textStateConfig as any)[stateKey]?.[stateValue]?.css if (css) { for (const [prop, value] of Object.entries(css)) { ;(styles as any)[hyphenToCamel(prop)] = value } } } if (Object.keys(styles).length > 0) { text = {text} } } return text }, }) export default function RichText({ data, ...rest }) { return } ``` The key insight is that `textStateConfig` is the single source of truth — it is passed directly to `TextStateFeature` in your field config and also imported by your frontend converter to resolve the CSS values at render time. # Blocks Source: https://payloadcms.com/docs/beta/rich-text/blocks The `BlocksFeature` allows you to embed Payload's [Blocks Field](/docs/v4/fields/blocks.md) directly inside your Lexical rich text editor. This provides a powerful way to create structured, reusable content components within your rich text content. > **Success** > > Blocks within Lexical support the same features as standard Payload > blocks—including all field types, hooks, validation, access control, and > conditional logic. The only difference is that the data is stored within the > rich text JSON structure rather than as separate fields. ## Basic Setup Documentation image To add blocks to your Lexical editor, include the `BlocksFeature` in your editor configuration: ```ts import { lexicalEditor, BlocksFeature } from '@payloadcms/richtext-lexical' { name: 'content', type: 'richText', editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, BlocksFeature({ blocks: [ { slug: 'banner', fields: [ { name: 'style', type: 'select', options: ['info', 'warning', 'error', 'success'], defaultValue: 'info', }, { name: 'content', type: 'textarea', required: true, }, ], }, { slug: 'cta', fields: [ { name: 'heading', type: 'text', required: true, }, { name: 'link', type: 'text', }, ], }, ], }), ], }), } ``` Blocks use the same configuration schema as Blocks within Payload's [Blocks Field](/docs/v4/fields/blocks.md). ## Blocks vs Inline Blocks Documentation image The `BlocksFeature` supports two types of blocks: ### Blocks Regular blocks are **block-level elements** that take up an entire line, similar to paragraphs or headings. They cannot be placed inline with text. Use blocks for: - Call-to-action sections - Image galleries - Code snippets - Embedded content (videos, maps) - Any component that should stand alone ### Inline Blocks Inline blocks can be **inserted within text**, appearing alongside other content in the same paragraph. They're useful for elements that need to flow with text. Use inline blocks for: - Mentions (@user) - Custom badges or tags - Inline icons or emojis - Variable placeholders - Footnote references ```ts BlocksFeature({ // Block-level blocks blocks: [ { slug: 'callout', fields: [{ name: 'content', type: 'textarea' }], }, ], // Inline blocks (appear within text) inlineBlocks: [ { slug: 'mention', fields: [ { name: 'user', type: 'relationship', relationTo: 'users', required: true, }, ], }, ], }) ``` ## Data Structure Block data is stored within the Lexical JSON structure. Each block node contains a `fields` object with all the block's field values: ```json { "type": "block", "version": 2, "fields": { "id": "65298b13db4ef8c744a7faaa", // default field (required, auto-generated) "blockType": "banner", // default field (required, identifies the block) "blockName": "Important Notice", // default field (optional, custom label for the block instance) "style": "warning", // custom field "content": "This is the block content..." // custom field } } ``` Inline blocks follow a similar structure with `type: "inlineBlock"`. ## Custom Block Components You can customize how blocks appear in the editor by providing custom React components. This is useful when you want a more visual representation of your block content. ### Block Components Documentation image For regular blocks, use the `admin.components.Block` property to provide a custom component: ```ts { slug: 'myCustomBlock', admin: { components: { Block: '/path/to/MyBlockComponent#MyBlockComponent', }, }, fields: [ { name: 'style', type: 'select', options: ['primary', 'secondary'], }, ], } ``` Your custom component can use composable primitives from `@payloadcms/richtext-lexical/client`. These components automatically receive block data from context, so you can use them to recreate the default block UI or arrange them in custom layouts: ```tsx 'use client' import type { LexicalBlockClientProps } from '@payloadcms/richtext-lexical' import { BlockCollapsible, BlockEditButton, BlockRemoveButton, } from '@payloadcms/richtext-lexical/client' import { useFormFields } from '@payloadcms/ui' export const MyBlockComponent: React.FC = () => { const style = useFormFields(([fields]) => fields.style) return (
Style: {(style?.value as string) ?? 'none'}
You can manually render the remove and edit buttons if you want to:
) } ``` The `BlockCollapsible` component automatically renders an edit button that opens a drawer with the block's fields. You can customize this behavior by passing props like `removeButton={false}` to hide the default remove button and render it yourself. You can also choose to render something completely different in your custom block component: Documentation image ```tsx 'use client' import type { LexicalBlockClientProps } from '@payloadcms/richtext-lexical' import { BlockEditButton, BlockRemoveButton, } from '@payloadcms/richtext-lexical/client' import { useFormFields } from '@payloadcms/ui' export const BlockComponent: React.FC = () => { const content = useFormFields(([fields]) => fields.content) return (
⚠️ Banner

{(content?.value as string) || 'No content'}

) } ``` ### Inline Block Components For inline blocks, similar composable primitives are available: Documentation image ```tsx 'use client' import type { LexicalInlineBlockClientProps } from '@payloadcms/richtext-lexical' import { InlineBlockContainer, InlineBlockEditButton, InlineBlockLabel, InlineBlockRemoveButton, } from '@payloadcms/richtext-lexical/client' export const MyInlineBlockComponent: React.FC< LexicalInlineBlockClientProps > = () => { return ( 1 2 3 ) } ``` Or, you can choose to render something completely different in your custom inline block component, for example a badge with a username: Documentation image ```tsx 'use client' import type { LexicalInlineBlockClientProps } from '@payloadcms/richtext-lexical' import { InlineBlockEditButton, InlineBlockRemoveButton, } from '@payloadcms/richtext-lexical/client' import { useFormFields } from '@payloadcms/ui' export const MyInlineBlockComponent: React.FC< LexicalInlineBlockClientProps > = () => { const username = useFormFields(([fields]) => fields.username) return (
@{(username?.value as string) || 'username'}
) } ``` ### Label Components You can also customize the label shown in the block header using `admin.components.Label`. This is useful for displaying dynamic information based on the block's field values. **Block Label:** Documentation image ```tsx 'use client' import type { LexicalBlockLabelClientProps } from '@payloadcms/richtext-lexical' import { useFormFields } from '@payloadcms/ui' export const MyBlockLabel: React.FC = () => { const title = useFormFields(([fields]) => fields.title) return (
Custom Label. Value of title field: {title?.value as string}
) } ``` **Inline Block Label:** Documentation image ```tsx 'use client' import type { LexicalInlineBlockLabelClientProps } from '@payloadcms/richtext-lexical' import { useFormFields } from '@payloadcms/ui' export const MyInlineBlockLabel: React.FC< LexicalInlineBlockLabelClientProps > = () => { const name = useFormFields(([fields]) => fields.name) return ( Custom Label. Name field: {name?.value as string} ) } ``` ### Example: Pre-made CodeBlock For a real-world example of a custom block component, see the [source code for Payload's pre-made CodeBlock](https://github.com/payloadcms/payload/blob/main/packages/richtext-lexical/src/features/blocks/premade/CodeBlock/index.ts). It's a standard block with a custom `admin.components.Block` component that uses the same APIs documented above—including `useFormFields`, `BlockCollapsible`, and the helper buttons. ### TypeScript When building custom block components, you can import the following types for proper typing: ```ts import type { // Block component types LexicalBlockClientProps, LexicalBlockServerProps, // Block label component types LexicalBlockLabelClientProps, LexicalBlockLabelServerProps, // Inline block component types LexicalInlineBlockClientProps, LexicalInlineBlockServerProps, // Inline block label component types LexicalInlineBlockLabelClientProps, LexicalInlineBlockLabelServerProps, } from '@payloadcms/richtext-lexical' ``` | Type | Use Case | | ---------------------------------------- | ---------------------------------------------------- | | **`LexicalBlockClientProps`** | Client component for `admin.components.Block` | | **`LexicalBlockServerProps`** | Server component for `admin.components.Block` | | **`LexicalBlockLabelClientProps`** | Client component for `admin.components.Label` | | **`LexicalBlockLabelServerProps`** | Server component for `admin.components.Label` | | **`LexicalInlineBlockClientProps`** | Client component for inline `admin.components.Block` | | **`LexicalInlineBlockServerProps`** | Server component for inline `admin.components.Block` | | **`LexicalInlineBlockLabelClientProps`** | Client component for inline `admin.components.Label` | | **`LexicalInlineBlockLabelServerProps`** | Server component for inline `admin.components.Label` | ## Rendering Blocks When rendering rich text content on the frontend, blocks need to be handled by your converter configuration. See the following guides for details: - [JSX Converters](/docs/v4/rich-text/converting-jsx.md#lexical-blocks) - For React-based applications - [HTML Converters](/docs/v4/rich-text/converting-html.md#blocks-to-html) - For static HTML output - [Markdown Converters](/docs/v4/rich-text/converting-markdown.md#defining-a-custom-block) - For markdown output Each converter allows you to define custom renderers for your block types, giving you full control over how block content appears on your frontend. ## Code Block Documentation image Payload provides a pre-built `CodeBlock` that you can use directly in your projects. It includes syntax highlighting, language selection, and optional TypeScript type definitions support: ```ts import { BlocksFeature, CodeBlock } from '@payloadcms/richtext-lexical' BlocksFeature({ blocks: [ CodeBlock({ defaultLanguage: 'ts', languages: { plaintext: 'Plain Text', js: 'JavaScript', ts: 'TypeScript', tsx: 'TSX', jsx: 'JSX', }, }), ], }) ``` ### CodeBlock Options | Option | Description | | --------------------- | ----------------------------------------------------------------- | | **`slug`** | Override the block slug. Default: `'Code'` | | **`defaultLanguage`** | The default language selection. Default: first key in `languages` | | **`languages`** | Object mapping language keys to display labels | | **`typescript`** | TypeScript-specific configuration (see below) | | **`fieldOverrides`** | Partial block config to override or extend the default CodeBlock | ### TypeScript Support When using TypeScript as a language option, you can load external type definitions to provide IntelliSense in the editor: ```ts CodeBlock({ slug: 'PayloadCode', languages: { ts: 'TypeScript', }, typescript: { fetchTypes: [ { // In the url you can use @latest or a specific version (e.g. @3.68.5) url: 'https://unpkg.com/payload@latest/dist/index.bundled.d.ts', filePath: 'file:///node_modules/payload/index.d.ts', }, { url: 'https://unpkg.com/@types/react@latest/index.d.ts', filePath: 'file:///node_modules/@types/react/index.d.ts', }, ], paths: { payload: ['file:///node_modules/payload/index.d.ts'], react: ['file:///node_modules/@types/react/index.d.ts'], }, typeRoots: ['node_modules/@types', 'node_modules/payload'], enableSemanticValidation: true, }, }) ``` | TypeScript Option | Description | | ------------------------------ | ------------------------------------------------------------------------------ | | **`fetchTypes`** | Array of `{ url, filePath }` objects to fetch external type definitions | | **`paths`** | Module path mappings for import resolution | | **`typeRoots`** | Directories to search for type definitions. Default: `['node_modules/@types']` | | **`target`** | TypeScript compilation target. Default: `'ESNext'` | | **`enableSemanticValidation`** | Enable full type checking (not just syntax). Default: `false` | # Custom Features Source: https://payloadcms.com/docs/beta/rich-text/custom-features Before you begin building custom features for Lexical, it is crucial to familiarize yourself with the [Lexical docs](https://lexical.dev/docs/intro), particularly the "Concepts" section. This foundation is necessary for understanding Lexical's core principles, such as nodes, editor state, and commands. Lexical features are designed to be modular, meaning each piece of functionality is encapsulated within just two specific interfaces: one for server-side code and one for client-side code. By convention, these are named `feature.server.ts` for server-side functionality and `feature.client.ts` for client-side functionality. The primary functionality is housed within `feature.server.ts`, which users will import into their projects. The client-side feature, although defined separately, is integrated and rendered server-side through the server feature. That way, we still maintain a clear boundary between server and client code, while also centralizing the code needed for a feature in basically one place. This approach is beneficial for managing all the bits and pieces which make up your feature as a whole, such as toolbar entries, buttons, or new nodes, allowing each feature to be neatly contained and managed independently. > **Important:** > Do not import directly from core lexical packages - this may break in minor Payload version bumps. > > Instead, import the re-exported versions from `@payloadcms/richtext-lexical`. For example, change `import { $insertNodeToNearestRoot } from '@lexical/utils'` to `import { $insertNodeToNearestRoot } from '@payloadcms/richtext-lexical/lexical/utils'` ## Do I need a custom feature? Before you start building a custom feature, consider whether you can achieve your desired functionality using the existing `BlocksFeature`. The `BlocksFeature` is a powerful feature that allows you to create custom blocks with a variety of options, including custom React components, markdown converters, and more. If you can achieve your desired functionality using the `BlocksFeature`, it is recommended to use it instead of building a custom feature. Using the BlocksFeature, you can add both inline blocks (= can be inserted into a paragraph, in between text) and block blocks (= take up the whole line) to the editor. If you simply want to bring custom react components into the editor, this is the way to go. For detailed documentation on the BlocksFeature, including custom block components and examples, see the [Blocks documentation](/docs/v4/rich-text/blocks.md). ## Server Feature Custom Blocks are not enough? To start building a custom feature, you should start with the server feature, which is the entry-point. **Example myFeature/feature.server.ts:** ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' export const MyFeature = createServerFeature({ feature: {}, key: 'myFeature', }) ``` `createServerFeature` is a helper function which lets you create new features without boilerplate code. Now, the feature is ready to be used in the editor: ```ts import { MyFeature } from './myFeature/feature.server'; import { lexicalEditor } from '@payloadcms/richtext-lexical'; //... { name: 'richText', type: 'richText', editor: lexicalEditor({ features: [ MyFeature(), ], }), }, ``` By default, this server feature does nothing - you haven't added any functionality yet. Depending on what you want your feature to do, the ServerFeature type exposes various properties you can set to inject custom functionality into the lexical editor. ### i18n Each feature can register their own translations, which are automatically scoped to the feature key: ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' export const MyFeature = createServerFeature({ feature: { i18n: { en: { label: 'My Feature', }, de: { label: 'Mein Feature', }, }, }, key: 'myFeature', }) ``` This allows you to add i18n translations scoped to your feature. This specific example translation will be available under `lexical:myFeature:label` - `myFeature` being your feature key. ### Markdown Transformers#server-feature-markdown-transformers The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](/docs/v4/rich-text/converting-markdown.md). ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' import type { ElementTransformer } from '@payloadcms/richtext-lexical/lexical/markdown' import { $createMyNode, $isMyNode, MyNode } from './nodes/MyNode' const MyMarkdownTransformer: ElementTransformer = { type: 'element', dependencies: [MyNode], export: (node, exportChildren) => { if (!$isMyNode(node)) { return null } return '+++' }, // match --- regExp: /^+++\s*$/, replace: (parentNode) => { const node = $createMyNode() if (node) { parentNode.replace(node) } }, } export const MyFeature = createServerFeature({ feature: { markdownTransformers: [MyMarkdownTransformer], }, key: 'myFeature', }) ``` In this example, the node will be outputted as `+++` in Markdown, and the markdown `+++` will be converted to a `MyNode` node in the editor. ### Nodes#server-feature-nodes While nodes added to the server feature do not control how the node is rendered in the editor, they control other aspects of the node: - HTML conversion - Node Hooks - Sub fields - Behavior in a headless editor The `createNode` helper function is used to create nodes with proper typing. It is recommended to use this function to create nodes. ```ts import { createServerFeature, createNode } from '@payloadcms/richtext-lexical' import { MyNode } from './nodes/MyNode' export const MyFeature = createServerFeature({ feature: { nodes: [ // Use the createNode helper function to more easily create nodes with proper typing createNode({ converters: { html: { converter: () => { return `
` }, nodeTypes: [MyNode.getType()], }, }, // Here you can add your actual node. On the server, they will be // used to initialize a headless editor which can be used to perform // operations on the editor, like markdown / html conversion. node: MyNode, }), ], }, key: 'myFeature', }) ``` While nodes in the client feature are added by themselves to the nodes array, nodes in the server feature can be added together with the following sibling options: | Option | Description | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`getSubFields`** | If a node includes sub-fields (e.g. block and link nodes), passing the subFields schema here will make Payload automatically populate & run hooks for them. | | **`getSubFieldsData`** | If a node includes sub-fields, the sub-fields data needs to be returned here, alongside `getSubFields` which returns their schema. | | **`graphQLPopulationPromises`** | Allows you to run population logic when a node's data was requested from GraphQL. While `getSubFields` and `getSubFieldsData` automatically handle populating sub-fields (since they run hooks on them), those are only populated in the Rest API. This is because the Rest API hooks do not have access to the 'depth' property provided by GraphQL. In order for them to be populated correctly in GraphQL, the population logic needs to be provided here. | | **`node`** | The actual lexical node needs to be provided here. This also supports [lexical node replacements](https://lexical.dev/docs/concepts/node-replacement). | | **`validations`** | This allows you to provide node validations, which are run when your document is being validated, alongside other Payload fields. You can use it to throw a validation error for a specific node in case its data is incorrect. | | **`converters`** | Allows you to define how a node can be serialized into different formats. Currently, only supports HTML. Markdown converters are defined in `markdownTransformers` and not here. | | **`hooks`** | Just like Payload fields, you can provide hooks which are run for this specific node. These are called Node Hooks. | ### Feature load order Server features can also accept a function as the `feature` property (useful for sanitizing props, as mentioned below). This function will be called when the feature is loaded during the Payload sanitization process: ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' createServerFeature({ //... feature: ({ config, isRoot, props, resolvedFeatures, unSanitizedEditorConfig, featureProviderMap, }) => { return { //Actual server feature here... } }, }) ``` "Loading" here means the process of calling this `feature` function. By default, features are called in the order in which they are added to the editor. However, sometimes you might want to load a feature after another feature has been loaded, or require a different feature to be loaded, throwing an error if this is not the case. Within lexical, one example where this is done are our list features. Both `UnorderedListFeature` and `OrderedListFeature` register the same `ListItem` node. Within `UnorderedListFeature` we register it normally, but within `OrderedListFeature` we want to only register the `ListItem` node if the `UnorderedListFeature` is not present - otherwise, we would have two features registering the same node. Here is how we do it: ```ts import { createServerFeature, createNode } from '@payloadcms/richtext-lexical' export const OrderedListFeature = createServerFeature({ feature: ({ featureProviderMap }) => { return { // ... nodes: featureProviderMap.has('unorderedList') ? [] : [ createNode({ // ... }), ], } }, key: 'orderedList', }) ``` `featureProviderMap` will always be available and contain all the features, even yet-to-be-loaded ones, so we can check if a feature is loaded by checking if its `key` present in the map. If you wanted to make sure a feature is loaded before another feature, you can use the `dependenciesPriority` property: ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' export const MyFeature = createServerFeature({ feature: ({ featureProviderMap }) => { return { // ... } }, key: 'myFeature', dependenciesPriority: ['otherFeature'], }) ``` | Option | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`dependenciesSoft`** | Keys of soft-dependencies needed for this feature. These are optional. Payload will attempt to load them before this feature, but doesn't throw an error if that's not possible. | | **`dependencies`** | Keys of dependencies needed for this feature. These dependencies do not have to be loaded first, but they have to exist, otherwise an error will be thrown. | | **`dependenciesPriority`** | Keys of priority dependencies needed for this feature. These dependencies have to be loaded first AND have to exist, otherwise an error will be thrown. They will be available in the `feature` property. | ## Client Feature Most of the functionality which the user actually sees and interacts with, like toolbar items and React components for nodes, resides on the client-side. To set up your client-side feature, follow these three steps: 1. **Create a Separate File**: Start by creating a new file specifically for your client feature, such as `myFeature/feature.client.ts`. It's important to keep client and server features in separate files to maintain a clean boundary between server and client code. 2. **'use client'**: Mark that file with a 'use client' directive at the top of the file 3. **Register the Client Feature**: Register the client feature within your server feature, by passing it to the `ClientFeature` prop. This is needed because the server feature is the sole entry-point of your feature. This also means you are not able to create a client feature without a server feature, as you will not be able to register it otherwise. **Example myFeature/feature.client.ts:** ```ts 'use client' import { createClientFeature } from '@payloadcms/richtext-lexical/client' export const MyClientFeature = createClientFeature({}) ``` Explore the APIs available through ClientFeature to add the specific functionality you need. Remember, do not import directly from `'@payloadcms/richtext-lexical'` when working on the client-side, as it will cause errors with webpack or turbopack. Instead, use `'@payloadcms/richtext-lexical/client'` for all client-side imports. Type-imports are excluded from this rule and can always be imported. ### Adding a client feature to the server feature Inside of your server feature, you can provide an [import path](/docs/v4/custom-components/overview.md#component-paths) to the client feature like this: ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' export const MyFeature = createServerFeature({ feature: { ClientFeature: './path/to/feature.client#MyClientFeature', }, key: 'myFeature', dependenciesPriority: ['otherFeature'], }) ``` ### Nodes#client-feature-nodes Add nodes to the `nodes` array in **both** your client & server feature. On the server side, nodes are utilized for backend operations like HTML conversion in a headless editor. On the client side, these nodes are integral to how content is displayed and managed in the editor, influencing how they are rendered, behave, and saved in the database. Example: **myFeature/feature.client.ts:** ```ts 'use client' import { createClientFeature } from '@payloadcms/richtext-lexical/client' import { MyNode } from './nodes/MyNode' export const MyClientFeature = createClientFeature({ nodes: [MyNode], }) ``` This also supports [lexical node replacements](https://lexical.dev/docs/concepts/node-replacement). **myFeature/nodes/MyNode.tsx:** Here is a basic DecoratorNode example: ```ts import type { DOMConversionMap, DOMConversionOutput, DOMExportOutput, EditorConfig, LexicalNode, SerializedLexicalNode, } from '@payloadcms/richtext-lexical/lexical' import { $applyNodeReplacement, DecoratorNode } from '@payloadcms/richtext-lexical/lexical' // SerializedLexicalNode is the default lexical node. // By setting your SerializedMyNode type to SerializedLexicalNode, // you are basically saying that this node does not save any additional data. // If you want your node to save data, feel free to extend it export type SerializedMyNode = SerializedLexicalNode // Lazy-import the React component to your node here const MyNodeComponent = React.lazy(() => import('../component/index.js').then((module) => ({ default: module.MyNodeComponent, })), ) /** * This node is a DecoratorNode. DecoratorNodes allow * you to render React components in the editor. * * They need both createDom and decorate functions. * createDom => outside of the html. * decorate => React Component inside of the html. * * If we used DecoratorBlockNode instead, * we would only need a decorate method */ export class MyNode extends DecoratorNode { static clone(node: MyNode): MyNode { return new MyNode(node.__key) } static getType(): string { return 'myNode' } /** * Defines what happens if you copy a div element * from another page and paste it into the lexical editor * * This also determines the behavior of lexical's * internal HTML -> Lexical converter */ static importDOM(): DOMConversionMap | null { return { div: () => ({ conversion: $yourConversionMethod, priority: 0, }), } } /** * The data for this node is stored serialized as JSON. * This is the "load function" of that node: it takes * the saved data and converts it into a node. */ static importJSON(serializedNode: SerializedMyNode): MyNode { return $createMyNode() } /** * Determines how the hr element is rendered in the * lexical editor. This is only the "initial" / "outer" * HTML element. */ createDOM(config: EditorConfig): HTMLElement { const element = document.createElement('div') return element } /** * Allows you to render a React component within * whatever createDOM returns. */ decorate(): React.ReactElement { return } /** * Opposite of importDOM, this function defines what * happens when you copy a div element from the lexical * editor and paste it into another page. * * This also determines the behavior of lexical's * internal Lexical -> HTML converter */ exportDOM(): DOMExportOutput { return { element: document.createElement('div') } } /** * Opposite of importJSON. This determines what * data is saved in the database / in the lexical * editor state. */ exportJSON(): SerializedLexicalNode { return { type: 'myNode', version: 1, } } getTextContent(): string { return '\n' } isInline(): false { return false } updateDOM(): boolean { return false } } // This is used in the importDOM method. Totally optional // if you do not want your node to be created automatically // when copy & pasting certain dom elements into your editor. function $yourConversionMethod(): DOMConversionOutput { return { node: $createMyNode() } } // This is a utility method to create a new MyNode. // Utility methods prefixed with $ make it explicit // that this should only be used within lexical export function $createMyNode(): MyNode { return $applyNodeReplacement(new MyNode()) } // This is just a utility method you can use // to check if a node is a MyNode. This also // ensures correct typing. export function $isMyNode( node: LexicalNode | null | undefined, ): node is MyNode { return node instanceof MyNode } ``` Please do not add any 'use client' directives to your nodes, as the node class can be used on the server. ### Plugins One small part of a feature are plugins. The name stems from the lexical playground plugins and is just a small part of a lexical feature. Plugins are simply React components which are added to the editor, within all the lexical context providers. They can be used to add any functionality to the editor, by utilizing the lexical API. Most commonly, they are used to register [lexical listeners](https://lexical.dev/docs/concepts/listeners), [node transforms](https://lexical.dev/docs/concepts/transforms) or [commands](https://lexical.dev/docs/concepts/commands). For example, you could add a drawer to your plugin and register a command which opens it. That command can then be called from anywhere within lexical, e.g. from within your custom lexical node. To add a plugin, simply add it to the `plugins` array in your client feature: ```ts 'use client' import { createClientFeature } from '@payloadcms/richtext-lexical/client' import { MyPlugin } from './plugin' export const MyClientFeature = createClientFeature({ plugins: [MyPlugin], }) ``` Example plugin.tsx: ```ts 'use client' import type { LexicalCommand } from '@payloadcms/richtext-lexical/lexical' import { createCommand, $getSelection, $isRangeSelection, COMMAND_PRIORITY_EDITOR, } from '@payloadcms/richtext-lexical/lexical' import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext' import { $insertNodeToNearestRoot } from '@payloadcms/richtext-lexical/lexical/utils' import { useEffect } from 'react' import type { PluginComponent } from '@payloadcms/richtext-lexical' // type imports can be imported from @payloadcms/richtext-lexical - even on the client import { $createMyNode } from '../nodes/MyNode' import './index.scss' export const INSERT_MYNODE_COMMAND: LexicalCommand = createCommand( 'INSERT_MYNODE_COMMAND', ) /** * Plugin which registers a lexical command to * insert a new MyNode into the editor */ export const MyNodePlugin: PluginComponent = () => { // The useLexicalComposerContext hook can be used // to access the lexical editor instance const [editor] = useLexicalComposerContext() useEffect(() => { return editor.registerCommand( INSERT_MYNODE_COMMAND, (type) => { const selection = $getSelection() if (!$isRangeSelection(selection)) { return false } const focusNode = selection.focus.getNode() if (focusNode !== null) { const newMyNode = $createMyNode() $insertNodeToNearestRoot(newMyNode) } return true }, COMMAND_PRIORITY_EDITOR, ) }, [editor]) return null } ``` In this example, we register a lexical command, which simply inserts a new MyNode into the editor. This command can be called from anywhere within lexical, e.g. from within a custom node. ### Toolbar groups Toolbar groups are visual containers which hold toolbar items. There are different toolbar group types which determine _how_ a toolbar item is displayed: `dropdown` and `buttons`. All the default toolbar groups are exported from `@payloadcms/richtext-lexical/client`. You can use them to add your own toolbar items to the editor: - Dropdown: `toolbarAddDropdownGroupWithItems` - Dropdown: `toolbarTextDropdownGroupWithItems` - Buttons: `toolbarFormatGroupWithItems` - Buttons: `toolbarFeatureButtonsGroupWithItems` Within dropdown groups, items are positioned vertically when the dropdown is opened and include the icon & label. Within button groups, items are positioned horizontally and only include the icon. If a toolbar group with the same key is declared twice, all its items will be merged into one group. #### Custom buttons toolbar group | Option | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`items`** | All toolbar items part of this toolbar group need to be added here. | | **`key`** | Each toolbar group needs to have a unique key. Groups with the same keys will have their items merged together. | | **`order`** | Determines where the toolbar group will be. | | **`type`** | Controls the toolbar group type. Set to `buttons` to create a buttons toolbar group, which displays toolbar items horizontally using only their icons. | Example: ```ts import type { ToolbarGroup, ToolbarGroupItem, } from '@payloadcms/richtext-lexical' export const toolbarFormatGroupWithItems = ( items: ToolbarGroupItem[], ): ToolbarGroup => { return { type: 'buttons', items, key: 'myButtonsToolbar', order: 10, } } ``` #### Custom dropdown toolbar group | Option | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`items`** | All toolbar items part of this toolbar group need to be added here. | | **`key`** | Each toolbar group needs to have a unique key. Groups with the same keys will have their items merged together. | | **`order`** | Determines where the toolbar group will be. | | **`type`** | Controls the toolbar group type. Set to `dropdown` to create a buttons toolbar group, which displays toolbar items vertically using their icons and labels, if the dropdown is open. | | **`ChildComponent`** | The dropdown toolbar ChildComponent allows you to pass in a React Component which will be displayed within the dropdown button. | Example: ```ts import type { ToolbarGroup, ToolbarGroupItem, } from '@payloadcms/richtext-lexical' import { MyIcon } from './icons/MyIcon' export const toolbarAddDropdownGroupWithItems = ( items: ToolbarGroupItem[], ): ToolbarGroup => { return { type: 'dropdown', ChildComponent: MyIcon, items, key: 'myDropdownToolbar', order: 10, } } ``` ### Toolbar items Custom nodes and features on its own are pointless, if they can't be added to the editor. You will need to hook in one of our interfaces which allow the user to interact with the editor: - Fixed toolbar which stays fixed at the top of the editor - Inline, floating toolbar which appears when selecting text - Slash menu which appears when typing `/` in the editor - Markdown transformers, which are triggered when a certain text pattern is typed in the editor - Or any other interfaces which can be added via your own plugins. Our toolbars are a prime example of this - they are just plugins. To add a toolbar item to either the floating or the inline toolbar, you can add a ToolbarGroup with a ToolbarItem to the `toolbarFixed` or `toolbarInline` props of your client feature: ```ts 'use client' import { createClientFeature, toolbarAddDropdownGroupWithItems, } from '@payloadcms/richtext-lexical/client' import { IconComponent } from './icon' import { $isHorizontalRuleNode } from './nodes/MyNode' import { INSERT_MYNODE_COMMAND } from './plugin' import { $isNodeSelection } from '@payloadcms/richtext-lexical/lexical' export const MyClientFeature = createClientFeature({ toolbarFixed: { groups: [ toolbarAddDropdownGroupWithItems([ { ChildComponent: IconComponent, isActive: ({ selection }) => { if (!$isNodeSelection(selection) || !selection.getNodes().length) { return false } const firstNode = selection.getNodes()[0] return $isHorizontalRuleNode(firstNode) }, key: 'myNode', label: ({ i18n }) => { return i18n.t('lexical:myFeature:label') }, onSelect: ({ editor }) => { editor.dispatchCommand(INSERT_MYNODE_COMMAND, undefined) }, }, ]), ], }, }) ``` You will have to provide a toolbar group first, and then the items for that toolbar group (more on that above). A `ToolbarItem` various props you can use to customize its behavior: | Option | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`ChildComponent`** | A React component which is rendered within your toolbar item's default button component. Usually, you want this to be an icon. | | **`Component`** | A React component which is rendered in place of the toolbar item's default button component, thus completely replacing it. The `ChildComponent` and `onSelect` properties will be ignored. | | **`label`** | The label will be displayed in your toolbar item, if it's within a dropdown group. To make use of i18n, this can be a function. | | **`key`** | Each toolbar item needs to have a unique key. | | **`onSelect`** | A function which is called when the toolbar item is clicked. | | **`isEnabled`** | This is optional and controls if the toolbar item is clickable or not. If `false` is returned here, it will be grayed out and unclickable. | | **`isActive`** | This is optional and controls if the toolbar item is highlighted or not | The API for adding an item to the floating inline toolbar (`toolbarInline`) is identical. If you wanted to add an item to both the fixed and inline toolbar, you can extract it into its own variable (typed as `ToolbarGroup[]`) and add it to both the `toolbarFixed` and `toolbarInline` props. ### Slash Menu groups We're exporting `slashMenuBasicGroupWithItems` from `@payloadcms/richtext-lexical/client` which you can use to add items to the slash menu labelled "Basic". If you want to create your own slash menu group, here is an example: ```ts import type { SlashMenuGroup, SlashMenuItem, } from '@payloadcms/richtext-lexical' export function mwnSlashMenuGroupWithItems( items: SlashMenuItem[], ): SlashMenuGroup { return { items, key: 'myGroup', label: 'My Group', // <= This can be a function to make use of i18n } } ``` By creating a helper function like this, you can easily reuse it and add items to it. All Slash Menu groups with the same keys will have their items merged together. | Option | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **`items`** | An array of `SlashMenuItem`'s which will be displayed in the slash menu. | | **`label`** | The label will be displayed before your Slash Menu group. In order to make use of i18n, this can be a function. | | **`key`** | Used for class names and, if label is not provided, for display. Slash menus with the same key will have their items merged together. | ### Slash Menu items The API for adding items to the slash menu is similar. There are slash menu groups, and each slash menu groups has items. Here is an example: ```ts 'use client' import { createClientFeature, slashMenuBasicGroupWithItems, } from '@payloadcms/richtext-lexical/client' import { INSERT_MYNODE_COMMAND } from './plugin' import { IconComponent } from './icon' export const MyClientFeature = createClientFeature({ slashMenu: { groups: [ slashMenuBasicGroupWithItems([ { Icon: IconComponent, key: 'myNode', keywords: ['myNode', 'myFeature', 'someOtherKeyword'], label: ({ i18n }) => { return i18n.t('lexical:myFeature:label') }, onSelect: ({ editor }) => { editor.dispatchCommand(INSERT_MYNODE_COMMAND, undefined) }, }, ]), ], }, }) ``` | Option | Description | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`Icon`** | The icon which is rendered in your slash menu item. | | **`label`** | The label will be displayed in your slash menu item. In order to make use of i18n, this can be a function. | | **`key`** | Each slash menu item needs to have a unique key. The key will be matched when typing, displayed if no `label` property is set, and used for classNames. | | **`onSelect`** | A function which is called when the slash menu item is selected. | | **`keywords`** | Keywords are used to match the item for different texts typed after the '/'. E.g. you might want to show a horizontal rule item if you type both /hr, /separator, /horizontal etc. In addition to the keywords, the label and key will be used to find the right slash menu item. | ### Markdown Transformers#client-feature-markdown-transformers The Client Feature, just like the Server Feature, allows you to add markdown transformers. Markdown transformers on the client are used to create new nodes when a certain markdown pattern is typed in the editor. ```ts import { createClientFeature } from '@payloadcms/richtext-lexical/client' import type { ElementTransformer } from '@payloadcms/richtext-lexical/lexical/markdown' import { $createMyNode, $isMyNode, MyNode } from './nodes/MyNode' const MyMarkdownTransformer: ElementTransformer = { type: 'element', dependencies: [MyNode], export: (node, exportChildren) => { if (!$isMyNode(node)) { return null } return '+++' }, // match --- regExp: /^+++\s*$/, replace: (parentNode) => { const node = $createMyNode() if (node) { parentNode.replace(node) } }, } export const MyFeature = createClientFeature({ markdownTransformers: [MyMarkdownTransformer], }) ``` In this example, a new `MyNode` will be inserted into the editor when `+++ ` is typed. ### Providers You can add providers to your client feature, which will be nested below the `EditorConfigProvider`. This can be useful if you want to provide some context to your nodes or other parts of your feature. ```ts 'use client' import { createClientFeature } from '@payloadcms/richtext-lexical/client' import { TableContext } from './context' export const MyClientFeature = createClientFeature({ providers: [TableContext], }) ``` ## Props To accept props in your feature, type them as a generic. Server Feature: ```ts createServerFeature({ //... }) ``` Client Feature: ```ts createClientFeature({ //... }) ``` The unSanitized props are what the user will pass to the feature when they call its provider function and add it to their editor config. You then have an option to sanitize those props. To sanitize those in the server feature, you can pass a function to `feature` instead of an object: ```ts createServerFeature({ //... feature: ({ config, isRoot, props, resolvedFeatures, unSanitizedEditorConfig, featureProviderMap, }) => { const sanitizedProps = doSomethingWithProps(props) return { sanitizedServerFeatureProps: sanitizedProps, //Actual server feature here... } }, }) ``` Keep in mind that any sanitized props then have to be returned in the `sanitizedServerFeatureProps` property. In the client feature, it works similarly: ```ts createClientFeature( ({ clientFunctions, featureProviderMap, props, resolvedFeatures, unSanitizedEditorConfig, }) => { const sanitizedProps = doSomethingWithProps(props) return { sanitizedClientFeatureProps: sanitizedProps, //Actual client feature here... } }, ) ``` ### Bringing props from the server to the client By default, the client feature will never receive any props from the server feature. In order to pass props from the server to the client, you can need to return those props in the server feature: ```ts type UnSanitizedClientProps = { test: string } createServerFeature({ //... feature: { clientFeatureProps: { test: 'myValue', }, }, }) ``` The reason the client feature does not have the same props available as the server by default is because all client props need to be serializable. You can totally accept things like functions or Maps as props in your server feature, but you will not be able to send those to the client. In the end, those props are sent from the server to the client over the network, so they need to be serializable. ## More information Have a look at the [features we've already built](https://github.com/payloadcms/payload/tree/main/packages/richtext-lexical/src/features) - understanding how they work will help you understand how to create your own. There is no difference between the features included by default and the ones you create yourself - since those features are all isolated from the "core", you have access to the same APIs, whether the feature is part of Payload or not! # Rendering On Demand Source: https://payloadcms.com/docs/beta/rich-text/rendering-on-demand Lexical in Payload is a **React Server Component (RSC)**. Historically that created three headaches: 1. You couldn't render the editor directly from the client. 2. Features like blocks, tables and link drawers require the server to know the shape of nested sub-fields at render time. If you tried to render on demand, the server didn't know those schemas. 3. The rich text field is designed to live inside a `Form`. For simple use cases, setting up a full form just to manage editor state was cumbersome. To simplify rendering richtext on demand, , that renders a Lexical editor while still covering the full feature set. On mount, it calls a server action to render the editor on the server using the new `render-field` server function. That server render gives Lexical everything it needs (including nested field schemas) and returns a ready-to-hydrate editor. > **Warning** > > `RenderLexical` and the underlying `render-field` server function are > experimental and may change in minor releases. ## Inside an existing Form If you have an existing Form and want to render a richtext field within it, you can use the `RenderLexical` component like this: ```tsx 'use client' import type React from 'react' import type { JSONFieldClientProps } from 'payload' import { buildEditorState, RenderLexical, } from '@payloadcms/richtext-lexical/client' import { lexicalFullyFeaturedSlug } from '../../slugs.js' export const Component: React.FC = (args) => { return ( ({ text: 'default value', })} schemaPath={`collection.${lexicalFullyFeaturedSlug}.richText`} /> ) } ``` ## Outside of a Form (you control state) ```tsx 'use client' import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical' import type { JSONFieldClientProps } from 'payload' import { buildEditorState, RenderLexical, } from '@payloadcms/richtext-lexical/client' import React, { useState } from 'react' import { lexicalFullyFeaturedSlug } from '../../slugs.js' export const Component: React.FC = (args) => { // Manually manage the editor state const [value, setValue] = useState(() => buildEditorState({ text: 'state default' }), ) const handleReset = React.useCallback(() => { setValue(buildEditorState({ text: 'state default' })) }, []) return (
({ text: 'default value', })} schemaPath={`collection.${lexicalFullyFeaturedSlug}.richText`} setValue={setValue as any} value={value} />
) } ``` ## Choosing the schemaPath `schemaPath` tells the server which richText field to render. This gives the server the exact nested field schemas (blocks, relationship drawers, upload fields, tables, etc.). Format: - `collection..` - `global..` Example (top level): `collection.posts.richText` Example (nested in a group/tab): `collection.posts.content.richText` > **Tip:** If your target editor lives deep in arrays/blocks and you're unsure of the exact path, you can define a **hidden top-level richText** purely as a "render anchor": > > ```ts > { > name: 'onDemandAnchor', > type: 'richText', > admin: { hidden: true } > } > ``` > > Then use `schemaPath="collection.posts.onDemandAnchor"` # Views Source: https://payloadcms.com/docs/beta/rich-text/views > **Warning** > > **Experimental** — This feature is experimental and may change or be removed > in future releases. Views let you define shared rendering logic for Lexical nodes that works in both the admin panel and JSX converters, creating different rendering modes for the same content. In the admin panel, editors can switch between views to preview how content will look on your site. The same view node maps can be passed to JSX converters for consistent rendering outside the editor. Common use cases include site previews within the admin panel, A/B testing different designs, or applying different visual themes. ## Views vs JSX Converters [JSX converters](/docs/v4/rich-text/converting-jsx.md) are the standard way to render Lexical content in your app. Views are an alternative worth considering when: - You want to **share the same node rendering logic** between the admin panel and JSX converters — define it once in a view, use the node map in both places. - You need **multiple rendering modes** for the same field — in the admin panel a view selector is automatically rendered above the field, letting editors switch between views; in JSX converters, pass a view's node map for consistent rendering. If you only need to render content outside the editor and don't need admin panel integration, JSX converters alone are simpler and sufficient. ### Precedence When you pass both `converters` and a view's `nodeMap` to `convertLexicalToJSX` or the `RichText` component, the node map takes precedence per-node-type. For blocks and inline blocks, converters from both sources are deep-merged, with the node map winning when both define a converter for the same block type. ## Overview Views work in two contexts: in the **admin panel**, where editors can switch between them using the built-in view selector, and in **JSX converters**, where you pass a view's node map for consistent rendering. You can use views for either context independently — to customize how nodes render in the admin panel, to share rendering logic between both, or any combination. The view system works by overriding three aspects of the editor per view: 1. **Node Rendering**: Customize how individual node types render 2. **Admin Configuration**: Control UI elements like gutter, add block button, etc. 3. **Lexical Editor Config**: Override theme classes and other editor-level settings You can define multiple named views and, in the admin panel, switch between them using the built-in view selector, without changing the underlying data structure. ### Key Concepts - **View Map**: A collection of named views (e.g., `default`, `preview`, `debug`) - **Node Map**: Overrides for specific node types within a view - **View Configuration**: Each view can override `nodes`, `admin`, and `lexical` editor config - **Reusable Node Maps**: A view's node map can be passed to JSX converters for consistent rendering outside the editor ## Defining Views Views are defined using the `views` property on your rich text field configuration. The value is an [import path](/docs/v4/custom-components/overview.md#component-paths) pointing to your views file. ### Step 1: Create Your Views File Create a client component file that exports your view maps: ```tsx // collections/Posts/views.tsx 'use client' import type { LexicalEditorViewMap } from '@payloadcms/richtext-lexical' export const postViews: LexicalEditorViewMap = { // The 'default' view is used when the field is first loaded default: { nodes: { heading: { createDOM(args) { const { node } = args const heading = document.createElement(node.getTag()) heading.style.color = '#333' return heading }, }, }, }, // Additional custom views — name can be anything (e.g. 'preview', 'branded', 'debug') preview: { admin: { hideGutter: true, }, lexical: { theme: { link: 'preview-link', paragraph: 'preview-paragraph', }, }, nodes: { heading: { createDOM(args) { const { node } = args const heading = document.createElement(node.getTag()) heading.style.color = '#3b82f6' heading.style.borderBottom = '2px solid #60a5fa' return heading }, }, blocks: { myBlock: { Component: ({ node, isEditor, isJSXConverter }) => { const text = isEditor ? node.__fields?.text : node.fields?.text return (
{text}
) }, }, }, }, }, } ``` ### Step 2: Reference Views in Your Collection In your collection config, reference the views using an [import path](/docs/v4/custom-components/overview.md#component-paths): ```ts // collections/Posts/index.ts import type { CollectionConfig } from 'payload' import { lexicalEditor } from '@payloadcms/richtext-lexical' export const Posts: CollectionConfig = { slug: 'posts', fields: [ { name: 'content', type: 'richText', editor: lexicalEditor({ views: './views#postViews', }), }, ], } ``` ## View Configuration Options Each view can customize three aspects of the editor: ### Admin Configuration Override admin UI settings for a specific view: ```tsx { myView: { admin: { hideGutter: true, hideAddBlockButton: false, hideDraggableBlockElement: false, hideInsertParagraphAtEnd: false, placeholder: 'Start typing in this view...', }, nodes: { // ... node overrides }, }, } ``` These are the same [admin options](/docs/v4/rich-text/overview.md#admin-customization) available on the `lexicalEditor()` config. ### Feature Filtering Use `filterFeatures` to control which client features are active for a specific view. It receives the full features map and returns a modified version, allowing you to remove features or even add new ones per-view: ```tsx { // Remove toolbars for a clean preview experience preview: { filterFeatures: (features) => { const { toolbarFixed, toolbarInline, ...rest } = features return rest }, nodes: { // ... node overrides }, }, } ``` ### Lexical Editor Configuration The `lexical` property accepts any standard [Lexical Editor Config](https://lexical.dev/docs/api/interfaces/lexical.EditorConfig) options such as theme classes. Pass a function to extend the default config, or an object to replace it entirely: ```tsx { // Extend the default config (recommended) preview: { lexical: (defaultConfig) => ({ ...defaultConfig, theme: { ...defaultConfig.theme, link: 'preview-link-class', paragraph: 'preview-paragraph-class', }, }), }, // Replace the config entirely minimal: { lexical: { theme: { link: 'custom-link-class', paragraph: 'custom-paragraph-class', }, }, }, } ``` ### Node Overrides Each node type can be customized using three different approaches: #### Component Use a React component for full control over rendering. Works in both admin panel and JSX converters. ```tsx { myView: { nodes: { myNode: { Component: ({ node, editor, config, isEditor, isJSXConverter }) => { // isEditor: true when rendering in admin panel, false in JSX converter // isJSXConverter: true when rendering for frontend, false in admin panel const nodeData = isEditor ? node.__fields // Access Lexical node fields in editor : node.fields // Access serialized fields in JSX converter return
{nodeData.text}
}, }, }, }, } ``` #### createDOM Use native DOM manipulation for ElementNodes. Only works in the admin panel editor. ```tsx { myView: { nodes: { heading: { createDOM(args) { const { node, editor, config, isEditor, isJSXConverter } = args const heading = document.createElement(node.getTag()) heading.style.color = '#3b82f6' return heading }, }, }, }, } ``` #### html Provide raw HTML as a string or function. Works in both admin panel and JSX converters. ```tsx { myView: { nodes: { link: { html: ({ node, isEditor, isJSXConverter }) => { const url = isEditor ? node.__url : node.url return `Click here` }, }, }, }, } ``` **Note**: If both `createDOM` and `html` are provided for a DecoratorNode, `html` will only be used in JSX converters, not in the admin panel editor where `createDOM` takes precedence. ## Node Type Reference The `nodes` structure mirrors how [JSX converters](/docs/v4/rich-text/converting-jsx.md) are organized: top-level keys for built-in node types, with block and inline block types nested under `blocks` and `inlineBlocks` keys respectively. ### Built-in Nodes ```tsx { myView: { nodes: { heading: { /* overrides */ }, paragraph: { /* overrides */ }, link: { /* overrides */ }, list: { /* overrides */ }, listitem: { /* overrides */ }, quote: { /* overrides */ }, horizontalrule: { /* overrides */ }, }, }, } ``` ### Blocks Block types are nested under the `blocks` key, keyed by block slug: ```tsx { myView: { nodes: { blocks: { myBlockType: { Component: ({ node }) => { // node.__fields in editor, node.fields in JSX converter return
Custom block
}, }, }, }, }, } ``` ### Block and Label Components For blocks and inline blocks, you can also use `Block` and `Label` as alternatives to `Component`. #### Block `Block` is equivalent to passing [`admin.Block`](/docs/v4/fields/blocks.md#block-component) on the block definition itself — the block is still wrapped in the default Form with collapsible, edit/remove buttons, and field state. This makes it useful when you want to customize how a block looks in the editor while still rendering its fields. `useBlockComponentContext` is passed as a prop rather than imported directly from `@payloadcms/richtext-lexical/client`. This is intentional: if this view's node map is also passed to a JSX converter, importing from `/client` would pull unnecessary Lexical editor dependencies into your bundle. By passing it as a prop, it is only available (and only callable) when `isEditor` is `true`. Always narrow by `props.isEditor` before calling `props.useBlockComponentContext` — without that check you will get a TypeScript error, since the prop does not exist on the JSX converter variant of the props type. > **Bundle size:** If your view's node map is also passed to a JSX converter, be > mindful of what you import at the top of the file. Imports from > `@payloadcms/richtext-lexical/client` or `@payloadcms/ui` will be included in > your bundle. Use `isEditor` checks to keep editor-only code out of the JSX > converter render path. ```tsx { myView: { nodes: { blocks: { myBlockType: { Block: (props) => { if (props.isEditor) { const { BlockCollapsible, EditButton, RemoveButton } = props.useBlockComponentContext() return (

{props.formData?.text}

) } // Frontend render — no editor dependencies return
{props.formData?.text}
}, }, }, }, }, } ``` #### Label `Label` replaces just the block label in the collapsible header. It is **only used in the editor** — it has no effect in JSX converter / frontend rendering. ### Inline Blocks Override specific inline block types: ```tsx { myView: { nodes: { inlineBlocks: { myInlineBlockType: { Component: ({ node }) => Custom inline block, }, }, }, }, } ``` ## Using Views in the Admin Panel When views are defined, a view selector automatically appears next to the field label in the admin panel. Users can switch between views to see how content renders in different modes. ### Accessing Current View Use the `useRichTextView` hook to access the current view: ```tsx 'use client' import { useRichTextView } from '@payloadcms/richtext-lexical/client' function MyCustomComponent() { const { currentView, views, currentViewMap } = useRichTextView() return (

Current view: {currentView}

{/* currentViewMap contains the active view config */} {currentViewMap?.nodes?.heading && (

Custom heading rendering is active

)} {currentViewMap?.admin?.hideGutter && (

Gutter is hidden in this view

)}
) } ``` ## Using View Node Maps with JSX Converters You can pass a view's node map to JSX converters (e.g. the `RichText` component or `convertLexicalToJSX`) to reuse the same rendering logic outside the editor. This is useful when you want the admin panel preview and your site to render content identically. When both `converters` and `nodeMap` are provided, the node map takes precedence per-node-type. For blocks and inline blocks, converters from both sources are deep-merged, with the node map winning when both define a converter for the same block type. ```tsx import { RichText } from '@payloadcms/richtext-lexical/react' import { postViews } from './views' export function BlogPost({ post }) { return (
{/* The view's node map overrides default converters per-node-type */}
) } ``` # Live Preview Source: https://payloadcms.com/docs/beta/live-preview/overview With Live Preview you can render your front-end application directly within the [Admin Panel](/docs/v4/admin/overview.md). As you type, your changes take effect in real-time. No need to save a draft or publish your changes. This works in both [Server-side](/docs/v4/live-preview/server.md) as well as [Client-side](/docs/v4/live-preview/client.md) environments. Live Preview works by rendering an iframe on the page that loads your front-end application. The Admin Panel communicates with your app through [`window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) events. These events are emitted every time a change is made to the Document. Your app then listens for these events and re-renders itself with the data it receives. To add Live Preview, use the `admin.livePreview` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { // ... // highlight-start livePreview: { url: 'http://localhost:3000', collections: ['pages'], }, // highlight-end }, }) ``` > **Reminder:** Alternatively, you can define the `admin.livePreview` property > on individual [Collection Admin > Configs](/docs/v4/configuration/collections.md#admin-options) and [Global Admin > Configs](/docs/v4/configuration/globals.md#admin-options). Settings defined here will > be merged into the top-level as overrides. ## Options Setting up Live Preview is easy. This can be done either globally through the [Root Admin Config](/docs/v4/admin/overview.md), or on individual [Collection Admin Configs](/docs/v4/configuration/collections.md#admin-options) and [Global Admin Configs](/docs/v4/configuration/globals.md#admin-options). Once configured, a new "Live Preview" button will appear at the top of enabled Documents. Toggling this button opens the preview window and loads your front-end application. The following options are available: | Path | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`url`** | String, or function that returns a string, pointing to your front-end application. This value is used as the iframe `src`. [More details](#url). | | **`breakpoints`** | Array of breakpoints to be used as “device sizes” in the preview window. Each item appears as an option in the toolbar. [More details](#breakpoints). | | **`collections`** | Array of collection slugs to enable Live Preview on. | | **`globals`** | Array of global slugs to enable Live Preview on. | ### URL The `url` property resolves to a string that points to your front-end application. This value is used as the `src` attribute of the iframe rendering your front-end. Once loaded, the Admin Panel will communicate directly with your app through `window.postMessage` events. To set the URL, use the `admin.livePreview.url` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { // ... livePreview: { url: 'http://localhost:3000', // highlight-line collections: ['pages'], }, }, }) ``` #### Dynamic URLs You can also pass a function in order to dynamically format URLs. This is useful for multi-tenant applications, localization, or any other scenario where the URL needs to be generated based on the Document being edited. This is also useful for conditionally rendering Live Preview, similar to access control. See [Conditional Rendering](/docs/v4/live-preview/overview.md#conditional-rendering) for more details. To set dynamic URLs, set the `admin.livePreview.url` property in your [Payload Config](/docs/v4/configuration/overview.md) to a function: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { // ... livePreview: { // highlight-start url: ({ data, collectionConfig, locale }) => `${data.tenant.url}${ collectionConfig.slug === 'posts' ? `/posts/${data.slug}` : `${data.slug !== 'home' ? `/${data.slug}` : ''}` }${locale ? `?locale=${locale?.code}` : ''}`, // Localization query param collections: ['pages'], }, // highlight-end }, }) ``` The following arguments are provided to the `url` function: | Path | Description | | ---------------------- | --------------------------------------------------------------------------------------------------------------------- | | **`data`** | The data of the Document being edited. This includes changes that have not yet been saved. | | **`locale`** | The locale currently being edited (if applicable). [More details](/docs/v4/configuration/localization.md). | | **`collectionConfig`** | The Collection Admin Config of the Document being edited. [More details](/docs/v4/configuration/collections.md#admin-options). | | **`globalConfig`** | The Global Admin Config of the Document being edited. [More details](/docs/v4/configuration/globals.md#admin-options). | | **`req`** | The Payload Request object. | You can return either an absolute URL or relative URL from this function. If you don't know the URL of your frontend at build-time, you can return a relative URL, and in that case, Payload will automatically construct an absolute URL by injecting the protocol, domain, and port from your browser window. Returning a relative URL is helpful for platforms like Vercel where you may have preview deployment URLs that are unknown at build time. If your application requires a fully qualified URL, or you are attempting to preview with a frontend on a different domain, you can use the `req` property to build this URL: ```ts url: ({ data, req }) => `${req.protocol}//${req.host}/${data.slug}` ``` #### Conditional Rendering You can conditionally render Live Preview by returning `undefined` or `null` from the `url` function. This is similar to access control, where you may want to restrict who can use Live Preview based on certain criteria, such as the current user or document data. For example, you could check the user's role and only enable Live Preview if they have the appropriate permissions: ```ts url: ({ req }) => (req.user?.role === 'admin' ? '/hello-world' : null) ``` ### Breakpoints The breakpoints property is an array of objects which are used as “device sizes” in the preview window. Each item will render as an option in the toolbar. When selected, the preview window will resize to the exact dimensions specified in that breakpoint. To set breakpoints, use the `admin.livePreview.breakpoints` property in your [Payload Config](/docs/v4/configuration/overview.md): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { // ... livePreview: { url: 'http://localhost:3000', // highlight-start breakpoints: [ { label: 'Mobile', name: 'mobile', width: 375, height: 667, }, ], // highlight-end }, }, }) ``` The following options are available for each breakpoint: | Path | Description | | --------------- | --------------------------------------------------------------------------- | | **`label`** \* | The label to display in the drop-down. This is what the user will see. | | **`name`** \* | The name of the breakpoint. | | **`width`** \* | The width of the breakpoint. This is used to set the width of the iframe. | | **`height`** \* | The height of the breakpoint. This is used to set the height of the iframe. | _\* An asterisk denotes that a property is required._ The "Responsive" option is always available in the drop-down and requires no additional configuration. This is the default breakpoint that will be used on initial load. This option styles the iframe with a width and height of `100%` so that it fills the screen at its maximum size and automatically resizes as the window changes size. You may also explicitly resize the Live Preview by using the corresponding inputs in the toolbar. This will temporarily override the breakpoint selection to "Custom" until a predefined breakpoint is selected once again. If you prefer to freely resize the Live Preview without the use of breakpoints, you can open it in a new window by clicking the button in the toolbar. This will close the iframe and open a new window which can be resized as you wish. Closing it will automatically re-open the iframe. ## Example For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview). # Implementing Live Preview in your frontend Source: https://payloadcms.com/docs/beta/live-preview/frontend There are two ways to use Live Preview in your own application depending on whether your front-end framework supports Server Components: - [Server-side Live Preview (suggested)](/docs/v4/live-preview/server.md) - [Client-side Live Preview](/docs/v4/live-preview/client.md) > **Note** > > We suggest using server-side Live Preview if your framework supports Server > Components, it is both simpler to setup and more performant to run than the > client-side alternative. # Server-side Live Preview Source: https://payloadcms.com/docs/beta/live-preview/server > **Note** > > Server-side Live Preview is only for front-end frameworks that support the > concept of Server Components, i.e. [React Server > Components](https://react.dev/reference/rsc/server-components). If your > front-end application is built with a client-side framework like the [Next.js > Pages Router](https://nextjs.org/docs/pages), [React > Router](https://reactrouter.com), [Vue 3](https://vuejs.org), etc., see > [client-side Live Preview](/docs/v4/live-preview/client.md). Server-side Live Preview works by making a roundtrip to the server every time your document is saved, i.e. draft save, autosave, or publish. While using Live Preview, the Admin Panel emits a new `window.postMessage` event which your front-end application can use to invoke this process. In Next.js, this means simply calling `router.refresh()`; in TanStack Start, calling `router.invalidate()`. Either re-runs the route on the server and hydrates the HTML using new data straight from the [Local API](/docs/v4/local-api/overview.md). > **Warning** > > It is recommended that you enable [Autosave](/docs/v4/versions/autosave.md) alongside > Live Preview to make the experience feel more responsive. If your front-end application is built with [React](#react), you can use the `RefreshRouteOnChange` function that Payload provides. In the future, all other major frameworks like Vue and Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own router refresh component](#building-your-own-router-refresh-component) for more information. ## React If your front-end application is built with server-side [React](https://react.dev) like the [Next.js App Router](https://nextjs.org/docs/app) or [TanStack Start](https://tanstack.com/start/latest), you can use the `RefreshRouteOnSave` component that Payload provides. First, install the `@payloadcms/live-preview-react` package: ```bash npm install @payloadcms/live-preview-react ``` Then, render the `RefreshRouteOnSave` component anywhere in your `page.tsx`. Here's an example: `page.tsx`: ```tsx import { RefreshRouteOnSave } from './RefreshRouteOnSave.tsx' import { headers } from 'next/headers' import { getPayload } from 'payload' import config from '../payload.config' export default async function Page() { const payload = await getPayload({ config }) const { user } = await payload.auth({ headers: await headers() }) const page = await payload.findByID({ collection: 'pages', overrideAccess: false, user, id: '123', draft: true, trash: true, // add this if trash is enabled in your collection and want to preview trashed documents }) return (

{page.title}

) } ``` `RefreshRouteOnSave.tsx`: ```tsx 'use client' import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react' import { useRouter } from 'next/navigation.js' import React from 'react' export const RefreshRouteOnSave: React.FC = () => { const router = useRouter() return ( router.refresh()} serverURL={process.env.NEXT_PUBLIC_PAYLOAD_URL} /> ) } ``` On TanStack Start, swap the router: import `useRouter` from `@tanstack/react-router`, refresh with `router.invalidate()`, and read the server URL from `import.meta.env.VITE_PAYLOAD_URL`: ```tsx 'use client' import { RefreshRouteOnSave as PayloadLivePreview } from '@payloadcms/live-preview-react' import { useRouter } from '@tanstack/react-router' import React from 'react' export const RefreshRouteOnSave: React.FC = () => { const router = useRouter() return ( router.invalidate()} serverURL={import.meta.env.VITE_PAYLOAD_URL} /> ) } ``` ## Building your own router refresh component No matter what front-end framework you are using, you can build your own component using the same underlying tooling that Payload provides. First, install the base `@payloadcms/live-preview` package: ```bash npm install @payloadcms/live-preview ``` This package provides the following functions: | Path | Description | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | **`ready`** | Sends a `window.postMessage` event to the Admin Panel to indicate that the front-end is ready to receive messages. | | **`isDocumentEvent`** | Checks if a `MessageEvent` originates from the Admin Panel and is a document-level event, i.e. draft save, autosave, publish, etc. | With these functions, you can build your own hook using your front-end framework of choice: ```tsx import { ready, isDocumentEvent } from '@payloadcms/live-preview' // To build your own component: // 1. Listen for document-level `window.postMessage` events sent from the Admin Panel // 2. Tell the Admin Panel when it is ready to receive messages // 3. Refresh the route every time a new document-level event is received // 4. Unsubscribe from the `window.postMessage` events when it unmounts ``` Here is an example of what the same `RefreshRouteOnSave` React component from above looks like under the hood: ```tsx 'use client' import type React from 'react' import { isDocumentEvent, ready } from '@payloadcms/live-preview' import { useCallback, useEffect, useRef } from 'react' export const RefreshRouteOnSave: React.FC<{ apiRoute?: string depth?: number refresh: () => void serverURL: string }> = (props) => { const { apiRoute, depth, refresh, serverURL } = props const hasSentReadyMessage = useRef(false) const onMessage = useCallback( (event: MessageEvent) => { if (isDocumentEvent(event, serverURL)) { if (typeof refresh === 'function') { refresh() } } }, [refresh, serverURL], ) useEffect(() => { if (typeof window !== 'undefined') { window.addEventListener('message', onMessage) } if (!hasSentReadyMessage.current) { hasSentReadyMessage.current = true ready({ serverURL, }) } return () => { if (typeof window !== 'undefined') { window.removeEventListener('message', onMessage) } } }, [serverURL, onMessage, depth, apiRoute]) return null } ``` ## Example For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview). There you will find a fully working example of how to implement Live Preview in your Next.js App Router application. ## Troubleshooting #### Updates do not appear as fast as client-side Live Preview If you are noticing that updates feel less snappy than client-side Live Preview (i.e. the `useLivePreview` hook), this is because of how the two differ in how they work—instead of emitting events against _form state_, server-side Live Preview refreshes the route after a new document is _saved_. Use [Autosave](/docs/v4/versions/autosave.md) to mimic this effect server-side. Try decreasing the value of `versions.autoSave.interval` to make the experience feel more responsive: ```ts // collection.ts { versions: { drafts: { autosave: { interval: 375, }, }, }, } ``` #### Iframe refuses to connect If your front-end application has set a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) that blocks the Admin Panel from loading your front-end application, the iframe will not be able to load your site. To resolve this, you can whitelist the Admin Panel's domain in your CSP by setting the `frame-ancestors` directive: ```plaintext frame-ancestors: "self" localhost:* https://your-site.com; ``` # Client-side Live Preview Source: https://payloadcms.com/docs/beta/live-preview/client > **Note** > > If your front-end application supports Server Components like the [Next.js App > Router](https://nextjs.org/docs/app), etc., we suggest setting up [server-side > Live Preview](/docs/v4/live-preview/server.md) instead. While using Live Preview, the [Admin Panel](/docs/v4/admin/overview.md) emits a new `window.postMessage` event every time your document has changed. Your front-end application can listen for these events and re-render accordingly. If your front-end application is built with [React](#react) or [Vue](#vue), use the `useLivePreview` hooks that Payload provides. In the future, all other major frameworks like Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own hook](#building-your-own-hook) for more information. By default, all hooks accept the following args: | Path | Description | | ------------------ | -------------------------------------------------------------------------------------- | | **`serverURL`** \* | The URL of your Payload server. | | **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. | | **`depth`** | The depth of the relationships to fetch. Defaults to `0`. | | **`apiRoute`** | The path of your API route as defined in `routes.api`. Defaults to `/api`. | _\* An asterisk denotes that a property is required._ And return the following values: | Path | Description | | --------------- | ---------------------------------------------------------------- | | **`data`** | The live data of the document, merged with the initial data. | | **`isLoading`** | A boolean that indicates whether or not the document is loading. | > **Note** > > If your front-end is tightly coupled to required fields, you should ensure > that your UI does not break when these fields are removed. For example, if you > are rendering something like `data.relatedPosts[0].title`, your page will > break once you remove the first related post. To get around this, use > conditional logic, optional chaining, or default values in your UI where > needed. For example, `data?.relatedPosts?.[0]?.title`. > **Note** > > It is important that the `depth` argument matches exactly with the depth of > your initial page request. The depth property is used to populated > relationships and uploads beyond their IDs. See [Depth](/docs/v4/queries/depth.md) for > more information. ## Frameworks Live Preview will work with any front-end framework that supports the native [`window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) API. By default, Payload officially supports the most popular frameworks, including: - [React](#react) - [Vue](#vue) If your framework is not listed, you can still integrate with Live Preview using the underlying tooling that Payload provides. [More details](#building-your-own-hook). ### React If your front-end application is built with client-side [React](https://react.dev) like [Next.js Pages Router](https://nextjs.org/docs/pages), you can use the `useLivePreview` hook that Payload provides. First, install the `@payloadcms/live-preview-react` package: ```bash npm install @payloadcms/live-preview-react ``` Then, use the `useLivePreview` hook in your React component: ```tsx 'use client' import { useLivePreview } from '@payloadcms/live-preview-react' import { Page as PageType } from '@/payload-types' // Fetch the page in a server component, pass it to the client component, then thread it through the hook // The hook will take over from there and keep the preview in sync with the changes you make // The `data` property will contain the live data of the document export const PageClient: React.FC<{ page: { title: string } }> = ({ page: initialPage }) => { const { data } = useLivePreview({ initialData: initialPage, serverURL: PAYLOAD_SERVER_URL, depth: 2, }) return

{data.title}

} ``` > **Reminder:** If you are using [React Server > Components](https://react.dev/reference/rsc/server-components), we strongly > suggest setting up [server-side Live Preview](/docs/v4/live-preview/server.md) instead. ### Vue If your front-end application is built with [Vue 3](https://vuejs.org) or [Nuxt 3](https://nuxt.js), you can use the `useLivePreview` composable that Payload provides. First, install the `@payloadcms/live-preview-vue` package: ```bash npm install @payloadcms/live-preview-vue ``` Then, use the `useLivePreview` hook in your Vue component: ```ts ``` ## Building your own hook No matter what front-end framework you are using, you can build your own hook using the same underlying tooling that Payload provides. First, install the base `@payloadcms/live-preview` package: ```bash npm install @payloadcms/live-preview ``` This package provides the following functions: | Path | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------ | | **`subscribe`** | Subscribes to the Admin Panel's `window.postMessage` events and calls the provided callback function. | | **`unsubscribe`** | Unsubscribes from the Admin Panel's `window.postMessage` events. | | **`ready`** | Sends a `window.postMessage` event to the Admin Panel to indicate that the front-end is ready to receive messages. | | **`isLivePreviewEvent`** | Checks if a `MessageEvent` originates from the Admin Panel and is a Live Preview event, i.e. debounced form state. | The `subscribe` function takes the following args: | Path | Description | | ------------------ | ------------------------------------------------------------------------------------------- | | **`callback`** \* | A callback function that is called with `data` every time a change is made to the document. | | **`serverURL`** \* | The URL of your Payload server. | | **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. | | **`depth`** | The depth of the relationships to fetch. Defaults to `0`. | With these functions, you can build your own hook using your front-end framework of choice: ```tsx import { subscribe, unsubscribe } from '@payloadcms/live-preview' // To build your own hook, subscribe to Live Preview events using the `subscribe` function // It handles everything from: // 1. Listening to `window.postMessage` events // 2. Merging initial data with active form state // 3. Populating relationships and uploads // 4. Calling the `onChange` callback with the result // Your hook should also: // 1. Tell the Admin Panel when it is ready to receive messages // 2. Handle the results of the `onChange` callback to update the UI // 3. Unsubscribe from the `window.postMessage` events when it unmounts ``` Here is an example of what the same `useLivePreview` React hook from above looks like under the hood: ```tsx import { subscribe, unsubscribe, ready } from '@payloadcms/live-preview' import { useCallback, useEffect, useState, useRef } from 'react' export const useLivePreview = (props: { depth?: number initialData: T serverURL: string }): { data: T isLoading: boolean } => { const { depth = 0, initialData, serverURL } = props const [data, setData] = useState(initialData) const [isLoading, setIsLoading] = useState(true) const hasSentReadyMessage = useRef(false) const onChange = useCallback((mergedData) => { // When a change is made, the `onChange` callback will be called with the merged data // Set this merged data into state so that React will re-render the UI setData(mergedData) setIsLoading(false) }, []) useEffect(() => { // Listen for `window.postMessage` events from the Admin Panel // When a change is made, the `onChange` callback will be called with the merged data const subscription = subscribe({ callback: onChange, depth, initialData, serverURL, }) // Once subscribed, send a `ready` message back up to the Admin Panel // This will indicate that the front-end is ready to receive messages if (!hasSentReadyMessage.current) { hasSentReadyMessage.current = true ready({ serverURL, }) } // When the component unmounts, unsubscribe from the `window.postMessage` events return () => { unsubscribe(subscription) } }, [serverURL, onChange, depth, initialData]) return { data, isLoading, } } ``` > **Note** > > When building your own hook, ensure that the args and return values are > consistent with the ones listed at the top of this document. This will ensure > that all hooks follow the same API. ## Example For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview). There you will find an example of a fully integrated Next.js App Router front-end that runs on the same server as Payload. ## Troubleshooting #### Relationships and/or uploads are not populating If you are using relationships or uploads in your front-end application, and your front-end application runs on a different domain than your Payload server, you may need to configure [CORS](/docs/v4/configuration/overview.md#cors) to allow requests to be made between the two domains. This includes sites that are running on a different port or subdomain. Similarly, if you are protecting resources behind user authentication, you may also need to configure [CSRF](/docs/v4/authentication/cookies.md#csrf-prevention) to allow cookies to be sent between the two domains. For example: ```ts // payload.config.ts { // ... // If your site is running on a different domain than your Payload server, // This will allow requests to be made between the two domains cors: [ 'http://localhost:3001' // Your front-end application ], // If you are protecting resources behind user authentication, // This will allow cookies to be sent between the two domains csrf: [ 'http://localhost:3001' // Your front-end application ], } ``` #### Relationships and/or uploads disappear after editing a document It is possible that either you are setting an improper [`depth`](/docs/v4/queries/depth.md) in your initial request and/or your `useLivePreview` hook, or they're mismatched. Ensure that the `depth` parameter is set to the correct value, and that it matches exactly in both places. For example: ```tsx import { headers } from 'next/headers' // Your initial request const { user } = await payload.auth({ headers: await headers() }) const { docs } = await payload.find({ collection: 'pages', overrideAccess: false, user, depth: 1, // Ensure this is set to the proper depth for your application where: { slug: { equals: 'home', }, }, }) ``` ```tsx // Your hook const { data } = useLivePreview({ initialData: initialPage, serverURL: PAYLOAD_SERVER_URL, depth: 1, // Ensure this matches the depth of your initial request }) ``` #### Iframe refuses to connect If your front-end application has set a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) that blocks the Admin Panel from loading your front-end application, the iframe will not be able to load your site. To resolve this, you can whitelist the Admin Panel's domain in your CSP by setting the `frame-ancestors` directive: ```plaintext frame-ancestors: "self" localhost:* https://your-site.com; ``` # Versions Source: https://payloadcms.com/docs/beta/versions/overview > **Note** > > Payload's powerful Versions functionality allows you to keep a running history > of changes over time and extensible to fit any content publishing workflow. When enabled, Payload will automatically scaffold a new Collection in your database to store versions of your document(s) over time, and the Admin UI will be extended with additional views that allow you to browse document versions, view diffs in order to see exactly what has changed in your documents (and when they changed), and restore documents back to prior versions easily. ![Versions](/images/docs/versions-v3.jpg) _Comparing an old version to a newer version of a document_ **With Versions, you can:** - Maintain an audit log / history of every change ever made to a document, including monitoring for what user made which change - Restore documents and globals to prior states in case you need to roll back changes - Build a true [Draft Preview](/docs/v4/versions/drafts.md) mode for your data - Manage who can see Drafts, and who can only see Published documents via [Access Control](/docs/v4/access-control/overview.md) - Enable [Autosave](/docs/v4/versions/autosave.md) on collections and globals to never lose your work again - Build a powerful publishing schedule mechanism to create documents and have them become publicly readable automatically at a future date > **Success** > > Versions are extremely performant. They don't change the shape of your data at > all. All versions are stored in a separate collection or global. Versions are{' '} > enabled by default for every collection and global — set{' '} > versions: false to opt out. ## Options Versions support a few different levels of functionality that each come with their own impacts to document workflow. ### Versions enabled, drafts disabled If you enable versions but keep draft mode disabled, Payload will simply create a new version of a document each time you update a document. This is great for use cases where you need to retain a history of all document updates over time, but always want to treat the newest document version as the version that is "published". For example, a use case for "versions enabled, drafts disabled" could be on a collection of users, where you might want to keep a version history (or audit log) of all changes ever made to users - but any changes to users should _always_ be treated as "published" and you have no need to maintain a "draft" version of a user. ### Versions and drafts enabled If you have versions _and_ drafts enabled, you are able to control which documents are published, and which are considered draft. That lets you write [Access Control](/docs/v4/access-control/overview.md) to control who can see published documents, and who can see draft documents. It also lets you save versions (drafts) that are _newer_ than your most recently published document, which is helpful if you want to draft changes and maybe even preview them before you publish the changes. Read more about Drafts [here](/docs/v4/versions/drafts.md). ### Versions, drafts, and autosave enabled When you have versions, drafts, _and_ `autosave` enabled, the Admin UI will automatically save changes that you make to a new `draft` version as you edit a document, which makes sure that you never lose your changes ever again. Autosave will not affect your published post at all—instead, it'll just save your changes and let you publish them whenever you or your editors are ready to do so. Read more about Autosave [here](/docs/v4/versions/autosave.md). ## Collection config Versions are **enabled by default** for every collection (`maxPerDoc: 100`, drafts disabled). You only need to set the `versions` key explicitly when you want to opt out or customise the behaviour: ```ts // Opt out entirely export const Users: CollectionConfig = { slug: 'users', auth: true, fields: [], versions: false, } // Customise — enable drafts and cap history to 50 versions per document export const Posts: CollectionConfig = { slug: 'posts', fields: [], versions: { drafts: { autosave: true }, maxPerDoc: 50, }, } ``` The available options are: | Option | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `maxPerDoc` | Use this setting to control how many versions to keep on a document by document basis. Must be an integer. Defaults to 100, use 0 to save all versions. | | `drafts` | Enable [Drafts](/docs/v4/versions/drafts.md) mode for this collection. To enable, set to `true` or pass an object with `draft` [options](/docs/v4/versions/drafts.md#options). | ## Global config Global versions are **enabled by default** (`max: 100`, drafts disabled), the same as collections. Set `versions: false` to opt out, or pass an object to customise: ```ts // Opt out entirely export const Header: GlobalConfig = { slug: 'header', fields: [], versions: false, } ``` The available options differ slightly from collections — globals use `max` instead of `maxPerDoc`: | Option | Description | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `max` | Use this setting to control how many versions to keep on a global by global basis. Must be an integer. Defaults to 100, use 0 to save all versions. | | `drafts` | Enable [Drafts](/docs/v4/versions/drafts.md) mode for this global. To enable, set to `true` or pass an object with `draft` [options](/docs/v4/versions/drafts.md#options) | ### Database impact By enabling `versions`, a new database collection will be made to store versions for your collection or global. The collection will be named based off the `slug` of the collection or global and will follow this pattern (where `slug` is replaced with the `slug` of your collection or global): ``` _slug_versions ``` Each document in this new `versions` collection will store a set of meta properties about the version as well as a _full_ copy of the document. For example, a version's data might look like this for a Collection document: ```json { "_id": "61cf752c19cdf1b1af7b61f1", // a unique ID of this version "parent": "61ce1354091d5b3ffc20ea6e", // the ID of the parent document "autosave": false, // used to denote if this version was created via autosave "version": { // your document's data goes here // all fields are set to not required and this property can be partially complete }, "createdAt": "2021-12-31T21:25:00.992+00:00", "updatedAt": "2021-12-31T21:25:00.992+00:00" } ``` Global versions are stored the same as the collection version shown above, except they do not feature the `parent` property, as each Global receives its own `versions` collection. That means we know that all versions in that collection correspond to that specific global. ## Version operations Versions expose new operations for both collections and globals. They allow you to find and query versions, find a single version by ID, and publish (or restore) a version by ID. Both Collections and Globals support the same new operations. They are used primarily by the admin UI, but if you are writing custom logic in your app and would like to utilize them, they're available for you to use as well via REST, GraphQL, and Local APIs. **Collection REST endpoints:** | Method | Path | Description | | ------ | ------------------------------------ | --------------------------------- | | `GET` | `/api/{collectionSlug}/versions` | Find and query paginated versions | | `GET` | `/api/{collectionSlug}/versions/:id` | Find a specific version by ID | | `POST` | `/api/{collectionSlug}/versions/:id` | Restore a version by ID | **Collection GraphQL queries:** | Query Name | Operation | | ---------------------------------------- | ----------------- | | **`version{collection.label.singular}`** | `findVersionByID` | | **`versions{collection.label.plural}`** | `findVersions` | **And mutation:** | Query Name | Operation | | ----------------------------------------------- | ---------------- | | **`restoreVersion{collection.label.singular}`** | `restoreVersion` | **Collection Local API methods:** ### Find ```js // Result will be a paginated set of Versions. // See /docs/queries/pagination for more. const result = await payload.findVersions({ collection: 'posts', // required depth: 2, page: 1, limit: 10, where: {}, // pass a `where` query here sort: '-createdAt', locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Find by ID ```js // Result will be a Post document. const result = await payload.findVersionByID({ collection: 'posts', // required id: '507f1f77bcf86cd799439013', // required depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Restore ```js // Result will be the restored global document. const result = await payload.restoreVersion({ collection: 'posts', // required id: '507f1f77bcf86cd799439013', // required depth: 2, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` **Global REST endpoints:** | Method | Path | Description | | ------ | ---------------------------------------- | --------------------------------- | | `GET` | `/api/globals/{globalSlug}/versions` | Find and query paginated versions | | `GET` | `/api/globals/{globalSlug}/versions/:id` | Find a specific version by ID | | `POST` | `/api/globals/{globalSlug}/versions/:id` | Restore a version by ID | **Global GraphQL queries:** | Query Name | Operation | | ---------------------------- | ----------------- | | **`version{global.label}`** | `findVersionByID` | | **`versions{global.label}`** | `findVersions` | **Global GraphQL mutation:** | Query Name | Operation | | ---------------------------------- | ---------------- | | **`restoreVersion{global.label}`** | `restoreVersion` | **Global Local API methods:** ### Find ```js // Result will be a paginated set of Versions. // See /docs/queries/pagination for more. const result = await payload.findGlobalVersions({ slug: 'header', // required depth: 2, page: 1, limit: 10, where: {}, // pass a `where` query here sort: '-createdAt', locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Find by ID ```js // Result will be a Post document. const result = await payload.findGlobalVersionByID({ slug: 'header', // required id: '507f1f77bcf86cd799439013', // required depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Restore ```js // Result will be the restored global document. const result = await payload.restoreGlobalVersion({ slug: 'header', // required id: '507f1f77bcf86cd799439013', // required depth: 2, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ## Access Control Versions expose a new [Access Control](/docs/v4/access-control/overview.md) function on both [Collections](/docs/v4/configuration/collections.md) and [Globals](/docs/v4/configuration/globals.md) that allow for you to control who can see versions of documents, and who can't. | Function | Allows/Denies Access | | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | | **`readVersions`** | Used to control who can read versions, and who can't. Will automatically restrict the Admin UI version viewing access. | For full details on how to use Access Control with Versions, see the [Access Control](/docs/v4/access-control/overview.md) documentation. # Drafts Source: https://payloadcms.com/docs/beta/versions/drafts Payload's Draft functionality builds on top of the Versions functionality to allow you to make changes to your collection documents and globals, but publish only when you're ready. This functionality allows you to build powerful Preview environments for your data, where you can make sure your changes look good before publishing documents. > **Warning** > > Drafts rely on Versions being enabled in order to function. By enabling Versions with Drafts, your collections and globals can maintain _newer_, and _unpublished_ versions of your documents. It's perfect for cases where you might want to work on a document, update it and save your progress, but not necessarily make it publicly published right away. Drafts are extremely helpful when building preview implementations. ![Drafts Enabled](/images/docs/autosave-drafts.jpg) _If Drafts are enabled, the typical Save button is replaced with new actions which allow you to either save a draft, or publish your changes._ ## Options Collections and Globals both support the same options for configuring drafts. You can either set `versions.drafts` to `true`, or pass an object to configure draft properties. | Draft Option | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `autosave` | Enable `autosave` to automatically save progress while documents are edited. To enable, set to `true` or pass an object with [options](/docs/v4/versions/autosave.md). | | `schedulePublish` | Allow for editors to schedule publish / unpublish events in the future. [More](#scheduled-publish) | | `validate` | Set `validate` to `true` to validate draft documents when saved. Default is `false`. | ## Database changes By enabling drafts on a collection or a global, Payload will **automatically inject a new field into your schema** called `_status`. The `_status` field is used internally by Payload to store if a document is set to `draft` or `published`. **Admin UI status indication** Within the Admin UI, if drafts are enabled, a document can be shown with one of three "statuses": 1. **Draft** - if a document has never been published, and only draft versions of the document are present 1. **Published** - if a document is published and there are no newer drafts available 1. **Changed** - if a document has been published, but there are newer drafts available and not yet published ## Draft API > **Success** > > If drafts are enabled on your collection or global, important and powerful > changes are made to your REST, GraphQL, and Local APIs that allow you to > specify if you are interacting with drafts or with live documents. #### Using the `draft` parameter When drafts are enabled, the `create`, `update`, `find`, and `findByID` operations for REST, GraphQL, and Local APIs expose a `draft` parameter. For write operations, it controls validation and where data is written. For read operations, it determines whether to return draft versions. ```ts // REST API POST /api/your-collection?draft=true // Local API await payload.create({ collection: 'your-collection', overrideAccess: true, data: { // your data here }, draft: true, }) // GraphQL mutation { createYourCollection(data: { ... }, draft: true) { // ... } } ``` **Understanding `draft` parameter and `_status` field** The `draft` parameter and `_status` field work together but serve different purposes: **`draft` parameter** - Controls two things: 1. **Validation**: When `draft: true`, required fields are not enforced, allowing you to save incomplete documents 2. **Write location**: Determines whether the main collection document is updated - `draft: true` - Saves ONLY to versions table (main collection unchanged) - `draft: false` or omitted - Saves to BOTH main collection AND versions table **`_status` field** - Indicates whether a document is published or in draft state - Defaults to `'draft'` when not explicitly provided - Can be explicitly set in your data to `'published'` or `'draft'` **First document creation** When you first create a document, it's always written to the main collection (since no document exists yet): - If you don't specify `_status`, it defaults to `_status: 'draft'` - A version is also created in the versions table - The `draft` parameter controls validation but doesn't change where the initial document is written **Subsequent updates** After initial creation, the `draft` parameter controls where your updates are written: - **`draft: true`** - Only the versions table is updated; the main collection document remains unchanged - **`draft: false` or omitted** - Both the main collection and versions table are updated **Important:** The `draft` parameter does NOT control whether a document is published or not. A document remains with `_status: 'draft'` by default unless you explicitly set `_status: 'published'` in your data. **Publishing a document** To publish a document, you must explicitly set `_status: 'published'` in your data. When you do this: - If you use `draft: false` or omit it, the main collection will be updated with the published status - If you use `draft: true`, the `_status: 'published'` takes precedence and will still update the main collection as published (overriding the `draft: true` behavior) **Quick reference** | Operation | `draft` param | `_status` in data | Result | | --------- | ------------------ | -------------------- | -------------------------------------------------------------- | | Create | `true` or `false` | omitted | Main collection updated with `_status: 'draft'` | | Create | `true` or `false` | `'published'` | Main collection updated with `_status: 'published'` | | Update | `true` | omitted or `'draft'` | Only versions table updated, main collection unchanged | | Update | `true` | `'published'` | Main collection updated with `_status: 'published'` (override) | | Update | `false` or omitted | omitted | Main collection updated with `_status: 'draft'` | | Update | `false` or omitted | `'published'` | Main collection updated with `_status: 'published'` | **Required fields** Setting `_status: "draft"` will not bypass required field validation. You need to set `draft: true` to save incomplete documents as shown in the previous examples. #### Reading drafts vs. published documents In addition to the `draft` argument within `create` and `update` operations, a `draft` argument is also exposed for `find` and `findByID` operations. When `draft` is set to `true` while reading a document, **Payload will return the most recent version from the versions table**, regardless of whether it's a draft or published document. **Example scenario:** 1. You create a document with `_status: 'published'` (published in main collection) 1. You update with `draft: true` to make changes without affecting the published version 1. You update again with `draft: true` to make more draft changes At this point, your published document remains unchanged in the main collection, and you have two newer draft versions in the `_[collectionSlug]_versions` table. When you fetch the document with a standard `find` or `findByID` operation, the published document from the main collection is returned and draft versions are ignored. However, if you pass `draft: true` to the read operation, Payload will return the most recent version from the versions table. In the scenario above with two draft versions, you'll get the latest (second) draft. **Note:** If there are no newer drafts (e.g., you published a document and haven't made draft changes since), querying with `draft: true` will still return the latest version from the versions table, which would be the same published content as in the main collection. > **Important:** the `draft` argument on its own will not restrict documents > with `_status: 'draft'` from being returned from the API. You need to use > Access Control to prevent documents with `_status: 'draft'` from being > returned to unauthenticated users. Read below for more information on how this > works. ## Controlling who can see Collection drafts > **Warning** > > If you're using the **drafts** feature, it's important for you to consider who > can view your drafts, and who can view only published documents. Luckily, > Payload makes this extremely simple and puts the power completely in your > hands. #### Restricting draft access You can use the `read` [Access Control](/docs/v4/access-control/collections.md#read) method to restrict who is able to view drafts of your documents by simply returning a [query constraint](/docs/v4/queries/overview.md) which restricts the documents that any given user is able to retrieve. Here is an example that utilizes the `_status` field to require a user to be logged in to retrieve drafts: ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', access: { read: ({ req }) => { // If there is a user logged in, // let them retrieve all documents if (req.user) return true // If there is no user, // restrict the documents that are returned // to only those where `_status` is equal to `published` return { _status: { equals: 'published', }, } }, }, versions: { drafts: true, }, //.. the rest of the Pages config here } ``` > **Warning** > > **Note regarding adding versions to an existing collection** > > If you already have a collection with documents, and you _opt in_ to draft functionality after you have already created existing documents, all of your old documents \_will not have a `_status` field\_ until you resave them. For this reason, if you are \_adding\_ versions into an existing collection, you might want to write your Access Control function to allow for users to read both documents where **`_status` is equal to `"published"`** as well as where **`_status` does not exist**. Here is an example for how to write an [Access Control](/docs/v4/access-control/overview.md) function that grants access to both documents where `_status` is equal to "published" and where `_status` does not exist: ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', access: { read: ({ req }) => { // If there is a user logged in, // let them retrieve all documents if (req.user) return true // If there is no user, // restrict the documents that are returned // to only those where `_status` is equal to `published` // or where `_status` does not exist return { or: [ { _status: { equals: 'published', }, }, { _status: { exists: false, }, }, ], } }, }, versions: { drafts: true, }, //.. the rest of the Pages config here } ``` ## Controlling who can publish You can use `update` [Access Control](/docs/v4/access-control/collections.md#update) to restrict who can publish documents by returning a [query constraint](/docs/v4/queries/overview.md) on the `_status` field. When this constraint prevents publishing, the Admin UI automatically hides the Publish and Unpublish buttons. Here is an example where admins can publish, but editors can only save drafts: ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', access: { update: ({ req: { user } }) => { if (!user) { // If there is no user, they can't update anything return false } // Admins can update any document, including publishing if (user?.role === 'admin') return true // Editors can only update documents that are not published. return { _status: { equals: 'draft', }, } }, }, versions: { drafts: true, }, } ``` The `update` function returns a query constraint instead of a boolean. Payload appends this constraint to the update query, so only documents where `_status` is not `published` can be updated by editors. This also blocks scheduled publish jobs from executing for those users. The `create` function uses a `data` check instead since there is no existing document to query against. ## Scheduled publish Payload provides for an ability to schedule publishing / unpublishing events in the future, which can be helpful if you need to set certain documents to "go live" at a given date in the future, or, vice versa, revert to a draft state after a certain time has passed. You can enable this functionality on both collections and globals via the `versions.drafts.schedulePublish: true` property. > **Important:** if you are going to enable scheduled publish / unpublish, you > need to make sure your Payload app is set up to process > [Jobs](/docs/v4/jobs-queue/overview.md). This feature works by creating a Job in the > background, which will be picked up after the job becomes available. If you do > not have any mechanism in place to run jobs, your scheduled publish / > unpublish jobs will never be executed. ## Unpublishing drafts If a document is published, the Payload Admin UI will be updated to show an "unpublish" button at the top of the sidebar, which will "unpublish" the currently published document. Consider this as a way to "revert" a document back to a draft state. On the API side, this is done by simply setting `_status: 'draft'` on any document. ## Reverting to published If a document is published, and you have made further changes which are saved as a draft, Payload will show a "revert to published" button at the top of the sidebar which will allow you to reject your draft changes and "revert" back to the published state of the document. Your drafts will still be saved, but a new version will be created that will reflect the last published state of the document. # Autosave Source: https://payloadcms.com/docs/beta/versions/autosave Extending on Payload's [Draft](/docs/v4/versions/drafts.md) functionality, you can configure your collections and globals to autosave changes as drafts, and publish only you're ready. The Admin UI will automatically adapt to autosaving progress at an interval that you define, and will store all autosaved changes as a new Draft version. Never lose your work - and publish changes to the live document only when you're ready. > **Warning** > > Autosave relies on Versions and Drafts being enabled in order to function. ![Autosave Enabled](/images/docs/autosave-v3.jpg) _If Autosave is enabled, drafts will be created automatically as the document is modified and the Admin UI adds an indicator describing when the document was last saved to the top right of the sidebar._ ## Options Collections and Globals both support the same options for configuring autosave. You can either set `versions.drafts.autosave` to `true`, or pass an object to configure autosave properties. | Drafts Autosave Options | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `800`. | | `showSaveDraftButton` | Set this to `true` to show the "Save as draft" button even while autosave is enabled. Defaults to `false`. | **Example config with versions, drafts, and autosave enabled:** ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', access: { read: ({ req }) => { // If there is a user logged in, // let them retrieve all documents if (req.user) return true // If there is no user, // restrict the documents that are returned // to only those where `_status` is equal to `published` return { _status: { equals: 'published', }, } }, }, versions: { drafts: { autosave: true, // Alternatively, you can specify an object to customize autosave: // autosave: { // Define how often the document should be autosaved (in milliseconds) // interval: 1500, // // Show the "Save as draft" button even while autosave is enabled // showSaveDraftButton: true, // }, }, }, //.. the rest of the Pages config here } ``` ## Autosave API When `autosave` is enabled, all `update` operations within Payload expose a new argument called `autosave`. When set to `true`, Payload will treat the incoming draft update as an `autosave`. This is primarily used by the Admin UI, but there may be some cases where you are building an app for your users and wish to implement `autosave` in your own app. To do so, use the `autosave` argument in your `update` operations. ### How autosaves are stored If we created a new version for each autosave, you'd quickly find a ton of autosaves that clutter up your `_versions` collection within the database. That would be messy quick because `autosave` is typically set to save a document at ~800ms intervals. > **Success** > > Instead of creating a new version each time a document is autosaved, Payload > smartly only creates **one** autosave version, and then updates that specific > version with each autosave performed. This makes sure that your versions > remain nice and tidy. # Uploads Source: https://payloadcms.com/docs/beta/upload/overview > **Note** > > Payload provides everything you need to enable file upload, storage, and > management directly on your server—including extremely powerful file [access > control](#access-control). ![Shows an Upload enabled collection in the Payload Admin Panel](https://payloadcms.com/images/docs/uploads-overview.jpg) _Admin Panel screenshot depicting a Media Collection with Upload enabled_ **Here are some common use cases of Uploads:** - Creating a "Media Library" that contains images for use throughout your site or app - Building a Gated Content library where users need to sign up to gain access to downloadable assets like ebook PDFs, whitepapers, etc. - Storing publicly available, downloadable assets like software, ZIP files, MP4s, etc. **By simply enabling Upload functionality on a Collection, Payload will automatically transform your Collection into a robust file management / storage solution. The following modifications will be made:** 1. `filename`, `mimeType`, and `filesize` fields will be automatically added to your Collection. Optionally, if you declare image sizes as `variants` on [`sharpTransformer`](#variants), a [`variants`](#variants) group will also be added containing auto-resized image sizes and filenames. 1. The Admin Panel will modify its built-in `List` component to show a thumbnail for each upload within the List View 1. The Admin Panel will modify its `Edit` view(s) to add a new set of corresponding Upload UI which will allow for file upload 1. The `create`, `update`, and `delete` Collection operations will be modified to support file upload, re-upload, and deletion ## Enabling Uploads Every Payload Collection can opt-in to supporting Uploads by specifying the `upload` property on the Collection's config to either `true` or to an object containing `upload` options. > **Tip:** > > A common pattern is to create a **"media"** collection and enable **upload** on that collection. ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { staticDir: 'media', adminThumbnail: 'thumbnail', mimeTypes: ['image/*'], }, fields: [ { name: 'alt', type: 'text', }, ], } ``` > **Note:** Image resizing (image sizes, `resizeOptions`, and related Sharp > options) is configured through > [`@payloadcms/transformer-sharp`](/docs/v4/upload/transformers.md#payloadcmstransformer-sharp), > not on the collection's own `upload` config. See [Variants](#variants) below. ### Collection Upload Options _An asterisk denotes that an option is required._ | Option | Description | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`adminThumbnail`** | Set the way that the [Admin Panel](/docs/v4/admin/overview.md) will display thumbnails for this Collection. [More](#admin-thumbnails) | | **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true | | **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. | | **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](/docs/v4/admin/overview.md). Crop is enabled by default. For a collection configured on `sharpTransformer`, a `crop` set there replaces this value. [More](#crop-and-focal-point-selector) | | **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) | | **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](/docs/v4/fields/upload.md#config-options). | | **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. If using this option, you should handle the removal of any sensitive cookies (like payload-prefixed cookies) to prevent leaking session information to external services. By default, Payload automatically filters out payload-prefixed cookies when this option is not defined. | | **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. | | **`filenameCompoundIndex`** | Field slugs to use for a compound index instead of the default filename index. | | **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](/docs/v4/admin/overview.md). The selector is shown when a registered transformer configures image sizes or image adjustments (`resizeOptions`, `formatOptions`, and so on) for this collection, or when this is set to `true`. For a collection configured on `sharpTransformer`, a `focalPoint` set there replaces this value. [More](#crop-and-focal-point-selector) | | **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. | | **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) | | **`pasteURL`** | Controls whether files can be uploaded from remote URLs by pasting them into the Upload field. **Enabled by default.** Accepts `false` to disable or an object with an `allowList` of valid remote URLs. [More](#uploading-files-from-remote-urls) | | **`skipSafeFetch`** | Set to an `allowList` to skip the safe fetch check when fetching external files. Set to `true` to skip the safe fetch for all documents in this collection. Defaults to `false`. | | **`allowRestrictedFileTypes`** | Set to `true` to allow restricted file types. If your Collection has defined [mimeTypes](#mimetypes), restricted file verification will be skipped. Defaults to `false`. [More](#restricted-file-types) | | **`staticDir`** | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. Defaults to your collection slug | | **`hideFileInputOnCreate`** | Set to `true` to prevent the admin UI from showing file inputs during document creation, useful for programmatic file generation. | | **`hideRemoveFile`** | Set to `true` to prevent the admin UI having a way to remove an existing file while editing. | | **`modifyResponseHeaders`** | Accepts an object with existing `headers` and allows you to manipulate the response headers for media files. [More](#modifying-response-headers) | > **Note:** Sharp-specific options — `constructorOptions`, `formatOptions`, > `resizeOptions`, `trimOptions`, and `withMetadata` — are no longer set here. > They're configured through `sharpTransformer({collections})`. See [File > Transformers](/docs/v4/upload/transformers.md#payloadcmstransformer-sharp). ### Payload-wide Upload Options Upload options are specifiable on a Collection by Collection basis, you can also control app wide options by passing your base Payload Config an `upload` property containing an object supportive of all `Busboy` configuration options. | Option | Description | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`abortOnLimit`** | A boolean that, if `true`, returns HTTP 413 if a file exceeds the file size limit. If `false`, the file is truncated. Defaults to `true`. | | **`createParentPath`** | Set to `true` to automatically create a directory path when moving files from a temporary directory or buffer. Defaults to `false`. | | **`debug`** | A boolean that turns upload process logging on if `true`, or off if `false`. Useful for troubleshooting. Defaults to `false`. | | **`limitHandler`** | A function which is invoked if the file is greater than configured limits. | | **`parseNested`** | Set to `true` to turn `req.body` and `req.files` into nested structures. By default `req.body` and `req.files` are flat objects. Defaults to `false`. | | **`preserveExtension`** | Preserves file extensions with the `safeFileNames` option. Limits file names to 3 characters if `true` or a custom length if a `number`, trimming from the start of the extension. | | **`requestSizeLimit`** | Maximum size in bytes for the complete raw multipart request, including files, fields, headers, and boundaries. Accepts a non-negative safe integer or `Infinity`. Defaults to 50 MiB. Set to `Infinity` to disable this limit. | | **`responseOnLimit`** | A `string` that is sent in the Response to a client if the file size limit is exceeded when used with `abortOnLimit`. | | **`safeFileNames`** | Set to `true` to strip non-alphanumeric characters except dashes and underscores. Can also be set to a regex to determine what to strip. Defaults to `false`. | | **`transformers`** | An ordered array of [file transformers](/docs/v4/upload/transformers.md), such as `sharpTransformer()`, that process uploads and serve dynamically-transformed files. Registering any transformer changes the file endpoint — see [File Transformers](/docs/v4/upload/transformers.md#registering-a-transformer-changes-the-file-endpoint). | | **`tempFileDir`** | A `string` path to store temporary files used when the `useTempFiles` option is set to `true`. Defaults to `'tmp'` in the current working directory. Supports absolute paths. | | **`uploadTimeout`** | A `number` that defines how long to wait for data before aborting, specified in milliseconds. Set to `0` to disable timeout checks. Defaults to `60000`. | | **`uriDecodeFileNames`** | Set to `true` to apply uri decoding to file names. Defaults to `false`. | | **`useTempFiles`** | Set to `true` to store files to a temporary directory instead of in RAM, reducing memory usage for large files or many files. | [Click here](https://github.com/mscdex/busboy#api) for more documentation about what you can control with `Busboy`. Multipart requests default to `requestSizeLimit: 50 * 1024 * 1024` (50 MiB for the complete raw request), `limits.fileSize: 20 * 1024 * 1024` (20 MiB per file), `limits.files: 3`, `limits.fields: 20`, and `limits.fieldSize: 1024 * 1024` (1 MiB per field). The request-wide limit counts files, fields, headers, boundaries, and other multipart overhead before content is passed to the parser. It allows two files at the per-file maximum with room for metadata while bounding the complete request independently of the individual Busboy limits. Concurrent requests multiply memory and temporary-storage usage; choose lower limits when appropriate for your deployment. Individual `limits` overrides preserve the other defaults. A non-negative safe integer `requestSizeLimit` overrides the aggregate cap, and `requestSizeLimit: Infinity` disables it intentionally. Invalid values are rejected before the request body is read. An `undefined` override retains the 50 MiB default. Explicit Busboy limit values, including `Infinity`, are also respected. Request size, field size, file count, field count, and configured part count limits reject with HTTP 413. File size limits reject by default; `abortOnLimit: false` opts into truncated files. Failed requests cancel body consumption and remove temporary files. For example, to set a 75 MiB request-wide limit and a smaller per-file limit for uploads sent to Payload: ```ts import { buildConfig } from 'payload' export default buildConfig({ collections: [ { slug: 'media', fields: [ { name: 'alt', type: 'text', }, ], upload: true, }, ], upload: { requestSizeLimit: 75 * 1024 * 1024, // 75 MiB for the complete multipart request limits: { fileSize: 5000000, // 5MB, written in bytes }, }, }) ``` ### Custom filename via hooks You can customize the filename before it's uploaded to the server by using a `beforeOperation` hook. ```ts beforeOperation: [ ({ req, operation }) => { if ((operation === 'create' || operation === 'update') && req.file) { req.file.name = 'test.jpg' } }, ], ``` The `req.file` object will have additional information about the file, such as mimeType and extension, and you also have full access to the file data itself. The filename from here will also be threaded to image sizes if they're enabled. ## Variants If you declare image sizes, Payload will automatically crop and resize your uploads to fit each of the sizes specified by your config. The [Admin Panel](/docs/v4/admin/overview.md) will also automatically display all available files, including width, height, and file size, for each of your uploaded files. Image sizes — declared as `variants`, along with the rest of Sharp's image-resizing options — are configured through [`@payloadcms/transformer-sharp`](/docs/v4/upload/transformers.md#payloadcmstransformer-sharp), Payload's official image-resizing transformer: ```ts import { buildConfig } from 'payload' import { sharpTransformer } from '@payloadcms/transformer-sharp' export default buildConfig({ collections: [/* your Media collection, with `upload: true` */], upload: { transformers: [ sharpTransformer({ collections: { media: { variants: [ { name: 'thumbnail', width: 400, height: 300, position: 'centre', }, { name: 'card', width: 768, height: 1024, position: 'centre', }, { name: 'tablet', width: 1024, // By specifying `undefined` or leaving a height undefined, // the image will be sized to a certain width, // but it will retain its original aspect ratio // and calculate a height automatically. height: undefined, position: 'centre', }, ], }, }, }), ], }, }) ``` `sharpTransformer` writes a Sharp-agnostic projection of each configured `variants` entry (`name`, `admin`, and `generateImageName` only) back onto the collection's sanitized `upload.variants`, so the rest of this section — and the generated `variants` shape on your documents — is unaffected by where the configuration is authored. Neither `imageSizes` nor `variants` can be set on the collection itself. #### Admin List View Options Each image size also supports `admin` options to control whether it appears in the [Admin Panel](/docs/v4/admin/overview.md) list view. ```ts { name: 'thumbnail', width: 400, height: 300, admin: { disabled: { groupBy: true, // hide from list view groupBy options column: true, // hide from list view columns filter: true, // hide from list view filters }, }, } ``` | Option | Description | | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **`disabled.groupBy`** | If set to `true`, this image size will not be available as a selectable groupBy option in the collection list view. Defaults to `false`. | | **`disabled.column`** | If set to `true`, this image size will not be available as a selectable column in the collection list view. Defaults to `false`. | | **`disabled.filter`** | If set to `true`, this image size will not be available as a filter option in the collection list view. Defaults to `false`. | This is useful for hiding large or rarely used image sizes from the list view UI while still keeping them available in the API. #### Accessing the resized images in hooks All auto-resized images are exposed to be reused in hooks and similar via an object that is bound to `req.payloadUploadSizes`. The object will have keys for each size generated, and each key will be set equal to a buffer containing the file data. #### Handling Image Enlargement When an uploaded image is smaller than the defined image size, we have 3 options: `withoutEnlargement: undefined | false | true` 1. `undefined` [default]: uploading images with smaller width AND height than the image size will return null 2. `false`: always enlarge images to the image size 3. `true`: if the image is smaller than the image size, return the original image > **Note:** > > By default, the image size will return NULL when the uploaded image is smaller than the defined > image size. Use the `withoutEnlargement` prop to change this. #### Custom file name per size Each image size supports a `generateImageName` function that can be used to generate a custom file name for the resized image. This function receives the original file name, the resize name, the extension, height and width as arguments. ```ts { name: 'thumbnail', width: 400, height: 300, generateImageName: ({ height, sizeName, extension, width }) => { return `custom-${sizeName}-${height}-${width}.${extension}` }, } ``` ## Crop and Focal Point Selector This feature is only available for image file types. Setting `crop: false` and `focalPoint: false` on `sharpTransformer({ collections })` for this collection will disable the respective selector in the [Admin Panel](/docs/v4/admin/overview.md). Cropping changes the main image. Configured image sizes are generated from the retained original, so cropping the main image does not also crop those sizes. The focal point controls how configured sizes are framed. The focal point selector is only displayed when the collection has image sizes (`variants`) or image adjustments (`resizeOptions`, `formatOptions`, `trimOptions`, `constructorOptions`, or `withMetadata`), or when `focalPoint` is explicitly `true`. ## File Versioning Payload retains the source of each new upload as `original`, even when the main image is processed. The document's `original` field records where that file is stored; its filename is not changed to mark it. When the main image is processed, the original is stored as a separate file. With a storage adapter, it keeps the uploaded filename in its own storage folder. With local storage, all files share one folder, so the original gets a counter (for example `photo-1.png`) if the main file already uses the name. When the original and the main file share a filename, `original.url` includes `?original=true`. Without it, the file endpoint serves the main file. Custom storage adapters must handle `isOriginal` to serve the original; see [Custom Storage Adapters](/docs/v4/upload/storage-adapters.md#custom-storage-adapters). Uploads saved before this change keep their `-original` filenames and are still served. With versions enabled, saved versions also retain the stored files they reference until those versions are removed. Keeping originals and file history uses additional storage, especially when an image has several generated sizes. Existing uploads are upgraded as they are edited; no bulk file migration is required. For an older image whose untouched source was already deleted, the existing stored file is the best available original. The upgrade cannot recover source bytes or historical files that were deleted before file versioning was enabled. SQL projects need a schema migration; see the [v4 migration guide](/docs/v4/migration-guide/v4.md). To read a stored historical file through Payload's file endpoint, include `?version=`. Version reads generate these URLs for the main file, original, stored variants, and thumbnail. The requested filename and storage prefix must belong to that version, and both parent-document read access and version read access are enforced. Without `version`, the endpoint resolves current published files and the latest draft, subject to collection read access; filenames found only in history return 404. Historical requests serve the saved bytes without rerunning transformations. Public URLs supplied directly by a storage provider remain direct. ## Disabling Local Upload Storage If you are using a plugin to send your files off to a third-party file storage host or CDN, like Amazon S3 or similar, you may not want to store your files locally at all. You can prevent Payload from writing files to disk by specifying `disableLocalStorage: true` on your collection's upload config. > **Note:** > > This is a fairly advanced feature. If you do disable local file storage, by default, your admin > panel's thumbnails will be broken as you will not have stored a file. It will be totally up to you > to use either a plugin or your own hooks to store your files in a permanent manner, as well as > provide your own admin thumbnail using **upload.adminThumbnail**. ## Admin Thumbnails You can specify how Payload retrieves admin thumbnails for your upload-enabled Collections with one of the following: 1. `adminThumbnail` as a **string**, equal to one of your image size names (declared as `variants` on `sharpTransformer`). ```ts import { buildConfig } from 'payload' import { sharpTransformer } from '@payloadcms/transformer-sharp' export default buildConfig({ collections: [ { slug: 'media', fields: [], upload: { // highlight-start adminThumbnail: 'small', // highlight-end }, }, ], upload: { transformers: [ sharpTransformer({ collections: { media: { variants: [ { name: 'small', fit: 'cover', height: 300, width: 900, }, { name: 'large', fit: 'cover', height: 600, width: 1800, }, ], }, }, }), ], }, }) ``` 2. `adminThumbnail` as a **function** that takes the document's data and sends back a full URL to load the thumbnail. ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { // highlight-start adminThumbnail: ({ doc }) => `https://google.com/custom-path-to-file/${doc.filename}`, // highlight-end }, } ``` ## File Previews When editing a document in an upload-enabled Collection, the Admin Panel renders a preview of the file in the upload side panel. The preview that's used is chosen automatically based on the file's MIME type: | File type | Built-in preview | | ----------------- | -------------------------------------------------- | | `image/*` | Image thumbnail (falls back to a file icon) | | `video/*` | Native HTML5 `