# 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>account0> 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)

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

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

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

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

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

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

_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).

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

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

_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).

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

_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

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

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

_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