# Payload Documentation # What is Payload? Source: https://payloadcms.com/docs/getting-started/what-is-payload **Payload is the Next.js fullstack framework.** Write a Payload Config and instantly get: - A full Admin Panel using React server / client components, matching the shape of your data and completely extensible with your own React components - Automatic database schema, including direct DB access and ownership, with migrations, transactions, proper indexing, and more - Instant REST, GraphQL, and straight-to-DB Node.js APIs - Authentication which can be used in your own apps - A deeply customizable access control pattern - File storage and image management tools like cropping / focal point selection - Live preview - see your frontend render content changes in realtime as you update - Lots more ### Instant backend superpowers No matter what you're building, Payload will give you backend superpowers. Your entire Payload config can be installed in one line into any existing Next.js app, and is designed to catapult your development process. Payload takes the most complex and time-consuming parts of any modern web app and makes them simple. ### Open source - deploy anywhere, including Vercel It's fully open source with an MIT license and you can self-host anywhere that you can run a Node.js app. You can also deploy serverless to hosts like Vercel, right inside your existing Next.js application. ### Code-first and version controlled In Payload, there are no "click ops" - as in clicking around in an Admin Panel to define your schema. In Payload, everything is done the right way—code-first and version controlled like a proper backend. But once developers define how Payload should work, non-technical users can independently make use of its Admin Panel to manage whatever they need to without having to know code whatsoever. ### Fully extensible Even in spite of how much you get out of the box, you still have full control over every aspect of your app - be it database, admin UI, or anything else. Every part of Payload has been designed to be extensible and customizable with modern TypeScript / React. And you'll fully understand the code that you write. ## 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, then Payload is a no-brainer. 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 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](../admin/overview). 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. ## Choosing a Framework 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 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 Ready to get started? First, let's review some high-level concepts that are used in Payload. # Payload Concepts Source: https://payloadcms.com/docs/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 The Payload Config is central to everything that Payload does. It allows for the 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. [More details](../configuration/overview). ## Database Payload is database agnostic, meaning you can use any type of database behind Payload's familiar APIs through what is known as a Database Adapter. [More details](../database/overview). ## Collections A Collection is a group of records, called Documents, that all share a common schema. Each Collection is stored in the [Database](../database/overview) based on the [Fields](../fields/overview) that you define. [More details](../configuration/collections). ## Globals Globals are in many ways similar to [Collections](../configuration/collections), except they correspond to only a single Document. Each Global is stored in the [Database](../database/overview) based on the [Fields](../fields/overview) that you define. [More details](../configuration/globals). ## Fields Fields are the building blocks of Payload. They define the schema of the Documents that will be stored in the [Database](../database/overview), as well as automatically generate the corresponding UI within the Admin Panel. [More details](../fields/overview). ## Hooks Hooks allow you to execute your own side effects during specific events of the Document lifecycle, such as before read, after create, etc. [More details](../hooks/overview). ## Authentication Payload provides a secure, portable way to manage user accounts out of the box. Payload Authentication is designed to be used in both the Admin Panel, as well as your own external applications. [More details](../authentication/overview). ## Access Control Access Control determines what a user can and cannot do with any given Document, such as read, update, etc., as well as what they can and cannot see within the Admin Panel. [More details](../access-control/overview). ## Admin Panel Payload dynamically generates a beautiful, fully type-safe interface to manage your users and data. The Admin Panel is a React application built using the Next.js App Router. [More details](../admin/overview). ## Retrieving Data Everything Payload does (create, read, update, delete, login, logout, etc.) is exposed to you via three APIs: - [Local API](#local-api) - Extremely fast, direct-to-database access - [REST API](#rest-api) - Standard HTTP endpoints for querying and mutating data - [GraphQL](#graphql-api) - A full GraphQL API with a GraphQL Playground **Note:** All of these APIs share the exact same query language. [More details](../queries/overview). ### Local API By far one of the most powerful aspects of Payload is the fact that it gives you direct-to-database access to your data through the [Local API](../local-api/overview). It's _extremely_ fast and does not incur any typical HTTP overhead—you query your database directly in Node.js. The Local API is written in TypeScript, and so it is strongly typed and extremely nice to use. It works anywhere on the server, including custom Next.js Routes, Payload Hooks, Payload Access Control, and React Server Components. Here's a quick example of a React Server Component fetching data using the Local API: ```tsx import React from 'react' import config from '@payload-config' import { getPayload } from 'payload' const MyServerComponent: React.FC = () => { const payload = await getPayload({ config }) // The `findResult` here will be fully typed as `PaginatedDocs`, // where you will have the `docs` that are returned as well as // information about how many items are returned / are available in total / etc const findResult = await payload.find({ collection: 'pages' }) return ( ) } ``` For more information about the Local API, [click here](../local-api/overview). ### REST API By default, the Payload [REST API](../rest-api/overview) is mounted automatically for you at the `/api` path of your app. For example, if you have a Collection called `pages`: ```ts fetch('https://localhost:3000/api/pages') // highlight-line .then((res) => res.json()) .then((data) => console.log(data)) ``` For more information about the REST API, [click here](../rest-api/overview). ### GraphQL API Payload automatically exposes GraphQL queries and mutations through a dedicated [GraphQL API](../graphql/overview). By default, the GraphQL route handler is mounted at the `/api/graphql` path of your app. You'll also find a full GraphQL Playground which can be accessible at the `/api/graphql-playground` path of your app. You can use any GraphQL client with Payload's GraphQL endpoint. Here are a few packages: - [`graphql-request`](https://www.npmjs.com/package/graphql-request) - a very lightweight GraphQL client - [`@apollo/client`](https://www.apollographql.com/docs/react/api/core/ApolloClient/) - an industry-standard GraphQL client with lots of nice features For more information about the GraphQL API, [click here](../graphql/overview). ## 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](../local-api/overview). It executes [Access Control](../access-control/overview), [Hooks](../hooks/overview), [Validation](../fields/overview#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](../rest-api/overview) and [GraphQL API](../graphql/overview). `@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`, `@payloadcms/richtext-slate` Payload's Rich Text functionality is abstracted into separate packages and if you want to enable Rich Text in your project, you'll need to install one of these packages. We recommend Lexical for all new projects, and this is where Payload will focus its efforts on from this point, but Slate is still supported if you have already built with it. **Note:** Rich Text is entirely optional and you may not need it for your project. # Installation Source: https://payloadcms.com/docs/getting-started/installation ## Software Requirements Payload requires the following software: - Any JavaScript package manager (pnpm, npm, or yarn - pnpm is preferred) - Node.js version 20.9.0+ - Any [compatible database](../database/overview) (MongoDB, Postgres or SQLite) **Important:** Before proceeding any further, please ensure that you have the above requirements met. ## 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: ``` npx create-payload-app ``` 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](../configuration/overview). ## 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:** Next.js version 15 or higher is required for Payload. #### 1. Install the relevant packages First, you'll want to add the required Payload packages to your project and can do so by running the command below: ```bash pnpm i payload @payloadcms/next @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](../database/overview). 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](../database/mongodb), run: ```bash pnpm i @payloadcms/db-mongodb ``` - To install the [Postgres Adapter](../database/postgres), run: ```bash pnpm i @payloadcms/db-postgres ``` - To install the [SQLite Adapter](../database/sqlite), run: ```bash pnpm i @payloadcms/db-sqlite ``` **Note:** New [Database Adapters](../database/overview) 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 Next.js app folder Payload installs directly in your Next.js `/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](../admin/overview#project-structure)._ 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)`. The files that Payload needs to have 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. 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](../admin/overview). #### 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 experimental: { reactCompiler: false, }, } // 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. #### 4. Create a Payload Config and add it to your TypeScript config Finally, you need to create a [Payload Config](../configuration/overview). 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_URI || '', }), // 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](../configuration/overview). 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"] } } } ``` #### 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! # The Payload Config Source: https://payloadcms.com/docs/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](../database/overview) choice to the appearance of the [Admin Panel](../admin/overview) is fully controlled through the Payload Config. From here you can define [Fields](../fields/overview), add [Localization](./localization), enable [Authentication](../authentication/overview), configure [Access Control](../access-control/overview), 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](../database/overview) you'd like to use, then use [Collections](./collections) or [Globals](./globals) to define the schema of your data through [Fields](../fields/overview). 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_URI, }), 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](../admin/overview#admin-options). | | **`bin`** | Register custom bin scripts for Payload to execute. [More Details](#custom-bin-scripts). | | **`editor`** | The Rich Text Editor which will be used by `richText` fields. [More details](../rich-text/overview). | | **`db`** \* | The Database Adapter which will be used by Payload. [More details](../database/overview). | | **`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](./collections). | | **`compatibility`** | Compatibility flags for earlier versions of Payload. [More details](#compatibility-flags). | | **`globals`** | An array of Globals for Payload to manage. [More details](./globals). | | **`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](./localization). | | **`logger`** | Logger options, logger options with a destination stream, or an instantiated logger instance. [More details](https://getpino.io/#/docs/api?id=options). | | **`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](../graphql/overview#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](../authentication/cookies#csrf-attacks). | | **`defaultDepth`** | If a user does not specify `depth` while requesting a resource, this depth will be used. [More details](../queries/depth). | | **`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](../folders/overview). | | `queryPresets` | An object that to configure Collection Query Presets. [More details](../query-presets/overview). | | **`maxDepth`** | The maximum allowed depth to be permitted application-wide. This setting helps prevent against malicious queries. Defaults to `10`. [More details](../queries/depth). | | **`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](../upload/overview#payload-wide-upload-options). | | **`routes`** | Control the routing structure that Payload binds itself to. [More details](../admin/overview#root-level-routes). | | **`email`** | Configure the Email Adapter for Payload to use. [More details](../email/overview). | | **`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](../hooks/overview). | | **`plugins`** | An array of Plugins. [More details](../plugins/overview). | | **`endpoints`** | An array of Custom Endpoints added to the Payload router. [More details](../rest-api/overview#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](./i18n). | | **`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](../custom-components/overview#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](./collections) and [Globals](./globals), and to ensure that Payload uses your [Generated Types](../typescript/overview) for all [Local API](../local-api/overview) 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](../typescript/overview). | | **`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. | ## 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](../configuration/environment-vars) 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 Next.js-based Admin Panel generates a ClientConfig, which strips away any server-only code and enriches the config with React Components. ## Compatibility flags The Payload Config can accept compatibility flags for running the newest versions but with older databases. You should only use these flags if you need to, and should confirm that you need to prior to enabling these flags. `allowLocalizedWithinLocalized` Payload localization works on a field-by-field basis. As you can nest fields within other fields, you could potentially nest a localized field within a localized field—but this would be redundant and unnecessary. There would be no reason to define a localized field within a localized parent field, given that the entire data structure from the parent field onward would be localized. By default, Payload will remove the `localized: true` property from sub-fields if a parent field is localized. Set this compatibility flag to `true` only if you have an existing Payload MongoDB database from pre-3.0, and you have nested localized fields that you would like to maintain without migrating. ## Custom bin scripts Using the `bin` configuration property, you can inject your own scripts to `npx payload`. Example for `pnpm payload seed`: Step 1: create `seed.ts` file in the same folder with `payload.config.ts` with: ```ts import type { SanitizedConfig } from 'payload' import payload from 'payload' // Script must define a "script" function export that accepts the sanitized config export const script = async (config: SanitizedConfig) => { await payload.init({ config }) await payload.create({ collection: 'pages', data: { title: 'my title' }, }) payload.logger.info('Successfully seeded!') process.exit(0) } ``` Step 2: add the `seed` script to `bin`: ```ts export default buildConfig({ bin: [ { scriptPath: path.resolve(dirname, 'seed.ts'), key: 'seed', }, ], }) ``` Now you can run the command using: ```sh pnpm payload seed ``` ## Running bin scripts on a schedule Every bin script supports being run on a schedule using cron syntax. Simply pass the `--cron` flag followed by the cron expression when running the script. Example: ```sh pnpm payload run ./myScript.ts --cron "0 * * * *" ``` This will use the `run` bin script to execute the specified script on the defined schedule. # Collection Configs Source: https://payloadcms.com/docs/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](../database/overview) based on the [Fields](../fields/overview) that you define, and automatically generates a [Local API](../local-api/overview), [REST API](../rest-api/overview), and [GraphQL API](../graphql/overview) used to manage your Documents. Collections are also used to achieve [Authentication](../authentication/overview) 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](../access-control/overview), [Hooks](../hooks/overview), [Admin Options](#admin-options), and more. To define a Collection Config, use the `collection` property in your [Payload Config](./overview): ```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](./globals) instead. ## Config Options It's often best practice to write your Collections in separate files and then import them into the main [Payload Config](./overview). 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](../access-control/collections). | | `auth` | Specify options if you would like this Collection to feature authentication. [More details](../authentication/overview). | | `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](../rest-api/overview#custom-endpoints). | | `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Collection. [More details](../fields/overview). | | `graphQL` | Manage GraphQL-related properties for this collection. [More](#graphql) | | `hooks` | Entry point for Hooks. [More details](../hooks/overview#collection-hooks). | | `orderable` | If true, enables custom ordering for the collection, and documents can be reordered via drag and drop. Uses [fractional indexing](https://observablehq.com/@dgreensp/implementing-fractional-indexing) 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](../query-presets/overview). | | `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](../admin/locked-documents). | | `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](../trash/overview). | | `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](../upload/overview) documentation. | | `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). | | `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#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. | | `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks. [More details](../queries/select). | | `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](../fields/overview) documentation. ### Access Control [Collection Access Control](../access-control/overview) determines what a user can and cannot do with any given Document within a Collection. To learn more, go to the [Access Control](../access-control/overview) documentation. ### Hooks [Collection Hooks](../hooks/collections) 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](../hooks/overview) documentation. ## Admin Options The behavior of Collections within the [Admin Panel](../admin/overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](../custom-components/overview), 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 the link from the navigation 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. | | `hooks` | Admin-specific hooks for this Collection. [More details](../hooks/collections). | | `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. | | `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. | | `groupBy` | Beta. Enable grouping by a field in the list view. | | `hideAPIURL` | Hides the "API URL" meta field while editing documents within this Collection. | | `enableRichTextLink` | The [Rich Text](../fields/rich-text) 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](../fields/rich-text) 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](../folders/overview). | | `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](../admin/metadata). | | `preview` | Function to generate preview URLs within the Admin Panel that can point to your app. [More details](../admin/preview). | | `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). | | `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). | | `enableListViewSelectAPI` | Performance opt-in. When `true`, uses the Select API in the List View to query only the active columns as opposed to entire documents. [More details](#enable-list-view-select-api). | | `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, | **Note:** If you set `useAsTitle` to a relationship or join field, it will use only the ID of the related document(s) as the title. To display a specific field (i.e. title) from the related document instead, create a virtual field that extracts the desired data, and set `useAsTitle` to that virtual field. ### Custom Components Collections can set their own [Custom Components](../custom-components/overview) which only apply to Collection-specific UI within the [Admin Panel](../admin/overview). 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](../custom-components/list-view#afterlist). | | `afterListTable` | An array of components to inject _after_ the built-in List View's table. [More details](../custom-components/list-view#afterlisttable). | | `beforeList` | An array of components to inject _before_ the built-in List View. [More details](../custom-components/list-view#beforelist). | | `beforeListTable` | An array of components to inject _before_ the built-in List View's table. [More details](../custom-components/list-view#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](../custom-components/list-view#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](../custom-components/custom-views). | #### Edit View Options ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { edit: { // highlight-line // ... }, }, }, } ``` The following options are available: | Option | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](../custom-components/edit-view#beforedocumentcontrols). | | `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document controls bar. [More details](../custom-components/edit-view#editmenuitems). | | `SaveButton` | Replace the default Save Button within the Edit View. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#savebutton). | | `SaveDraftButton` | Replace the default Save Draft Button within the Edit View. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. [More details](../custom-components/edit-view#savedraftbutton). | | `PublishButton` | Replace the default Publish Button within the Edit View. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publishbutton). | | `PreviewButton` | Replace the default Preview Button within the Edit View. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#previewbutton). | | `Upload` | Replace the default Upload component within the Edit View. [Upload](../upload/overview) must be enabled. [More details](../custom-components/edit-view#upload). | **Note:** For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#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](../queries/pagination) 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. ## Enable List View Select API When `true`, the List View will use the [Select API](../queries/select) to query only the _active_ columns as opposed to entire documents. This can greatly improve performance, especially for collections with large documents or many fields. To enable this, set `enableListViewSelectAPI: true` in your Collection Config: ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { // ... admin: { // ... // highlight-start enableListViewSelectAPI: true, // highlight-end }, } ``` **Note:** The `enableListViewSelectAPI` property is labeled as experimental, as it will likely become the default behavior in v4 and be deprecated. Enabling this feature may cause unexpected behavior in some cases, however, 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 no longer 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 no longer be populated by default, unless the column is active ], }, }, ], } ``` To ensure title is always present, you will need to add that field to the [`forceSelect`](../queries/select) property in your Collection Config: ```ts export const Posts: CollectionConfig = { // ... forceSelect: { 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](../types/payload-request) 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/configuration/globals Globals are in many ways similar to [Collections](./collections), 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](../database/overview) based on the [Fields](../fields/overview) that you define, and automatically generates a [Local API](../local-api/overview), [REST API](../rest-api/overview), and [GraphQL API](../graphql/overview) 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](../access-control/overview), [Hooks](../hooks/overview), [Admin Options](#admin-options), and more. To define a Global Config, use the `globals` property in your [Payload Config](./overview): ```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](./collections) instead. ## Config Options It's often best practice to write your Globals in separate files and then import them into the main [Payload Config](./overview). Here is what a simple Global Config might look like: ```ts import { 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](../access-control/globals). | | `admin` | The configuration options for the Admin Panel. [More details](#admin-options). | | `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](../rest-api/overview#custom-endpoints). | | `fields` \* | Array of field types that will determine the structure and functionality of the data stored within this Global. [More details](../fields/overview). | | `graphQL` | Manage GraphQL-related properties related to this global. [More details](#graphql) | | `hooks` | Entry point for Hooks. [More details](../hooks/overview#global-hooks). | | `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](../admin/locked-documents). | | `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](../versions/overview#global-config). | | `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks. [More details](../queries/select). | _\* An asterisk denotes that a property is required._ ### Fields Fields define the schema of the Global. To learn more, go to the [Fields](../fields/overview) documentation. ### Access Control [Global Access Control](../access-control/globals) determines what a user can and cannot do with any given Global Document. To learn more, go to the [Access Control](../access-control/overview) documentation. ### Hooks [Global Hooks](../hooks/globals) 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](../hooks/overview) documentation. ## Admin Options The behavior of Globals within the [Admin Panel](../admin/overview) can be fully customized to fit the needs of your application. This includes grouping or hiding their navigation links, adding [Custom Components](../custom-components/overview), setting page metadata, and more. To configure Admin Options for Globals, use the `admin` property in your Global Config: ```ts import { 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 link from the navigation 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](../admin/preview). | | `livePreview` | Enable real-time editing for instant visual feedback of your front-end application. [More details](../live-preview/overview). | | `hideAPIURL` | Hides the "API URL" meta field while editing documents within this collection. | | `meta` | Page metadata overrides to apply to this Global within the Admin Panel. [More details](../admin/metadata). | ### Custom Components Globals can set their own [Custom Components](../custom-components/overview) which only apply to Global-specific UI within the [Admin Panel](../admin/overview). 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 | | ---------- | ------------------------------------------------------------------------------------------------------- | | `elements` | Override or create new elements within the Edit View. [More details](#edit-view-options). | | `views` | Override or create new views within the Admin Panel. [More details](../custom-components/custom-views). | #### Edit View Options ```ts import type { SanitizedGlobalConfig } from 'payload' export const MyGlobal: SanitizedGlobalConfig = { // ... admin: { components: { elements: { // highlight-line // ... }, }, }, } ``` The following options are available: | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `SaveButton` | Replace the default Save Button with a Custom Component. [Drafts](../versions/drafts) must be disabled. [More details](../custom-components/edit-view#savebutton). | | `SaveDraftButton` | Replace the default Save Draft Button with a Custom Component. [Drafts](../versions/drafts) must be enabled and autosave must be disabled. [More details](../custom-components/edit-view#savedraftbutton). | | `PublishButton` | Replace the default Publish Button with a Custom Component. [Drafts](../versions/drafts) must be enabled. [More details](../custom-components/edit-view#publishbutton). | | `PreviewButton` | Replace the default Preview Button with a Custom Component. [Preview](../admin/preview) must be enabled. [More details](../custom-components/edit-view#previewbutton). | **Note:** For details on how to build Custom Components, see [Building Custom Components](../custom-components/overview#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/configuration/i18n The [Admin Panel](../admin/overview) 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](./localization), 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](./overview): ```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](./overview): ```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. ### 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](./overview): ```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](./collections) and [Globals](./globals), 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](../admin/preferences). ## 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](../custom-components/overview): ```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 { DefaultTranslationKeys, 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/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](./i18n), 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. To configure Localization, use the `localization` key in your [Payload Config](./overview): ```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](./overview): ```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](../configuration/i18n). 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', 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. ## 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](../graphql/overview#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 } } } ``` 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', 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/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](../database/overview) 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_URI=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](../admin/overview) does **not** include Environment Variables in its _client-side_ bundle by default. But, Next.js provides a mechanism to expose Environment Variables to the client-side bundle when needed. If you are building a [Custom Component](../custom-components/overview) and need to access Environment Variables from the client-side, you can do so by prefixing them with `NEXT_PUBLIC_`. **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). ## 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. # Database Source: https://payloadcms.com/docs/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](../database/mongodb) with [Mongoose](https://mongoosejs.com/) - [Postgres](../database/postgres) with [Drizzle](https://drizzle.team/) - [SQLite](../database/sqlite) with [Drizzle](https://drizzle.team/) To configure a Database Adapter, use the `db` property in your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' import { mongooseAdapter } from '@payloadcms/db-mongodb' export default buildConfig({ // ... // highlight-start db: mongooseAdapter({ url: process.env.DATABASE_URI, }), // 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](../configuration/localization) - You leverage a lot of [Arrays](../fields/array), [Blocks](../fields/blocks), or `hasMany` [Select](../fields/select) 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](./migrations) - You require enforced data consistency at the database level - You have a lot of relationships between collections and require relationships to be enforced ## Payload Differences It's important to note that nearly every Payload feature is available in all of our officially supported Database Adapters, including [Localization](../configuration/localization), [Arrays](../fields/array), [Blocks](../fields/blocks), etc. The only thing that is not supported in SQLite yet is the [Point Field](../fields/point), 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. # Migrations Source: https://payloadcms.com/docs/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 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](../database/transactions) for you. All you need to do is pass the `req` object to any [Local API](../local-api/overview) 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](./transactions). ```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](./postgres#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: 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. 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 --turbo", // 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/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', 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', 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', 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', 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', data: { some: 'data', }, where: { slug: { equals: 'my-slug' }, }, disableTransaction: true, }) ``` # Indexes Source: https://payloadcms.com/docs/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 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](../configuration/collections): ```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 }, ], } ``` ## 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/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_URI, }), }) ``` ## Options | Option | Description | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `autoPluralization` | Tell Mongoose to auto-pluralize any collection names if it encounters any singular words used as collection `slug`s. | | `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_URI, // 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 the root config property `indexSortableFields` must be set to `true`. # Postgres Source: https://payloadcms.com/docs/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_URI, }, }), }) ``` `@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` 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. | | `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. | | `blocksAsJSON` | Store blocks as a JSON column instead of using the relational structure which can improve performance with a large amount of blocks | ## 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`](../database/migrations) 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](./migrations#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/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 | ## 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`](../database/migrations) 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](./migrations#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 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/). 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. # Fields Overview Source: https://payloadcms.com/docs/fields/overview Fields are the building blocks of Payload. They define the schema of the Documents that will be stored in the [Database](../database/overview), as well as automatically generate the corresponding UI within the [Admin Panel](../admin/overview). 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](./overview#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). To configure fields, use the `fields` property in your [Collection](../configuration/collections) or [Global](../configuration/globals) 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](../admin/overview). To configure fields, use the `fields` property in your [Collection](../configuration/collections) or [Global](../configuration/globals) 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](../database/overview). 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](./array) - for repeating content, supports nested fields - [Blocks](./blocks) - for block-based content, supports nested fields - [Checkbox](./checkbox) - saves boolean true / false values - [Code](./code) - renders a code editor interface that saves a string - [Date](./date) - renders a date picker and saves a timestamp - [Email](./email) - ensures the value is a properly formatted email address - [Group](./group) - nests fields within a keyed object - [JSON](./json) - renders a JSON editor interface that saves a JSON object - [Number](./number) - saves numeric values - [Point](./point) - for location data, saves geometric coordinates - [Radio](./radio) - renders a radio button group that allows only one value to be selected - [Relationship](./relationship) - assign relationships to other collections - [Rich Text](./rich-text) - renders a fully extensible rich text editor - [Select](./select) - renders a dropdown / picklist style value selector - [Tabs (Named)](./tabs) - similar to group, but renders nested fields within a tabbed layout - [Text](./text) - simple text input that saves a string - [Textarea](./textarea) - similar to text, but allows for multi-line input - [Upload](./upload) - 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](../admin/overview), or to add custom UI components. Here are the available Presentational Fields: - [Collapsible](../fields/collapsible) - nests fields within a collapsible component - [Row](../fields/row) - aligns fields horizontally - [Tabs (Unnamed)](../fields/tabs) - nests fields within a tabbed layout. It is not presentational if the tab has a name. - [Group (Unnamed)](../fields/group) - nests fields within a keyed object It is not presentational if the group has a name. - [UI](../fields/ui) - 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](../fields/join) - 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](../custom-components/overview), you can override the entirety of how a component functions within the [Admin Panel](../admin/overview) 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](../admin/overview). 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', } ``` 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 } ``` 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](../hooks/overview) 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](../hooks/fields) documentation. ### Field-level Access Control In addition to being able to define [Access Control](../access-control/overview) 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](../access-control/fields) documentation. ### Default Values Fields can be optionally prefilled with initial values. This is used in both the [Admin Panel](../admin/overview) 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](../database/overview) 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](../configuration/i18n#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 }, ], } ``` For more performance tips, see the [Performance documentation](../performance/overview). ## Custom ID Fields All [Collections](../configuration/collections) 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', }, ], } ``` **Reminder:** The Custom ID Fields can only be of type [`Number`](./number) or [`Text`](./text). Custom ID fields with type `text` must not contain `/` or `.` characters. ## Admin Options You can customize the appearance and behavior of fields within the [Admin Panel](../admin/overview) 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](../custom-components/overview) that you define. | | **`description`** | Helper text to display alongside the field to provide more information for the editor. [More details](#description). | | **`position`** | Specify if the field should be rendered in the sidebar by defining `position: 'sidebar'`. | | **`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`** | If a field is `disabled`, it is completely omitted from the [Admin Panel](../admin/overview) entirely. | | **`disableBulkEdit`** | Set `disableBulkEdit` to `true` to prevent fields from appearing in the select options when making edits for multiple documents. Defaults to `true` for UI fields. | | **`disableGroupBy`** | Set `disableGroupBy` to `true` to prevent fields from appearing in the list view groupBy options. Defaults to `false`. | | **`disableListColumn`** | Set `disableListColumn` to `true` to prevent fields from appearing in the list view column selector. Defaults to `false`. | | **`disableListFilter`** | Set `disableListFilter` to `true` to prevent fields from appearing in the list view filter options. Defaults to `false`. | | **`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. | ### 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](../custom-components/overview), 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](../configuration/localization). 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](../configuration/i18n) | **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](../admin/overview), 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](../custom-components/overview#building-custom-components)._ 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-beforinput), and [`afterInput`](#afterinput-and-beforinput) properties. ##### Default Props All Field Components receive the following props by default: | Property | Description | | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`docPreferences`** | An object that contains the [Preferences](../admin/preferences) 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](../configuration/localization). | | **`readOnly`** | A boolean value that represents if the field is read-only or not. | | **`user`** | The currently authenticated user. [More details](../authentication/overview). | | **`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](../configuration/i18n) object. | | **`payload`** | The [Payload](../local-api/overview) 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](../authentication/overview). | | **`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`](../admin/react-hooks#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} /> } ``` For a complete list of all available React hooks, see the [Payload React Hooks](../admin/react-hooks) documentation. For additional help, see [Building Custom Components](../custom-components/overview#building-custom-components). ##### TypeScript#field-component-types When building Custom Field Components, you can import the client field props to ensure type safety in your component. There is an explicit type for the Field Component, one for every Field Type and server/client environment. The convention is to prepend the field type onto the target type, i.e. `TextFieldClientComponent`: ```tsx import type { TextFieldClientComponent, TextFieldServerComponent, 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 }, }, } ``` All Cell Components receive the same [Default Field Component Props](#field), plus the following: | Property | Description | | ------------- | --------------------------------------------------------------------- | | **`link`** | A boolean representing whether this cell should be wrapped in a link. | | **`onClick`** | A function that is called when the cell is clicked. | For details on how to build Custom Components themselves, see [Building Custom Components](../custom-components/overview#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. 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](../custom-components/overview#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](../custom-components/overview#building-custom-components). ##### TypeScript#label-component-types When building Custom Label Components, you can import the component types to ensure type safety in your component. There is an explicit type for the Label Component, one for every Field Type and server/client environment. The convention is to append `LabelServerComponent` or `LabelClientComponent` to the type of field, i.e. `TextFieldLabelClientComponent`. ```tsx import type { TextFieldLabelServerComponent, TextFieldLabelClientComponent, // ...and so on for each Field Type } from 'payload' ``` #### Description Alternatively to the [Description Property](#field-descriptions), you can also use a [Custom Component](../custom-components/overview) 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](../custom-components/overview#building-custom-components). ##### TypeScript#description-component-types When building Custom Description Components, you can import the component props to ensure type safety in your component. There is an explicit type for the Description Component, one for every Field Type and server/client environment. The convention is to append `DescriptionServerComponent` or `DescriptionClientComponent` to the type of field, i.e. `TextFieldDescriptionClientComponent`. ```tsx import type { TextFieldDescriptionServerComponent, TextFieldDescriptionClientComponent, // 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](../custom-components/overview#building-custom-components). ##### TypeScript#error-component-types When building Custom Error Components, you can import the component types to ensure type safety in your component. There is an explicit type for the Error Component, one for every Field Type and server/client environment. The convention is to append `ErrorServerComponent` or `ErrorClientComponent` to the type of field, i.e. `TextFieldErrorClientComponent`. ```tsx import type { TextFieldErrorServerComponent, TextFieldErrorClientComponent, // 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](../custom-components/overview#building-custom-components). ##### TypeScript#diff-component-types When building Custom Diff Components, you can import the component types to ensure type safety in your component. There is an explicit type for the Diff Component, one for every Field Type and server/client environment. The convention is to append `DiffServerComponent` or `DiffClientComponent` to the type of field, i.e. `TextFieldDiffClientComponent`. ```tsx import type { TextFieldDiffServerComponent, TextFieldDiffClientComponent, // 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](../custom-components/overview#building-custom-components). ## TypeScript You can import the Payload `Field` type as well as other common types from the `payload` package. [More details](../typescript/overview). ```ts import type { Field } from 'payload' ``` # Array Field Source: https://payloadcms.com/docs/fields/array The Array Field is used when you need to have a set of "repeating" [Fields](./overview). 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](../fields/upload)) and a caption ([text field](../fields/text)) - Navigational structures where editors can specify nav items containing pages ([relationship field](../fields/relationship)), an "open in new tab" [checkbox field](../fields/checkbox) - Event agenda "timeslots" where you need to specify start & end time ([date field](../fields/date)), label ([text field](../fields/text)), and Learn More page [relationship](../fields/relationship) To create an Array Field, set the `type` to `array` in your [Field Config](./overview): ```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](../fields/overview#field-names). | | **`label`** | Text used as the heading in the [Admin Panel](../admin/overview) 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](../admin/overview) and the backend. [More details](../fields/overview#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](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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](../typescript/generating-types#custom-field-interfaces) & [GraphQL type](../graphql/graphql-schema#custom-field-schemas). | | **`dbName`** | Custom table name for the field when using SQL Database Adapter ([Postgres](../database/postgres)). Auto-generated from name if not defined. | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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 Array Field in the [Admin Panel](../admin/overview), 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](./overview#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 { ArrayFieldServerComponent } from 'payload' export const CustomArrayFieldServer: ArrayFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { ArrayField } from '@payloadcms/ui' import type { ArrayFieldClientComponent } from 'payload' export const CustomArrayFieldClient: ArrayFieldClientComponent = (props) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { ArrayFieldLabelServerComponent } from 'payload' export const CustomArrayFieldLabelServer: ArrayFieldLabelServerComponent = ({ clientField, path, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import type { ArrayFieldLabelClientComponent } from 'payload' import { FieldLabel } from '@payloadcms/ui' import React from 'react' export const CustomArrayFieldLabelClient: ArrayFieldLabelClientComponent = ({ 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/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`. To add a Blocks Field, set the `type` to `blocks` in your [Field Config](./overview): ```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 ### Config Options | Option | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](../fields/overview#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](../fields/blocks#block-configs) 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](../fields/overview#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](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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. | | **`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) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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 Blocks Field in the [Admin Panel](../admin/overview), 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](./overview#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](../rich-text/overview), 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' ``` ## 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. | | **`imageURL`** | Provide a custom image thumbnail to help editors identify this block in the Admin UI. | | **`imageAltText`** | Customize this block's image thumbnail alt text. | | **`interfaceName`** | Create a top level, reusable [Typescript interface](../typescript/generating-types#custom-field-interfaces) & [GraphQL type](../graphql/graphql-schema#custom-field-schemas). | | **`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](../database/postgres)). Auto-generated from slug if not defined. | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | ### 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) | ### 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 { BlocksFieldServerComponent } from 'payload' export const CustomBlocksFieldServer: BlocksFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` ##### Client Component ```tsx 'use client' import React from 'react' import { BlocksField } from '@payloadcms/ui' import type { BlocksFieldClientComponent } from 'payload' export const CustomBlocksFieldClient: BlocksFieldClientComponent = (props) => { return } ``` #### Label ##### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { BlocksFieldLabelServerComponent } from 'payload' export const CustomBlocksFieldLabelServer: BlocksFieldLabelServerComponent = ({ clientField, path, }) => { return ( ) } ``` ##### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { BlocksFieldLabelClientComponent } from 'payload' export const CustomBlocksFieldLabelClient: BlocksFieldLabelClientComponent = ({ label, path, required, }) => { 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 `blockReferences` array - leaving the `blocks` array empty for compatibility reasons. ```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 blockReferences: ['TextBlock'], blocks: [], // Required to be empty, for compatibility reasons }, ], }, { 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 in the `blockReferences` array 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 in the `blockReferences` are 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. ### 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/fields/checkbox The Checkbox Field saves a boolean in the database. To add a Checkbox Field, set the `type` to `checkbox` in your [Field Config](./overview): ```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](../fields/overview#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](../fields/overview#validation). | | **`index`** | Build an [index](../database/indexes) 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](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](./overview#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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 { CheckboxFieldServerComponent } from 'payload' export const CustomCheckboxFieldServer: CheckboxFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { CheckboxField } from '@payloadcms/ui' import type { CheckboxFieldClientComponent } from 'payload' export const CustomCheckboxFieldClient: CheckboxFieldClientComponent = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { CheckboxFieldLabelServerComponent } from 'payload' export const CustomCheckboxFieldLabelServer: CheckboxFieldLabelServerComponent = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { CheckboxFieldLabelClientComponent } from 'payload' export const CustomCheckboxFieldLabelClient: CheckboxFieldLabelClientComponent = ({ label, path, required }) => { return ( ) } ``` # Code Field Source: https://payloadcms.com/docs/fields/code The Code Field saves a string in the database, but provides the [Admin Panel](../admin/overview) with a code editor styled interface. To add a Code Field, set the `type` to `code` in your [Field Config](./overview): ```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](../fields/overview#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. | | **`index`** | Build an [index](../database/indexes) 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](../fields/overview#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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](../admin/overview), 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](./overview#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 { CodeFieldServerComponent } from 'payload' export const CustomCodeFieldServer: CodeFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { CodeField } from '@payloadcms/ui' import type { CodeFieldClientComponent } from 'payload' export const CustomCodeFieldClient: CodeFieldClientComponent = (props) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { CodeFieldLabelServerComponent } from 'payload' export const CustomCodeFieldLabelServer: CodeFieldLabelServerComponent = ({ clientField, path, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { CodeFieldLabelClientComponent } from 'payload' export const CustomCodeFieldLabelClient: CodeFieldLabelClientComponent = ({ field, path, }) => { return ( ) } ``` # JSON Field Source: https://payloadcms.com/docs/fields/json The JSON Field saves raw JSON to the database and provides the [Admin Panel](../admin/overview) with a code editor styled interface. This is different from the [Code Field](./code) which saves the value as a string in the database. To add a JSON Field, set the `type` to `json` in your [Field Config](./overview): ```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](../fields/overview#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. | | **`index`** | Build an [index](../database/indexes) 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](../fields/overview#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](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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](../admin/overview), 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](./overview#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/typedoc/variables/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 { JSONFieldServerComponent } from 'payload' export const CustomJSONFieldServer: JSONFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { JSONField } from '@payloadcms/ui' import type { JSONFieldClientComponent } from 'payload' export const CustomJSONFieldClient: JSONFieldClientComponent = (props) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { JSONFieldLabelServerComponent } from 'payload' export const CustomJSONFieldLabelServer: JSONFieldLabelServerComponent = ({ clientField, path, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { JSONFieldLabelClientComponent } from 'payload' export const CustomJSONFieldLabelClient: JSONFieldLabelClientComponent = ({ field, path, }) => { return ( ) } ``` # Collapsible Field Source: https://payloadcms.com/docs/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. To add a Collapsible Field, set the `type` to `collapsible` in your [Field Config](./overview): ```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](../admin/overview), 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](./overview#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/fields/date The Date Field saves a Date in the database and provides the [Admin Panel](../admin/overview) with a customizable time picker interface. To add a Date Field, set the `type` to `date` in your [Field Config](./overview): ```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](../fields/overview#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](../database/indexes) 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](../fields/overview#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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). | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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](../admin/overview), 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](./overview#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 { DateFieldServerComponent } from 'payload' export const CustomDateFieldServer: DateFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { DateTimeField } from '@payloadcms/ui' import type { DateFieldClientComponent } from 'payload' export const CustomDateFieldClient: DateFieldClientComponent = (props) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { DateFieldLabelServerComponent } from 'payload' export const CustomDateFieldLabelServer: DateFieldLabelServerComponent = ({ clientField, path, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { DateFieldLabelClientComponent } from 'payload' export const CustomDateFieldLabelClient: DateFieldLabelClientComponent = ({ 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](../admin/overview#timezones). **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. # Email Field Source: https://payloadcms.com/docs/fields/email The Email Field enforces that the value provided is a valid email address. To create an Email Field, set the `type` to `email` in your [Field Config](./overview): ```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](../fields/overview#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. | | **`index`** | Build an [index](../database/indexes) 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](../fields/overview#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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](../admin/overview), 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](./overview#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 { EmailFieldServerComponent } from 'payload' export const CustomEmailFieldServer: EmailFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { EmailField } from '@payloadcms/ui' import type { EmailFieldClientComponent } from 'payload' export const CustomEmailFieldClient: EmailFieldClientComponent = (props) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { EmailFieldLabelServerComponent } from 'payload' export const CustomEmailFieldLabelServer: EmailFieldLabelServerComponent = ({ clientField, path, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { EmailFieldLabelClientComponent } from 'payload' export const CustomEmailFieldLabelClient: EmailFieldLabelClientComponent = ({ field, path, }) => { return ( ) } ``` # Group Field Source: https://payloadcms.com/docs/fields/group The Group Field allows [Fields](./overview) to be nested under a common property name. It also groups fields together visually in the [Admin Panel](../admin/overview). To add a Group Field, set the `type` to `group` in your [Field Config](./overview): ```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](../fields/overview#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](../fields/overview#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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](../typescript/generating-types#custom-field-interfaces) & [GraphQL type](../graphql/graphql-schema#custom-field-schemas). | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`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](../admin/overview), 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](./overview#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 label will render just the grouped fields. ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { 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/fields/number The Number Field stores and validates numeric entry and supports additional numerical validation and formatting features. To add a Number Field, set the `type` to `number` in your [Field Config](./overview): ```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](../fields/overview#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. | | **`index`** | Build an [index](../database/indexes) 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](../fields/overview#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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](../admin/overview), 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](./overview#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 { NumberFieldServerComponent } from 'payload' export const CustomNumberFieldServer: NumberFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { NumberField } from '@payloadcms/ui' import type { NumberFieldClientComponent } from 'payload' export const CustomNumberFieldClient: NumberFieldClientComponent = (props) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { NumberFieldLabelServerComponent } from 'payload' export const CustomNumberFieldLabelServer: NumberFieldLabelServerComponent = ({ clientField, path, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { NumberFieldLabelClientComponent } from 'payload' export const CustomNumberFieldLabelClient: NumberFieldLabelClientComponent = ({ field, path, }) => { return ( ) } ``` # Point Field Source: https://payloadcms.com/docs/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. To add a Point Field, set the `type` to `point` in your [Field Config](./overview): ```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](../fields/overview#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. | | **`index`** | Build an [index](../database/indexes) 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](../fields/overview#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](./overview#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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', 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', 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 { PointFieldServerComponent } from 'payload' export const CustomPointFieldServer: PointFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { PointField } from '@payloadcms/ui' import type { PointFieldClientComponent } from 'payload' export const CustomPointFieldClient: PointFieldClientComponent = (props) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { PointFieldLabelServerComponent } from 'payload' export const CustomPointFieldLabelServer: PointFieldLabelServerComponent = ({ clientField, path, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { PointFieldLabelClientComponent } from 'payload' export const CustomPointFieldLabelClient: PointFieldLabelClientComponent = ({ field, path, }) => { return ( ) } ``` # Radio Group Field Source: https://payloadcms.com/docs/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](../admin/overview). To add a Radio Field, set the `type` to `radio` in your [Field Config](./overview): ```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](../fields/overview#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](../fields/overview#validation). | | **`index`** | Build an [index](../database/indexes) 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](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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](../database/postgres)). Auto-generated from name if not defined. | | **`interfaceName`** | Create a top level, reusable [Typescript interface](../typescript/generating-types#custom-field-interfaces) & [GraphQL type](../graphql/graphql-schema#custom-field-schemas). | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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](../admin/overview), 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](./overview#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 { RadioFieldServerComponent } from 'payload' export const CustomRadioFieldServer: RadioFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { RadioGroupField } from '@payloadcms/ui' import type { RadioFieldClientComponent } from 'payload' export const CustomRadioFieldClient: RadioFieldClientComponent = (props) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { RadioFieldLabelServerComponent } from 'payload' export const CustomRadioFieldLabelServer: RadioFieldLabelServerComponent = ({ clientField, path, required, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { RadioFieldLabelClientComponent } from 'payload' export const CustomRadioFieldLabelClient: RadioFieldLabelClientComponent = ({ field, path, }) => { return ( ) } ``` # Relationship Field Source: https://payloadcms.com/docs/fields/relationship The Relationship Field is one of the most powerful fields Payload features. It provides the ability to easily relate documents together. 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](./overview): ```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](../fields/overview#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](../queries/depth#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. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](../fields/overview#validation). | | **`index`** | Build an [index](../database/indexes) 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](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`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](../fields/overview#virtual-field-configuration) | | **`graphQL`** | Custom graphQL configuration for the field. [More details](../graphql/overview#field-complexity) | _\* An asterisk denotes that a property is required._ **Tip:** The [Depth](../queries/depth) 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](../admin/overview), 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](./overview#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](../queries/overview), 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](../queries/overview). **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](./join). ## 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 { RelationshipFieldServerComponent } from 'payload' export const CustomRelationshipFieldServer: RelationshipFieldServerComponent = ({ clientField, path, schemaPath, permissions }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { RelationshipField } from '@payloadcms/ui' import type { RelationshipFieldClientComponent } from 'payload' export const CustomRelationshipFieldClient: RelationshipFieldClientComponent = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { RelationshipFieldLabelServerComponent } from 'payload' export const CustomRelationshipFieldLabelServer: RelationshipFieldLabelServerComponent = (clientField, path) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { RelationshipFieldLabelClientComponent } from 'payload' export const CustomRelationshipFieldLabelClient: RelationshipFieldLabelClientComponent = ({ field, path }) => { return ( ) } ``` # Join Field Source: https://payloadcms.com/docs/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 For the Join field to work, you must have an existing [relationship](./relationship) or [upload](./upload) 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](./overview): ```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. 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. 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. 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](./overview#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](https://observablehq.com/@dgreensp/implementing-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](../queries/depth#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](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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). | | **`typescriptSchema`** | Override field type generation with providing a JSON schema. | | **`graphQL`** | Custom graphQL configuration for the field. [More details](../graphql/overview#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](./overview#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', where: { title: { equals: 'My Category', }, }, joins: { relatedPosts: { limit: 5, where: { title: { equals: 'My Post', }, }, sort: 'title', }, }, }) ``` 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/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. ## Config Options | Option | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](./overview#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](./overview#validation). | | **`saveToJWT`** | If this field is top-level and nested in a config supporting [Authentication](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](./overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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](../rich-text/overview). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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](../admin/overview), you can use the `admin` option. The Rich Text Field inherits all the default options from the base [Field Admin Config](./overview#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](../rich-text/overview). # Row Field Source: https://payloadcms.com/docs/fields/row The Row Field is presentational-only and only affects the [Admin Panel](../admin/overview). By using it, you can arrange [Fields](./overview) next to each other horizontally. To add a Row Field, set the `type` to `row` in your [Field Config](./overview): ```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](./overview#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/fields/select The Select Field provides a dropdown-style interface for choosing options from a predefined list as an enumeration. To add a Select Field, set the `type` to `select` in your [Field Config](./overview): ```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](../fields/overview#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. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](../fields/overview#validation). | | **`index`** | Build an [index](../database/indexes) 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](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. See the [default field admin config](../fields/overview#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](../database/postgres)). 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](../database/postgres)). Auto-generated from name if not defined. | | **`interfaceName`** | Create a top level, reusable [Typescript interface](../typescript/generating-types#custom-field-interfaces) & [GraphQL type](../graphql/graphql-schema#custom-field-schemas). | | **`filterOptions`** | Dynamically filter which options are available based on the user, data, etc. [More details](#filteroptions) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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 To do this, use the `filterOptions` property in your [Field Config](./overview): ```ts import type { Field } from 'payload' export const MySelectField: Field = { // ... // highlight-start type: 'select', options: [ { label: 'One', value: 'one', }, { label: 'Two', value: 'two', }, { label: 'Three', value: 'three', }, ], filterOptions: ({ options, data }) => data.disallowOption1 ? options.filter( (option) => (typeof option === 'string' ? options : option.value) !== 'one', ) : options, // highlight-end } ``` **Note:** This property is similar to `filterOptions` in [Relationship](./relationship) or [Upload](./upload) fields, except that the return value of this function is simply an array of options, not a query constraint. ## Admin Options To customize the appearance and behavior of the Select Field in the [Admin Panel](../admin/overview), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MySelectField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Select Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options: | Property | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | **`isClearable`** | Set to `true` if you'd like this field to be clearable within the Admin UI. | | **`isSortable`** | Set to `true` if you'd like this field to be sortable within the Admin UI using drag and drop. (Only works when `hasMany` is set to `true`) | | **`placeholder`** | Define a custom text or function to replace the generic default placeholder | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'selectedFeatures', // required type: 'select', // required hasMany: true, admin: { isClearable: true, isSortable: true, // use mouse to drag and drop different values, and sort them according to your choice }, options: [ { label: 'Metallic Paint', value: 'metallic_paint', }, { label: 'Alloy Wheels', value: 'alloy_wheels', }, { label: 'Carbon Fiber Dashboard', value: 'carbon_fiber_dashboard', }, ], }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type { SelectFieldServerComponent } from 'payload' import type React from 'react' import { SelectField } from '@payloadcms/ui' export const CustomSelectFieldServer: SelectFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import type { SelectFieldClientComponent } from 'payload' import { SelectField } from '@payloadcms/ui' import React from 'react' export const CustomSelectFieldClient: SelectFieldClientComponent = (props) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { SelectFieldLabelServerComponent } from 'payload' export const CustomSelectFieldLabelServer: SelectFieldLabelServerComponent = ({ clientField, path, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { SelectFieldLabelClientComponent } from 'payload' export const CustomSelectFieldLabelClient: SelectFieldLabelClientComponent = ({ field, path, }) => { return ( ) } ``` # Tabs Field Source: https://payloadcms.com/docs/fields/tabs The Tabs Field is presentational-only and only affects the [Admin Panel](../admin/overview) (unless a tab is named). By using it, you can place fields within a nice layout component that separates certain sub-fields by a tabbed interface. To add a Tabs Field, set the `type` to `tabs` in your [Field Config](./overview): ```ts import type { Field } from 'payload' export const MyTabsField: Field = { // ... // highlight-start type: 'tabs', tabs: [ // ... ], // highlight-end } ``` ## Config Options | Option | Description | | ------------- | ----------------------------------------------------------------------- | | **`tabs`** \* | Array of tabs to render within this Tabs field. | | **`admin`** | Admin-specific configuration. [More details](./overview#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | ### Tab-specific Config Each tab must have either a `name` or `label` and the required `fields` array. You can also optionally pass a `description` to render within each individual tab. | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** | Groups field data into an object when stored and retrieved from the database. [More details](../fields/overview#field-names). | | **`label`** | The label to render on the tab itself. Required when name is undefined, defaults to name converted to words. | | **`fields`** \* | The fields to render within this tab. | | **`description`** | Optionally render a description within this tab to describe the contents of the tab itself. | | **`interfaceName`** | Create a top level, reusable [Typescript interface](../typescript/generating-types#custom-field-interfaces) & [GraphQL type](../graphql/graphql-schema#custom-field-schemas). (`name` must be present) | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). 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: [ { type: 'tabs', // required tabs: [ // required { label: 'Tab One Label', // required description: 'This will appear within the tab above the fields.', fields: [ // required { name: 'someTextField', type: 'text', required: true, }, ], }, { name: 'tabTwo', label: 'Tab Two Label', // required interfaceName: 'TabTwo', // optional (`name` must be present) fields: [ // required { name: 'numberField', // accessible via tabTwo.numberField type: 'number', required: true, }, ], }, ], }, ], } ``` # Text Field Source: https://payloadcms.com/docs/fields/text The Text Field is one of the most commonly used fields. It saves a string to the database and provides the [Admin Panel](../admin/overview) with a simple text input. To add a Text Field, set the `type` to `text` in your [Field Config](./overview): ```ts import type { Field } from 'payload' export const MyTextField: Field = { // ... type: 'text', // highlight-line } ``` ## Config Options | Option | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](../fields/overview#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. | | **`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](../fields/overview#validation). | | **`index`** | Build an [index](../database/indexes) 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](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [More details](#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`hasMany`** | Makes this field an ordered array of text instead of just a single text. | | **`minRows`** | Minimum number of texts in the array, if `hasMany` is set to true. | | **`maxRows`** | Maximum number of texts in the array, if `hasMany` is set to true. | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Text Field in the [Admin Panel](../admin/overview), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyTextField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Text Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options: | Option | Description | | ------------------ | --------------------------------------------------------------------------------------------------------------------------- | | **`placeholder`** | Set this property to define a placeholder string in the text input. | | **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. | | **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'pageTitle', // required type: 'text', // required required: true, }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { TextField } from '@payloadcms/ui' import type { TextFieldServerComponent } from 'payload' export const CustomTextFieldServer: TextFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { TextField } from '@payloadcms/ui' import type { TextFieldClientComponent } from 'payload' export const CustomTextFieldClient: TextFieldClientComponent = (props) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { TextFieldLabelServerComponent } from 'payload' export const CustomTextFieldLabelServer: TextFieldLabelServerComponent = ({ clientField, path, required, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { TextFieldLabelClientComponent } from 'payload' export const CustomTextFieldLabelClient: TextFieldLabelClientComponent = ({ field, path, }) => { return ( ) } ``` ## Slug Field The slug field is experimental and may change, or even be removed, in future releases. Use at your own risk. One common use case for the Text Field is to create a "slug" for a document. A slug is a unique, indexed, URL-friendly string that identifies a particular document, often used to construct the URL of a webpage. Payload provides a built-in Slug Field so you don't have to built one from scratch. This field automatically generates a slug based on the value of another field, such as a title or name field. It provides UI to lock and unlock the field to protect its value, as well as to re-generate the slug on-demand. To add a Slug Field, import the `slugField` into your field schema: ```ts import { slugField } from 'payload' import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { // ... fields: [ // ... // highlight-line slugField(), // highlight-line ], } ``` The slug field exposes a few top-level config options for easy customization: | Option | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `name` | To be used as the slug field's name. Defaults to `slug`. | | `overrides` | A function that receives the default fields so you can override on a granular level. See example below. [More details](#slug-overrides). | | `checkboxName` | To be used as the name for the `generateSlug` checkbox field. Defaults to `generateSlug`. | | `fieldToUse` | The name of the field to use when generating the slug. This field must exist in the same collection. Defaults to `title`. | | `localized` | Enable localization on the `slug` and `generateSlug` fields. Defaults to `false`. | | `position` | The position of the slug field. [More details](./overview#admin-options). | | `required` | Require the slug field. Defaults to `true`. | ### Slug Overrides If the above options aren't sufficient for your use case, you can use the `overrides` function to customize the slug field at a granular level. The `overrides` function receives the default fields that make up the slug field, and you can modify them to any extent you need. ```ts import { slugField } from 'payload' import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { // ... fields: [ // ... // highlight-line slugField({ overrides: (defaultField) => { defaultField.fields[1].label = 'Custom Slug Label' return defaultField }, }), // highlight-line ], } ``` # Textarea Field Source: https://payloadcms.com/docs/fields/textarea The Textarea Field is nearly identical to the [Text Field](./text) but it features a slightly larger input that is better suited to edit longer text. To add a Textarea Field, set the `type` to `textarea` in your [Field Config](./overview): ```ts import type { Field } from 'payload' export const MyTextareaField: Field = { // ... type: 'textarea', // highlight-line } ``` ## Config Options | Option | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](../fields/overview#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. | | **`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](../fields/overview#validation). | | **`index`** | Build an [index](../database/indexes) 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](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) 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) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | _\* An asterisk denotes that a property is required._ ## Admin Options To customize the appearance and behavior of the Textarea Field in the [Admin Panel](../admin/overview), you can use the `admin` option: ```ts import type { Field } from 'payload' export const MyTextareaField: Field = { // ... admin: { // highlight-line // ... }, } ``` The Textarea Field inherits all of the default admin options from the base [Field Admin Config](./overview#admin-options), plus the following additional options: | Option | Description | | ------------------ | --------------------------------------------------------------------------------------------------------------------------- | | **`placeholder`** | Set this property to define a placeholder string in the textarea. | | **`autoComplete`** | Set this property to a string that will be used for browser autocomplete. | | **`rows`** | Set the number of visible text rows in the textarea. Defaults to `2` if not specified. | | **`rtl`** | Override the default text direction of the Admin Panel for this field. Set to `true` to force right-to-left text direction. | ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'metaDescription', // required type: 'textarea', // required required: true, }, ], } ``` ## Custom Components ### Field #### Server Component ```tsx import type React from 'react' import { TextareaField } from '@payloadcms/ui' import type { TextareaFieldServerComponent } from 'payload' export const CustomTextareaFieldServer: TextareaFieldServerComponent = ({ clientField, path, schemaPath, permissions, }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { TextareaField } from '@payloadcms/ui' import type { TextareaFieldClientComponent } from 'payload' export const CustomTextareaFieldClient: TextareaFieldClientComponent = ( props, ) => { return } ``` ### Label #### Server Component ```tsx import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { TextareaFieldLabelServerComponent } from 'payload' export const CustomTextareaFieldLabelServer: TextareaFieldLabelServerComponent = ({ clientField, path }) => { return ( ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { FieldLabel } from '@payloadcms/ui' import type { TextareaFieldLabelClientComponent } from 'payload' export const CustomTextareaFieldLabelClient: TextareaFieldLabelClientComponent = ({ field, path }) => { return ( ) } ``` # UI Field Source: https://payloadcms.com/docs/fields/ui The UI (user interface) Field gives you a ton of power to add your own React components directly into the [Admin Panel](../admin/overview), nested directly within your other fields. It has absolutely no effect on the data of your documents. It is presentational-only. Think of it as a way for you to "plug in" your own React components directly within your other fields, so you can provide your editors with new controls exactly where you want them to go. With the UI Field, you can: - Add a custom message or block of text within the body of an Edit View to describe the purpose of surrounding fields - Add a "Refund" button to an Order's Edit View sidebar, which might make a fetch call to a custom `refund` endpoint - Add a "view page" button into a Pages List View to give editors a shortcut to view a page on the frontend of the site - Build a "clear cache" button or similar mechanism to manually clear caches of specific documents To add a UI Field, set the `type` to `ui` in your [Field Config](./overview): ```ts import type { Field } from 'payload' export const MyUIField: Field = { // ... type: 'ui', // highlight-line } ``` ## Config Options | Option | Description | | ------------------------------- | ---------------------------------------------------------------------------------------------------------- | | **`name`** \* | A unique identifier for this field. | | **`label`** | Human-readable label for this UI field. | | **`admin.components.Field`** \* | React component to be rendered for this field within the Edit View. [More details](./overview#field). | | **`admin.components.Cell`** | React component to be rendered as a Cell within collection List views. [More details](./overview#cell). | | **`admin.disableListColumn`** | Set `disableListColumn` to `true` to prevent the UI field from appearing in the list view column selector. | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | _\* An asterisk denotes that a property is required._ ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'myCustomUIField', // required type: 'ui', // required admin: { components: { Field: '/path/to/MyCustomUIField', Cell: '/path/to/MyCustomUICell', }, }, }, ], } ``` # Upload Field Source: https://payloadcms.com/docs/fields/upload The Upload Field allows for the selection of a Document from a Collection supporting [Uploads](../upload/overview), and formats the selection as a thumbnail in the Admin Panel. Upload fields are useful for a variety of use cases, such as: - To provide a `Page` with a featured image - To allow for a `Product` to deliver a downloadable asset like PDF or MP3 - To give a layout building block the ability to feature a background image To create an Upload Field, set the `type` to `upload` in your [Field Config](./overview): ```ts import type { Field } from 'payload' export const MyUploadField: Field = { // ... // highlight-start type: 'upload', relationTo: 'media', // highlight-end } ``` **Important:** To use the Upload Field, you must have a [Collection](../configuration/collections) configured to allow [Uploads](../upload/overview). ## Config Options | Option | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](../fields/overview#field-names). | | **`relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. **Note: the related collection must be configured to support Uploads.** | | **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-upload-options). | | **`hasMany`** | Boolean which, if set to true, allows this field to have many relations instead of only one. | | **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with hasMany. | | **`maxRows`** | A number for the most allowed items during validation when a value is present. Used with hasMany. | | **`maxDepth`** | Sets a number limit on iterations of related documents to populate when queried. [Depth](../queries/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. | | **`validate`** | Provide a custom validation function that will be executed on both the Admin Panel and the backend. [More details](../fields/overview#validation). | | **`index`** | Build an [index](../database/indexes) 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](../authentication/overview), include its data in the user JWT. | | **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). | | **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). | | **`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](../fields/overview#default-values). | | **`displayPreview`** | Enable displaying preview of the uploaded file. Overrides related Collection's `displayPreview` option. [More details](../upload/overview#collection-upload-options). | | **`localized`** | Enable localization for this field. Requires [localization to be enabled](../configuration/localization) in the Base config. | | **`required`** | Require this field to have a value. | | **`admin`** | Admin-specific configuration. [Admin Options](./overview#admin-options). | | **`custom`** | Extension point for adding custom data (e.g. for plugins) | | **`typescriptSchema`** | Override field type generation with providing a JSON schema | | **`virtual`** | Provide `true` to disable field in the database, or provide a string path to [link the field with a relationship](../fields/relationship#linking-virtual-fields-with-relationships). See [Virtual Fields](https://payloadcms.com/blog/learn-how-virtual-fields-can-help-solve-common-cms-challenges) | | **`graphQL`** | Custom graphQL configuration for the field. [More details](../graphql/overview#field-complexity) | _\* An asterisk denotes that a property is required._ ## Example ```ts import type { CollectionConfig } from 'payload' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'backgroundImage', // required type: 'upload', // required relationTo: 'media', // required required: true, }, ], } ``` ## Filtering upload options Options can be dynamically limited by supplying a [query constraint](../queries/overview), which will be used both for validating input and filtering available uploads in the UI. The `filterOptions` property can either be a `Where` query, or a function returning `true` to not filter, `false` to prevent all, or a `Where` query. When using a function, it will be called with an argument object with the following properties: | Property | Description | | ------------- | ----------------------------------------------------------------------------------------------------- | | `relationTo` | The collection `slug` to filter against, limited to this field's `relationTo` property | | `data` | An object containing the full collection or global document currently being edited | | `siblingData` | An object containing document data that is scoped to only fields within the same parent of this field | | `id` | The `id` of the current document being edited. `id` is `undefined` during the `create` operation | | `user` | An object containing the currently authenticated user | | `req` | The Payload Request, which contains references to `payload`, `user`, `locale`, and more. | ### Example#filter-options-example ```ts const uploadField = { name: 'image', type: 'upload', relationTo: 'media', filterOptions: { mimeType: { contains: 'image' }, }, } ``` You can learn more about writing queries [here](../queries/overview). **Note:** When an upload field has both **filterOptions** and a custom **validate** function, the api will not validate **filterOptions** unless you call the default upload field validation function imported from **payload/shared** in your validate function. ## Bi-directional relationships The `upload` field on its own is used to reference documents in an upload collection. This can be considered a "one-way" relationship. If you wish to allow an editor to visit the upload document and see where it is being used, you may use the `join` field in the upload enabled collection. Read more about bi-directional relationships using the [Join field](./join) # Access Control Source: https://payloadcms.com/docs/access-control/overview Access Control determines what a user can and cannot do with any given Document, as well as what they can and cannot see within the [Admin Panel](../admin/overview). By implementing Access Control, you can define granular restrictions based on the user, their roles (RBAC), Document data, or any other criteria your application requires. Access Control functions are scoped to the _operation_, meaning you can have different rules for `create`, `read`, `update`, `delete`, etc. Access Control functions are executed _before_ any changes are made and _before_ any operations are completed. This allows you to determine if the user has the necessary permissions before fulfilling the request. There are many use cases for Access Control, including: - Allowing anyone `read` access to all posts - Only allowing public access to posts where a `status` field is equal to `published` - Giving only users with a `role` field equal to `admin` the ability to delete posts - Allowing anyone to submit contact forms, but only logged in users to `read`, `update` or `delete` them - Restricting a user to only be able to see their own orders, but no-one else's - Allowing users that belong to a certain organization to access only that organization's resources There are three main types of Access Control in Payload: - [Collection Access Control](./collections) - [Global Access Control](./globals) - [Field Access Control](./fields) ## Default Access Control Payload provides default Access Control so that your data is secured behind [Authentication](../authentication/overview) without additional configuration. To do this, Payload sets a default function that simply checks if a user is present on the request. You can override this default behavior by defining your own Access Control functions as needed. Here is the default Access Control that Payload provides: ```ts const defaultPayloadAccess = ({ req: { user } }) => { // Return `true` if a user is found // and `false` if it is undefined or null return Boolean(user) // highlight-line } ``` **Important:** In the [Local API](../local-api/overview), all Access Control is _skipped_ by default. This allows your server to have full control over your application. To opt back in, you can set the `overrideAccess` option to `false` in your requests. ## The Access Operation The Admin Panel responds dynamically to your changes to Access Control. For example, if you restrict editing `ExampleCollection` to only users that feature an "admin" role, Payload will **hide** that Collection from the Admin Panel entirely. This is super powerful and allows you to control who can do what within your Admin Panel using the same functions that secure your APIs. To accomplish this, Payload exposes the [Access Operation](../authentication/operations#access). Upon login, Payload executes each Access Control function at the top level, across all Collections, Globals, and Fields, and returns a response that contains a reflection of what the currently authenticated user can do within your application. **Important:** When your access control functions are executed via the [Access Operation](../authentication/operations#access), the `id` and `data` arguments will be `undefined`. This is because Payload is executing your functions without referencing a specific Document. If you use `id` or `data` within your access control functions, make sure to check that they are defined first. If they are not, then you can assume that your Access Control is being executed via the Access Operation to determine solely what the user can do within the Admin Panel. ## Locale Specific Access Control To implement locale-specific access control, you can use the `req.locale` argument in your access control functions. This argument allows you to evaluate the current locale of the request and determine access permissions accordingly. Here is an example: ```ts const access = ({ req }) => { // Grant access if the locale is 'en' if (req.locale === 'en') { return true } // Deny access for all other locales return false } ``` # Collection Access Control Source: https://payloadcms.com/docs/access-control/collections Collection Access Control is [Access Control](../access-control/overview) used to restrict access to Documents within a [Collection](../getting-started/concepts#collections), as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Collection. To add Access Control to a Collection, use the `access` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithAccessControl: CollectionConfig = { // ... access: { // highlight-line // ... }, } ``` ## Config Options Access Control is specific to the operation of the request. To add Access Control to a Collection, use the `access` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload'; export const CollectionWithAccessControl: CollectionConfig = { // ... // highlight-start access: { create: () => {...}, read: () => {...}, update: () => {...}, delete: () => {...}, // Auth-enabled Collections only admin: () => {...}, unlock: () => {...}, // Version-enabled Collections only readVersions: () => {...}, }, // highlight-end } ``` The following options are available: | Function | Allows/Denies Access | | ------------ | -------------------------------------------------------------------- | | **`create`** | Used in the `create` operation. [More details](#create). | | **`read`** | Used in the `find` and `findByID` operations. [More details](#read). | | **`update`** | Used in the `update` operation. [More details](#update). | | **`delete`** | Used in the `delete` operation. [More details](#delete). | If a Collection supports [`Authentication`](../authentication/overview), the following additional options are available: | Function | Allows/Denies Access | | ------------ | ---------------------------------------------------------------------------------------- | | **`admin`** | Used to restrict access to the [Admin Panel](../admin/overview). [More details](#admin). | | **`unlock`** | Used to restrict which users can access the `unlock` operation. [More details](#unlock). | If a Collection supports [Versions](../versions/overview), the following additional options are available: | Function | Allows/Denies Access | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`readVersions`** | Used to control who can read versions, and who can't. Will automatically restrict the Admin UI version viewing access. [More details](#read-versions). | ### Create Returns a boolean which allows/denies access to the `create` request. To add create Access Control to a Collection, use the `create` property in the [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithCreateAccess: CollectionConfig = { // ... access: { // highlight-start create: ({ req: { user }, data }) => { return Boolean(user) }, // highlight-end }, } ``` The following arguments are provided to the `create` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`data`** | The data passed to create the document with. | ### Read Returns a boolean which allows/denies access to the `read` request. To add read Access Control to a Collection, use the `read` property in the [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithReadAccess: CollectionConfig = { // ... access: { // highlight-start read: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` **Tip:** Return a [Query](../queries/overview) to limit the Documents to only those that match the constraint. This can be helpful to restrict users' access to specific Documents. [More details](../queries/overview). As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config: ```ts import type { Access } from 'payload' export const canReadPage: Access = ({ req: { user } }) => { // Allow authenticated users if (user) { return true } // By returning a Query, guest users can read public Documents // Note: this assumes you have a `isPublic` checkbox field on your Collection return { isPublic: { equals: true, }, } } ``` The following arguments are provided to the `read` function: | Option | Description | | --------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`id`** | `id` of document requested, if within `findByID`. | ### Update Returns a boolean which allows/denies access to the `update` request. To add update Access Control to a Collection, use the `update` property in the [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithUpdateAccess: CollectionConfig = { // ... access: { // highlight-start update: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` **Tip:** Return a [Query](../queries/overview) to limit the Documents to only those that match the constraint. This can be helpful to restrict users' access to specific Documents. [More details](../queries/overview). As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config: ```ts import type { Access } from 'payload' export const canUpdateUser: Access = ({ req: { user }, id }) => { // Allow users with a role of 'admin' if (user.roles && user.roles.some((role) => role === 'admin')) { return true } // allow any other users to update only oneself return user.id === id } ``` The following arguments are provided to the `update` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`id`** | `id` of document requested to update. | | **`data`** | The data passed to update the document with. | ### Delete Similarly to the Update function, returns a boolean or a [query constraint](../queries/overview) to limit which documents can be deleted by which users. To add delete Access Control to a Collection, use the `delete` property in the [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithDeleteAccess: CollectionConfig = { // ... access: { // highlight-start delete: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` As your application becomes more complex, you may want to define your function in a separate file and import them into your Collection Config: ```ts import type { Access } from 'payload' export const canDeleteCustomer: Access = async ({ req, id }) => { if (!id) { // allow the admin UI to show controls to delete since it is indeterminate without the `id` return true } // Query another Collection using the `id` const result = await req.payload.find({ collection: 'contracts', limit: 0, depth: 0, where: { customer: { equals: id }, }, }) return result.totalDocs === 0 } ``` The following arguments are provided to the `delete` function: | Option | Description | | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object with additional `user` property, which is the currently logged in user. | | **`id`** | `id` of document requested to delete. | ### Admin If the Collection is used to access the [Admin Panel](../admin/overview#the-admin-user-collection), the `Admin` Access Control function determines whether or not the currently logged in user can access the admin UI. To add Admin Access Control to a Collection, use the `admin` property in the [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithAdminAccess: CollectionConfig = { // ... access: { // highlight-start admin: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` The following arguments are provided to the `admin` function: | Option | Description | | --------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | ### Unlock Determines which users can [unlock](../authentication/operations#unlock) other users who may be blocked from authenticating successfully due to [failing too many login attempts](../authentication/overview#config-options). To add Unlock Access Control to a Collection, use the `unlock` property in the [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithUnlockAccess: CollectionConfig = { // ... access: { // highlight-start unlock: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` The following arguments are provided to the `unlock` function: | Option | Description | | --------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | ### Read Versions If the Collection has [Versions](../versions/overview) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document. To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithVersionsAccess: CollectionConfig = { // ... access: { // highlight-start readVersions: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` The following arguments are provided to the `readVersions` function: | Option | Description | | --------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | # Globals Access Control Source: https://payloadcms.com/docs/access-control/globals Global Access Control is [Access Control](../access-control/overview) used to restrict access to [Global](../configuration/globals) Documents, as well as what they can and cannot see within the [Admin Panel](../admin/overview) as it relates to that Global. To add Access Control to a Global, use the `access` property in your [Global Config](../configuration/globals): ```ts import type { GlobalConfig } from 'payload' export const GlobalWithAccessControl: GlobalConfig = { // ... access: { // highlight-line // ... }, } ``` ## Config Options Access Control is specific to the operation of the request. To add Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals): ```ts import { GlobalConfig } from 'payload' const GlobalWithAccessControl: GlobalConfig = { // ... // highlight-start access: { read: ({ req: { user } }) => {...}, update: ({ req: { user } }) => {...}, // Version-enabled Globals only readVersions: () => {...}, }, // highlight-end } export default Header ``` The following options are available: | Function | Allows/Denies Access | | ------------ | --------------------------------------------------------------- | | **`read`** | Used in the `findOne` Global operation. [More details](#read). | | **`update`** | Used in the `update` Global operation. [More details](#update). | If a Global supports [Versions](../versions/overview), the following additional options are available: | Function | Allows/Denies Access | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`readVersions`** | Used to control who can read versions, and who can't. Will automatically restrict the Admin UI version viewing access. [More details](#read-versions). | ### Read Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can read this global based on its current properties. To add read Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals): ```ts import { GlobalConfig } from 'payload' const Header: GlobalConfig = { // ... // highlight-start access: { read: ({ req: { user } }) => { return Boolean(user) }, }, // highlight-end } ``` The following arguments are provided to the `read` function: | Option | Description | | --------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | ### Update Returns a boolean result or optionally a [query constraint](../queries/overview) which limits who can update this global based on its current properties. To add update Access Control to a [Global](../configuration/globals), use the `access` property in the [Global Config](../configuration/globals): ```ts import { GlobalConfig } from 'payload' const Header: GlobalConfig = { // ... // highlight-start access: { update: ({ req: { user }, data }) => { return Boolean(user) }, }, // highlight-end } ``` The following arguments are provided to the `update` function: | Option | Description | | ---------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | | **`data`** | The data passed to update the global with. | ### Read Versions If the Global has [Versions](../versions/overview) enabled, the `readVersions` Access Control function determines whether or not the currently logged in user can access the version history of a Document. To add Read Versions Access Control to a Collection, use the `readVersions` property in the [Global Config](../configuration/globals): ```ts import type { GlobalConfig } from 'payload' export const GlobalWithVersionsAccess: GlobalConfig = { // ... access: { // highlight-start readVersions: ({ req: { user } }) => { return Boolean(user) }, // highlight-end }, } ``` The following arguments are provided to the `readVersions` function: | Option | Description | | --------- | ----------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user`. | # Field-level Access Control Source: https://payloadcms.com/docs/access-control/fields Field Access Control is [Access Control](../access-control/overview) used to restrict access to specific [Fields](../fields/overview) within a Document. To add Access Control to a Field, use the `access` property in your [Field Config](../fields/overview): ```ts import type { Field } from 'payload' export const FieldWithAccessControl: Field = { // ... access: { // highlight-line // ... }, } ``` **Note:** Field Access Controls does not support returning [Query](../queries/overview) constraints like [Collection Access Control](./collections) does. ## Config Options Access Control is specific to the operation of the request. To add Access Control to a Field, use the `access` property in the [Field Config](../fields/overview): ```ts import type { CollectionConfig } from 'payload'; export const Posts: CollectionConfig = { slug: 'posts', fields: [ { name: 'title', type: 'text', // highlight-start access: { create: ({ req: { user } }) => { ... }, read: ({ req: { user } }) => { ... }, update: ({ req: { user } }) => { ... }, }, // highlight-end }; ], }; ``` The following options are available: | Function | Purpose | | ------------ | ---------------------------------------------------------------------------------------------------------- | | **`create`** | Allows or denies the ability to set a field's value when creating a new document. [More details](#create). | | **`read`** | Allows or denies the ability to read a field's value. [More details](#read). | | **`update`** | Allows or denies the ability to update a field's value [More details](#update). | ### Create Returns a boolean which allows or denies the ability to set a field's value when creating a new document. If `false` is returned, any passed values will be discarded. **Available argument properties:** | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` | | **`data`** | The full data passed to create the document. | | **`siblingData`** | Immediately adjacent field data passed to create the document. | ### Read Returns a boolean which allows or denies the ability to read a field's value. If `false`, the entire property is omitted from the resulting document. **Available argument properties:** | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` | | **`id`** | `id` of the document being read | | **`doc`** | The full document data. | | **`siblingData`** | Immediately adjacent field data of the document being read. | ### Update Returns a boolean which allows or denies the ability to update a field's value. If `false` is returned, any passed values will be discarded. If `false` is returned and you attempt to update the field's value, the operation will **not** throw an error however the field will be omitted from the update operation and the value will remain unchanged. **Available argument properties:** | Option | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **`req`** | The [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object containing the currently authenticated `user` | | **`id`** | `id` of the document being updated | | **`data`** | The full data passed to update the document. | | **`siblingData`** | Immediately adjacent field data passed to update the document with. | | **`doc`** | The full document data, before the update is applied. | # Hooks Overview Source: https://payloadcms.com/docs/hooks/overview Hooks allow you to execute your own side effects during specific events of the Document lifecycle. They allow you to do things like mutate data, perform business logic, integrate with third-parties, or anything else, all during precise moments within your application. With Hooks, you can transform Payload from a traditional CMS into a fully-fledged application framework. There are many use cases for Hooks, including: - Modify data before it is read or updated - Encrypt and decrypt sensitive data - Integrate with a third-party CRM like HubSpot or Salesforce - Send a copy of uploaded files to Amazon S3 or similar - Process orders through a payment provider like Stripe - Send emails when contact forms are submitted - Track data ownership or changes over time There are four main types of Hooks in Payload: - [Root Hooks](#root-hooks) - [Collection Hooks](../hooks/collections) - [Global Hooks](../hooks/globals) - [Field Hooks](../hooks/fields) **Reminder:** Payload also ships a set of _React_ hooks that you can use in your frontend application. Although they share a common name, these are very different things and should not be confused. [More details](../admin/react-hooks). ## Root Hooks Root Hooks are not associated with any specific Collection, Global, or Field. They are useful for globally-oriented side effects, such as when an error occurs at the application level. To add Root Hooks, use the `hooks` property in your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... // highlight-start hooks: { afterError:[() => {...}] }, // highlight-end }) ``` The following options are available: | Option | Description | | ---------------- | ------------------------------------------------------ | | **`afterError`** | Runs after an error occurs in the Payload application. | ### afterError The `afterError` Hook is triggered when an error occurs in the Payload application. This can be useful for logging errors to a third-party service, sending an email to the development team, logging the error to Sentry or DataDog, etc. The output can be used to transform the result object / status code. ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... hooks: { afterError: [ async ({ error }) => { // Do something }, ], }, }) ``` The following arguments are provided to the `afterError` Hook: | Argument | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`error`** | The error that occurred. | | **`context`** | Custom context passed between Hooks. [More details](./context). | | **`graphqlResult`** | The GraphQL result object, available if the hook is executed within a GraphQL context. | | **`req`** | The `PayloadRequest` object that extends [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request). Contains currently authenticated `user` and the Local API instance `payload`. | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. This will be `undefined` if the hook is executed from a non-collection endpoint or GraphQL. | | **`result`** | The formatted error result object, available if the hook is executed from a REST context. | ## Async vs. Synchronous All Hooks can be written as either synchronous or asynchronous functions. Choosing the right type depends on your use case, but switching between the two is as simple as adding or removing the `async` keyword. #### Asynchronous If the Hook should modify data before a Document is updated or created, and it relies on asynchronous actions such as fetching data from a third party, it might make sense to define your Hook as an asynchronous function. This way you can be sure that your Hook completes before the operation's lifecycle continues. Async hooks are run in series - so if you have two async hooks defined, the second hook will wait for the first to complete before it starts. **Tip:** If your hook executes a long-running task that doesn't affect the response in any way, consider [offloading it to the job queue](#offloading-long-running-tasks). That will free up the request to continue processing without waiting for the task to complete. #### Synchronous If your Hook simply performs a side-effect, such as mutating document data, it might be okay to define it synchronously, so the Payload operation does not have to wait for your hook to complete. ## Server-only Execution Hooks are only triggered on the server and are automatically excluded from the client-side bundle. This means that you can safely use sensitive business logic in your Hooks without worrying about exposing it to the client. ## Performance Hooks are a powerful way to customize the behavior of your APIs, but some hooks are run very often and can add significant overhead to your requests if not optimized. When building hooks, combine together as many of these strategies as possible to ensure your hooks are as performant as they can be. For more performance tips, see the [Performance documentation](../performance/overview). ### Writing efficient hooks Consider when hooks are run. One common pitfall is putting expensive logic in hooks that run very often. For example, the `read` operation runs on every read request, so avoid putting expensive logic in a `beforeRead` or `afterRead` hook. ```ts { hooks: { beforeRead: [ async () => { // This runs on every read request - avoid expensive logic here await doSomethingExpensive() return data }, ], }, } ``` Instead, you might want to use a `beforeChange` or `afterChange` hook, which only runs when a document is created or updated. ```ts { hooks: { beforeChange: [ async ({ context }) => { // This is more acceptable here, although still should be mindful of performance await doSomethingExpensive() // ... }, ] }, } ``` ### Using Hook Context Use [Hook Context](./context) avoid prevent infinite loops or avoid repeating expensive operations across multiple hooks in the same request. ```ts { hooks: { beforeChange: [ async ({ context }) => { const somethingExpensive = await doSomethingExpensive() context.somethingExpensive = somethingExpensive // ... }, ], }, } ``` To learn more, see the [Hook Context documentation](./context). ### Offloading to the jobs queue If your hooks perform any long-running tasks that don't direct affect request lifecycle, consider offloading them to the [jobs queue](../jobs-queue/overview). This will free up the request to continue processing without waiting for the task to complete. ```ts { hooks: { afterChange: [ async ({ doc, req }) => { // Offload to job queue await req.payload.jobs.queue(...) // ... }, ], }, } ``` To learn more, see the [Job Queue documentation](../jobs-queue/overview). # Collection Hooks Source: https://payloadcms.com/docs/hooks/collections Collection Hooks are [Hooks](./overview) that run on Documents within a specific [Collection](../configuration/collections). They allow you to execute your own logic during specific events of the Document lifecycle. To add Hooks to a Collection, use the `hooks` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const CollectionWithHooks: CollectionConfig = { // ... hooks: { // highlight-line // ... }, } ``` **Tip:** You can also set hooks on the field-level to isolate hook logic to specific fields. [More details](./fields). ## Config Options All Collection Hooks accept an array of [synchronous or asynchronous functions](./overview#async-vs-synchronous). Each Collection Hook receives specific arguments based on its own type, and has the ability to modify specific outputs. ```ts import type { CollectionConfig } from 'payload'; export const CollectionWithHooks: CollectionConfig = { // ... // highlight-start hooks: { beforeOperation: [(args) => {...}], beforeValidate: [(args) => {...}], beforeDelete: [(args) => {...}], beforeChange: [(args) => {...}], beforeRead: [(args) => {...}], afterChange: [(args) => {...}], afterRead: [(args) => {...}], afterDelete: [(args) => {...}], afterOperation: [(args) => {...}], afterError: [(args) => {....}], // Auth-enabled Hooks beforeLogin: [(args) => {...}], afterLogin: [(args) => {...}], afterLogout: [(args) => {...}], afterRefresh: [(args) => {...}], afterMe: [(args) => {...}], afterForgotPassword: [(args) => {...}], refresh: [(args) => {...}], me: [(args) => {...}], }, // highlight-end } ``` ### beforeOperation The `beforeOperation` hook can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins. ```ts import type { CollectionBeforeOperationHook } from 'payload' const beforeOperationHook: CollectionBeforeOperationHook = async ({ args, operation, req, }) => { return args // return modified operation arguments as necessary } ``` The following arguments are provided to the `beforeOperation` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. Available options include: `autosave`, `count`, `countVersions`, `create`, `delete`, `forgotPassword`, `login`, `read`, `readDistinct`, `refresh`, `resetPassword`, `restoreVersion`, and `update`. | | **`context`** | Custom context passed between Hooks. [More details](./context). | | **`operation`** | The name of the operation that this hook is running within. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### beforeValidate Runs during the `create` and `update` operations. This hook allows you to add or format data before the incoming data is validated server-side. Please do note that this does not run before client-side validation. If you render a custom field component in your front-end and provide it with a `validate` function, the order that validations will run in is: 1. `validate` runs on the client 2. if successful, `beforeValidate` runs on the server 3. `validate` runs on the server ```ts import type { CollectionBeforeValidateHook } from 'payload' const beforeValidateHook: CollectionBeforeValidateHook = async ({ data }) => { return data } ``` The following arguments are provided to the `beforeValidate` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between Hooks. [More details](./context). | | **`data`** | The incoming data passed through the operation. | | **`operation`** | The name of the operation that this hook is running within. | | **`originalDoc`** | The Document before changes are applied. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### beforeChange Immediately before validation, beforeChange hooks will run during create and update operations. At this stage, the data should be treated as unvalidated user input. There is no guarantee that required fields exist or that fields are in the correct format. As such, using this data for side effects requires manual validation. You can optionally modify the shape of the data to be saved. ```ts import type { CollectionBeforeChangeHook } from 'payload' const beforeChangeHook: CollectionBeforeChangeHook = async ({ data }) => { return data } ``` The following arguments are provided to the `beforeChange` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`data`** | The incoming data passed through the operation. | | **`operation`** | The name of the operation that this hook is running within. | | **`originalDoc`** | The Document before changes are applied. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### afterChange After a document is created or updated, the `afterChange` hook runs. This hook is helpful to recalculate statistics such as total sales within a global, syncing user profile changes to a CRM, and more. ```ts import type { CollectionAfterChangeHook } from 'payload' const afterChangeHook: CollectionAfterChangeHook = async ({ doc }) => { return doc } ``` The following arguments are provided to the `afterChange` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`data`** | The incoming data passed through the operation. | | **`doc`** | The resulting Document after changes are applied. | | **`operation`** | The name of the operation that this hook is running within. | | **`previousDoc`** | The Document before changes were applied. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### beforeRead Runs before `find` and `findByID` operations are transformed for output by `afterRead`. This hook fires before hidden fields are removed and before localized fields are flattened into the requested locale. Using this Hook will provide you with all locales and all hidden fields via the `doc` argument. ```ts import type { CollectionBeforeReadHook } from 'payload' const beforeReadHook: CollectionBeforeReadHook = async ({ doc }) => { return doc } ``` The following arguments are provided to the `beforeRead` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`doc`** | The resulting Document after changes are applied. | | **`query`** | The [Query](../queries/overview) of the request. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### afterRead Runs as the last step before documents are returned. Flattens locales, hides protected fields, and removes fields that users do not have access to. ```ts import type { CollectionAfterReadHook } from 'payload' const afterReadHook: CollectionAfterReadHook = async ({ doc }) => { return doc } ``` The following arguments are provided to the `afterRead` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`doc`** | The resulting Document after changes are applied. | | **`query`** | The [Query](../queries/overview) of the request. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### beforeDelete Runs before the `delete` operation. Returned values are discarded. ```ts import type { CollectionBeforeDeleteHook } from 'payload'; const beforeDeleteHook: CollectionBeforeDeleteHook = async ({ req, id, }) => {...} ``` The following arguments are provided to the `beforeDelete` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`id`** | The ID of the Document being deleted. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### afterDelete Runs immediately after the `delete` operation removes records from the database. Returned values are discarded. ```ts import type { CollectionAfterDeleteHook } from 'payload'; const afterDeleteHook: CollectionAfterDeleteHook = async ({ req, id, doc, }) => {...} ``` The following arguments are provided to the `afterDelete` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`doc`** | The resulting Document after changes are applied. | | **`id`** | The ID of the Document that was deleted. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### afterOperation The `afterOperation` hook can be used to modify the result of operations or execute side-effects that run after an operation has completed. Available Collection operations include `create`, `find`, `findByID`, `update`, `updateByID`, `delete`, `deleteByID`, `login`, `refresh`, and `forgotPassword`. ```ts import type { CollectionAfterOperationHook } from 'payload' const afterOperationHook: CollectionAfterOperationHook = async ({ result }) => { return result } ``` The following arguments are provided to the `afterOperation` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`args`** | The arguments passed into the operation. | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | | **`operation`** | The name of the operation that this hook is running within. | | **`result`** | The result of the operation, before modifications. | ### afterError The `afterError` Hook is triggered when an error occurs in the Payload application. This can be useful for logging errors to a third-party service, sending an email to the development team, logging the error to Sentry or DataDog, etc. The output can be used to transform the result object / status code. ```ts import type { CollectionAfterErrorHook } from 'payload'; const afterErrorHook: CollectionAfterErrorHook = async ({ req, id, doc, }) => {...} ``` The following arguments are provided to the `afterError` Hook: | Argument | Description | | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`error`** | The error that occurred. | | **`context`** | Custom context passed between Hooks. [More details](./context). | | **`graphqlResult`** | The GraphQL result object, available if the hook is executed within a GraphQL context. | | **`req`** | The `PayloadRequest` object that extends [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request). Contains currently authenticated `user` and the Local API instance `payload`. | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`result`** | The formatted error result object, available if the hook is executed from a REST context. | ### beforeLogin For [Auth-enabled Collections](../authentication/overview), this hook runs during `login` operations where a user with the provided credentials exist, but before a token is generated and added to the response. You can optionally modify the user that is returned, or throw an error in order to deny the login operation. ```ts import type { CollectionBeforeLoginHook } from 'payload' const beforeLoginHook: CollectionBeforeLoginHook = async ({ user }) => { return user } ``` The following arguments are provided to the `beforeLogin` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | | **`user`** | The user being logged in. | ### afterLogin For [Auth-enabled Collections](../authentication/overview), this hook runs after successful `login` operations. You can optionally modify the user that is returned. ```ts import type { CollectionAfterLoginHook } from 'payload'; const afterLoginHook: CollectionAfterLoginHook = async ({ user, token, }) => {...} ``` The following arguments are provided to the `afterLogin` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | | **`token`** | The token generated for the user. | | **`user`** | The user being logged in. | ### afterLogout For [Auth-enabled Collections](../authentication/overview), this hook runs after `logout` operations. ```ts import type { CollectionAfterLogoutHook } from 'payload'; const afterLogoutHook: CollectionAfterLogoutHook = async ({ req, }) => {...} ``` The following arguments are provided to the `afterLogout` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### afterMe For [Auth-enabled Collections](../authentication/overview), this hook runs after `me` operations. ```ts import type { CollectionAfterMeHook } from 'payload'; const afterMeHook: CollectionAfterMeHook = async ({ req, response, }) => {...} ``` The following arguments are provided to the `afterMe` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | | **`response`** | The response to return. | ### afterRefresh For [Auth-enabled Collections](../authentication/overview), this hook runs after `refresh` operations. ```ts import type { CollectionAfterRefreshHook } from 'payload'; const afterRefreshHook: CollectionAfterRefreshHook = async ({ token, }) => {...} ``` The following arguments are provided to the `afterRefresh` hook: | Option | Description | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`exp`** | The expiration time of the token. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | | **`token`** | The newly refreshed user token. | ### afterForgotPassword For [Auth-enabled Collections](../authentication/overview), this hook runs after successful `forgotPassword` operations. Returned values are discarded. ```ts import type { CollectionAfterForgotPasswordHook } from 'payload' const afterForgotPasswordHook: CollectionAfterForgotPasswordHook = async ({ args, context, collection, }) => {...} ``` The following arguments are provided to the `afterForgotPassword` hook: | Option | Description | | ---------------- | ------------------------------------------------------------------------------------- | | **`args`** | The arguments passed into the operation. | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | ### refresh For [Auth-enabled Collections](../authentication/overview), this hook allows you to optionally replace the default behavior of the `refresh` operation with your own. If you optionally return a value from your hook, the operation will not perform its own logic and continue. ```ts import type { CollectionRefreshHook } from 'payload' const myRefreshHook: CollectionRefreshHook = async ({ args, user, }) => {...} ``` The following arguments are provided to the `afterRefresh` hook: | Option | Description | | ---------- | ---------------------------------------- | | **`args`** | The arguments passed into the operation. | | **`user`** | The user being logged in. | ### me For [Auth-enabled Collections](../authentication/overview), this hook allows you to optionally replace the default behavior of the `me` operation with your own. If you optionally return a value from your hook, the operation will not perform its own logic and continue. ```ts import type { CollectionMeHook } from 'payload' const meHook: CollectionMeHook = async ({ args, user, }) => {...} ``` The following arguments are provided to the `me` hook: | Option | Description | | ---------- | ---------------------------------------- | | **`args`** | The arguments passed into the operation. | | **`user`** | The user being logged in. | ## TypeScript Payload exports a type for each Collection hook which can be accessed as follows: ```ts import type { CollectionBeforeOperationHook, CollectionBeforeValidateHook, CollectionBeforeChangeHook, CollectionAfterChangeHook, CollectionAfterReadHook, CollectionBeforeReadHook, CollectionBeforeDeleteHook, CollectionAfterDeleteHook, CollectionBeforeLoginHook, CollectionAfterLoginHook, CollectionAfterLogoutHook, CollectionAfterRefreshHook, CollectionAfterMeHook, CollectionAfterForgotPasswordHook, CollectionRefreshHook, CollectionMeHook, } from 'payload' ``` # Global Hooks Source: https://payloadcms.com/docs/hooks/globals Global Hooks are [Hooks](./overview) that run on [Global](../configuration/globals) Documents. They allow you to execute your own logic during specific events of the Document lifecycle. To add Hooks to a Global, use the `hooks` property in your [Global Config](../configuration/globals): ```ts import type { GlobalConfig } from 'payload' export const GlobalWithHooks: GlobalConfig = { // ... hooks: { // highlight-line // ... }, } ``` **Tip:** You can also set hooks on the field-level to isolate hook logic to specific fields. [More details](./fields). ## Config Options All Global Hooks accept an array of [synchronous or asynchronous functions](./overview#async-vs-synchronous). Each Global Hook receives specific arguments based on its own type, and has the ability to modify specific outputs. ```ts import type { GlobalConfig } from 'payload'; const GlobalWithHooks: GlobalConfig = { // ... // highlight-start hooks: { beforeOperation: [(args) => {...}], beforeValidate: [(args) => {...}], beforeChange: [(args) => {...}], beforeRead: [(args) => {...}], afterChange: [(args) => {...}], afterRead: [(args) => {...}], } // highlight-end } ``` ### beforeOperation The `beforeOperation` hook can be used to modify the arguments that operations accept or execute side-effects that run before an operation begins. ```ts import type { GlobalBeforeOperationHook } from 'payload' const beforeOperationHook: GlobalBeforeOperationHook = async ({ args, operation, req, }) => { return args // return modified operation arguments as necessary } ``` The following arguments are provided to the `beforeOperation` hook: | Option | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](../configuration/globals) in which this Hook is running against. Available operation include: `countVersions`, `read`, `restoreVersion`, and `update`. | | **`context`** | Custom context passed between Hooks. [More details](./context). | | **`operation`** | The name of the operation that this hook is running within. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### beforeValidate Runs during the `update` operation. This hook allows you to add or format data before the incoming data is validated server-side. Please do note that this does not run before client-side validation. If you render a custom field component in your front-end and provide it with a `validate` function, the order that validations will run in is: 1. `validate` runs on the client 2. if successful, `beforeValidate` runs on the server 3. `validate` runs on the server ```ts import type { GlobalBeforeValidateHook } from 'payload' const beforeValidateHook: GlobalBeforeValidateHook = async ({ data, req, originalDoc, }) => { return data } ``` The following arguments are provided to the `beforeValidate` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](../configuration/globals) in which this Hook is running against. | | **`context`** | Custom context passed between Hooks. [More details](./context). | | **`data`** | The incoming data passed through the operation. | | **`originalDoc`** | The Document before changes are applied. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### beforeChange Immediately following validation, `beforeChange` hooks will run within the `update` operation. At this stage, you can be confident that the data that will be saved to the document is valid in accordance to your field validations. You can optionally modify the shape of data to be saved. ```ts import type { GlobalBeforeChangeHook } from 'payload' const beforeChangeHook: GlobalBeforeChangeHook = async ({ data, req, originalDoc, }) => { return data } ``` The following arguments are provided to the `beforeChange` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](../configuration/globals) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`data`** | The incoming data passed through the operation. | | **`originalDoc`** | The Document before changes are applied. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### afterChange After a global is updated, the `afterChange` hook runs. Use this hook to purge caches of your applications, sync site data to CRMs, and more. ```ts import type { GlobalAfterChangeHook } from 'payload' const afterChangeHook: GlobalAfterChangeHook = async ({ doc, previousDoc, req, }) => { return data } ``` The following arguments are provided to the `afterChange` hook: | Option | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](../configuration/globals) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`data`** | The incoming data passed through the operation. | | **`doc`** | The resulting Document after changes are applied. | | **`previousDoc`** | The Document before changes were applied. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### beforeRead Runs before `findOne` global operation is transformed for output by `afterRead`. This hook fires before hidden fields are removed and before localized fields are flattened into the requested locale. Using this Hook will provide you with all locales and all hidden fields via the `doc` argument. ```ts import type { GlobalBeforeReadHook } from 'payload' const beforeReadHook: GlobalBeforeReadHook = async ({ doc, req, }) => {...} ``` The following arguments are provided to the `beforeRead` hook: | Option | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](../configuration/globals) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`doc`** | The resulting Document after changes are applied. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ### afterRead Runs as the last step before a global is returned. Flattens locales, hides protected fields, and removes fields that users do not have access to. ```ts import type { GlobalAfterReadHook } from 'payload' const afterReadHook: GlobalAfterReadHook = async ({ doc, req, findMany, }) => {...} ``` The following arguments are provided to the `beforeRead` hook: | Option | Description | | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`global`** | The [Global](../configuration/globals) in which this Hook is running against. | | **`context`** | Custom context passed between hooks. [More details](./context). | | **`findMany`** | Boolean to denote if this hook is running against finding one, or finding many (useful in versions). | | **`doc`** | The resulting Document after changes are applied. | | **`query`** | The [Query](../queries/overview) of the request. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | ## TypeScript Payload exports a type for each Global hook which can be accessed as follows: ```ts import type { GlobalBeforeValidateHook, GlobalBeforeChangeHook, GlobalAfterChangeHook, GlobalBeforeReadHook, GlobalAfterReadHook, } from 'payload' ``` # Field Hooks Source: https://payloadcms.com/docs/hooks/fields Field Hooks are [Hooks](./overview) that run on Documents on a per-field basis. They allow you to execute your own logic during specific events of the Document lifecycle. Field Hooks offer incredible potential for isolating your logic from the rest of your [Collection Hooks](./collections) and [Global Hooks](./globals). To add Hooks to a Field, use the `hooks` property in your [Field Config](../fields/overview): ```ts import type { Field } from 'payload' export const FieldWithHooks: Field = { // ... hooks: { // highlight-line // ... }, } ``` ## Config Options All Field Hooks accept an array of synchronous or asynchronous functions. These functions can optionally modify the return value of the field before the operation continues. All Field Hooks are formatted to accept the same arguments, although some arguments may be `undefined` based the specific hook type. **Important:** Due to GraphQL's typed nature, changing the type of data that you return from a field will produce errors in the [GraphQL API](../graphql/overview). If you need to change the shape or type of data, consider [Collection Hooks](./collections) or [Global Hooks](./globals) instead. To add hooks to a Field, use the `hooks` property in your [Field Config](../fields/overview): ```ts import type { Field } from 'payload'; const FieldWithHooks: Field = { name: 'name', type: 'text', // highlight-start hooks: { beforeValidate: [(args) => {...}], beforeChange: [(args) => {...}], beforeDuplicate: [(args) => {...}], afterChange: [(args) => {...}], afterRead: [(args) => {...}], } // highlight-end } ``` The following arguments are provided to all Field Hooks: | Option | Description | | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`collection`** | The [Collection](../configuration/collections) in which this Hook is running against. If the field belongs to a Global, this will be `null`. | | **`context`** | Custom context passed between Hooks. [More details](./context). | | **`data`** | In the `afterRead` hook this is the full Document. In the `create` and `update` operations, this is the incoming data passed through the operation. | | **`field`** | The [Field](../fields/overview) which the Hook is running against. | | **`findMany`** | Boolean to denote if this hook is running against finding one, or finding many within the `afterRead` hook. | | **`global`** | The [Global](../configuration/globals) in which this Hook is running against. If the field belongs to a Collection, this will be `null`. | | **`operation`** | The name of the operation that this hook is running within. Useful within `beforeValidate`, `beforeChange`, and `afterChange` hooks to differentiate between `create` and `update` operations. | | **`originalDoc`** | In the `update` operation, this is the Document before changes were applied. In the `afterChange` hook, this is the resulting Document. | | **`overrideAccess`** | A boolean to denote if the current operation is overriding [Access Control](../access-control/overview). | | **`path`** | The path to the [Field](../fields/overview) in the schema. | | **`previousDoc`** | In the `afterChange` Hook, this is the Document before changes were applied. | | **`previousSiblingDoc`** | The sibling data of the Document before changes being applied, only in `beforeChange` and `afterChange` hook. | | **`previousValue`** | The previous value of the field, before changes, only in `beforeChange` and `afterChange` hooks. | | **`req`** | The [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object. This is mocked for [Local API](../local-api/overview) operations. | | **`schemaPath`** | The path of the [Field](../fields/overview) in the schema. | | **`siblingData`** | The data of sibling fields adjacent to the field that the Hook is running against. | | **`siblingDocWithLocales`** | The sibling data of the Document with all [Locales](../configuration/localization). | | **`siblingFields`** | The sibling fields of the field which the hook is running against. | | **`value`** | The value of the [Field](../fields/overview). | **Tip:** It's a good idea to conditionally scope your logic based on which operation is executing. For example, if you are writing a `beforeChange` hook, you may want to perform different logic based on if the current `operation` is `create` or `update`. ### beforeValidate Runs during the `create` and `update` operations. This hook allows you to add or format data before the incoming data is validated server-side. Please do note that this does not run before client-side validation. If you render a custom field component in your front-end and provide it with a `validate` function, the order that validations will run in is: 1. `validate` runs on the client 2. if successful, `beforeValidate` runs on the server 3. `validate` runs on the server ```ts import type { Field } from 'payload' const usernameField: Field = { name: 'username', type: 'text', hooks: { beforeValidate: [ ({ value }) => { // Trim whitespace and convert to lowercase return value.trim().toLowerCase() }, ], }, } ``` In this example, the `beforeValidate` hook is used to process the `username` field. The hook takes the incoming value of the field and transforms it by trimming whitespace and converting it to lowercase. This ensures that the username is stored in a consistent format in the database. ### beforeChange Immediately following validation, `beforeChange` hooks will run within `create` and `update` operations. At this stage, you can be confident that the field data that will be saved to the document is valid in accordance to your field validations. ```ts import type { Field } from 'payload' const emailField: Field = { name: 'email', type: 'email', hooks: { beforeChange: [ ({ value, operation }) => { if (operation === 'create') { // Perform additional validation or transformation for 'create' operation } return value }, ], }, } ``` In the `emailField`, the `beforeChange` hook checks the `operation` type. If the operation is `create`, it performs additional validation or transformation on the email field value. This allows for operation-specific logic to be applied to the field. ### afterChange The `afterChange` hook is executed after a field's value has been changed and saved in the database. This hook is useful for post-processing or triggering side effects based on the new value of the field. ```ts import type { Field } from 'payload' const membershipStatusField: Field = { name: 'membershipStatus', type: 'select', options: [ { label: 'Standard', value: 'standard' }, { label: 'Premium', value: 'premium' }, { label: 'VIP', value: 'vip' }, ], hooks: { afterChange: [ ({ value, previousValue, req }) => { if (value !== previousValue) { // Log or perform an action when the membership status changes console.log( `User ID ${req.user.id} changed their membership status from ${previousValue} to ${value}.`, ) // Here, you can implement actions that could track conversions from one tier to another } }, ], }, } ``` In this example, the `afterChange` hook is used with a `membershipStatusField`, which allows users to select their membership level (Standard, Premium, VIP). The hook monitors changes in the membership status. When a change occurs, it logs the update and can be used to trigger further actions, such as tracking conversion from one tier to another or notifying them about changes in their membership benefits. ### afterRead The `afterRead` hook is invoked after a field value is read from the database. This is ideal for formatting or transforming the field data for output. ```ts import type { Field } from 'payload' const dateField: Field = { name: 'createdAt', type: 'date', hooks: { afterRead: [ ({ value }) => { // Format date for display return new Date(value).toLocaleDateString() }, ], }, } ``` Here, the `afterRead` hook for the `dateField` is used to format the date into a more readable format using `toLocaleDateString()`. This hook modifies the way the date is presented to the user, making it more user-friendly. ### beforeDuplicate The `beforeDuplicate` field hook is called on each locale (when using localization), when duplicating a document. It may be used when documents having the exact same properties may cause issue. This gives you a way to avoid duplicate names on `unique`, `required` fields or when external systems expect non-repeating values on documents. This hook gets called before the `beforeValidate` and `beforeChange` hooks are called. By Default, unique and required text fields Payload will append "- Copy" to the original document value. The default is not added if your field has its own, you must return non-unique values from your beforeDuplicate hook to avoid errors or enable the `disableDuplicate` option on the collection. Here is an example of a number field with a hook that increments the number to avoid unique constraint errors when duplicating a document: ```ts import type { Field } from 'payload' const numberField: Field = { name: 'number', type: 'number', hooks: { // increment existing value by 1 beforeDuplicate: [ ({ value }) => { return (value ?? 0) + 1 }, ], }, } ``` ## TypeScript Payload exports a type for field hooks which can be accessed and used as follows: ```ts import type { FieldHook } from 'payload' // Field hook type is a generic that takes three arguments: // 1: The document type // 2: The value type // 3: The sibling data type type ExampleFieldHook = FieldHook const exampleFieldHook: ExampleFieldHook = (args) => { const { value, // Typed as `string` as shown above data, // Typed as a Partial of your ExampleDocumentType siblingData, // Typed as a Partial of SiblingDataType originalDoc, // Typed as ExampleDocumentType operation, req, } = args // Do something here... return value // should return a string as typed above, undefined, or null } ``` # Context Source: https://payloadcms.com/docs/hooks/context The `context` object is used to share data across different Hooks. This persists throughout the entire lifecycle of a request and is available within every Hook. By setting properties to `req.context`, you can effectively share logic across multiple Hooks. ## When To Use Context Context gives you a way forward on otherwise difficult problems such as: 1. **Passing data between Hooks**: Needing data in multiple Hooks from a 3rd party API, it could be retrieved and used in `beforeChange` and later used again in an `afterChange` hook without having to fetch it twice. 2. **Preventing infinite loops**: Calling `payload.update()` on the same document that triggered an `afterChange` hook will create an infinite loop, control the flow by assigning a no-op condition to context 3. **Passing data to Local API**: Setting values on the `req.context` and pass it to `payload.create()` you can provide additional data to hooks without adding extraneous fields. 4. **Passing data between hooks and middleware or custom endpoints**: Hooks could set context across multiple collections and then be used in a final `postMiddleware`. ## How To Use Context Let's see examples on how context can be used in the first two scenarios mentioned above: ### Passing Data Between Hooks To pass data between hooks, you can assign values to context in an earlier hook in the lifecycle of a request and expect it in the context of a later hook. For example: ```ts import type { CollectionConfig } from 'payload' const Customer: CollectionConfig = { slug: 'customers', hooks: { beforeChange: [ async ({ context, data }) => { // assign the customerData to context for use later context.customerData = await fetchCustomerData(data.customerID) return { ...data, // some data we use here name: context.customerData.name, } }, ], afterChange: [ async ({ context, doc, req }) => { // use context.customerData without needing to fetch it again if (context.customerData.contacted === false) { createTodo('Call Customer', context.customerData) } }, ], }, fields: [ /* ... */ ], } ``` ### Preventing Infinite Loops Let's say you have an `afterChange` hook, and you want to do a calculation inside the hook (as the document ID needed for the calculation is available in the `afterChange` hook, but not in the `beforeChange` hook). Once that's done, you want to update the document with the result of the calculation. Bad example: ```ts import type { CollectionConfig } from 'payload' const Customer: CollectionConfig = { slug: 'customers', hooks: { afterChange: [ async ({ doc, req }) => { await req.payload.update({ // DANGER: updating the same slug as the collection in an afterChange will create an infinite loop! collection: 'customers', id: doc.id, data: { ...(await fetchCustomerData(data.customerID)), }, }) }, ], }, fields: [ /* ... */ ], } ``` Instead of the above, we need to tell the `afterChange` hook to not run again if it performs the update (and thus not update itself again). We can solve that with context. Fixed example: ```ts import type { CollectionConfig } from 'payload' const MyCollection: CollectionConfig = { slug: 'slug', hooks: { afterChange: [ async ({ context, doc, req }) => { // return if flag was previously set if (context.triggerAfterChange === false) { return } await req.payload.update({ collection: contextHooksSlug, id: doc.id, data: { ...(await fetchCustomerData(data.customerID)), }, context: { // set a flag to prevent from running again triggerAfterChange: false, }, }) }, ], }, fields: [ /* ... */ ], } ``` ## TypeScript The default TypeScript interface for `context` is `{ [key: string]: unknown }`. If you prefer a more strict typing in your project or when authoring plugins for others, you can override this using the `declare module` syntax. This is known as [module augmentation / declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation), a TypeScript feature which allows us to add properties to existing types. Simply put this in any `.ts` or `.d.ts` file: ```ts declare module 'payload' { // Augment the RequestContext interface to include your custom properties export interface RequestContext { myObject?: string // ... } } ``` This will add the property `myObject` with a type of string to every context object. Make sure to follow this example correctly, as module augmentation can mess up your types if you do it wrong. # Local API Source: https://payloadcms.com/docs/local-api/overview The Payload Local API gives you the ability to execute the same operations that are available through REST and GraphQL within Node, directly on your server. Here, you don't need to deal with server latency or network speed whatsoever and can interact directly with your database. **Tip:** The Local API is incredibly powerful when used in React Server Components and other similar server-side contexts. With other headless CMS, you need to request your data from third-party servers via an HTTP layer, which can add significant loading time to your server-rendered pages. With Payload, you don't have to leave your server to gather the data you need. It can be incredibly fast and is definitely a game changer. Here are some common examples of how you can use the Local API: - Fetching Payload data within React Server Components - Seeding data via Node seed scripts that you write and maintain - Opening custom Next.js route handlers which feature additional functionality but still rely on Payload - Within [Access Control](../access-control/overview) and [Hooks](../hooks/overview) ## Accessing Payload You can gain access to the currently running `payload` object via two ways: #### Accessing from args or `req` In most places within Payload itself, you can access `payload` directly from the arguments of [Hooks](../hooks/overview), [Access Control](../access-control/overview), [Validation](../fields/overview#validation) functions, and similar. This is the simplest way to access Payload in most cases. Most config functions take the `req` (Request) object, which has Payload bound to it (`req.payload`). Example: ```ts const afterChangeHook: CollectionAfterChangeHook = async ({ req: { payload }, }) => { const posts = await payload.find({ collection: 'posts', }) } ``` #### Importing it If you want to import Payload in places where you don't have the option to access it from function arguments or `req`, you can import it and initialize it. ```ts import { getPayload } from 'payload' import config from '@payload-config' const payload = await getPayload({ config }) ``` If you're working in Next.js' development mode, Payload will work with Hot Module Replacement (HMR), and as you make changes to your Payload Config, your usage of Payload will always be in sync with your changes. In production, `getPayload` simply disables all HMR functionality so you don't need to write your code any differently. We handle optimization for you in production mode. If you are accessing Payload via function arguments or `req.payload`, HMR is automatically supported if you are using it within Next.js. For more information about using Payload outside of Next.js, [click here](./outside-nextjs). ## Local options available You can specify more options within the Local API vs. REST or GraphQL due to the server-only context that they are executed in. | Local Option | Description | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `collection` | Required for Collection operations. Specifies the Collection slug to operate against. | | `data` | The data to use within the operation. Required for `create`, `update`. | | `depth` | [Control auto-population](../queries/depth) of nested relationship and upload fields. | | `locale` | Specify [locale](../configuration/localization) for any returned documents. | | `select` | Specify [select](../queries/select) to control which fields to include to the result. | | `populate` | Specify [populate](../queries/select#populate) to control which fields to include to the result from populated documents. | | `fallbackLocale` | Specify a [fallback locale](../configuration/localization) to use for any returned documents. This can be a single locale or array of locales. | | `overrideAccess` | Skip access control. By default, this property is set to true within all Local API operations. | | `overrideLock` | By default, document locks are ignored (`true`). Set to `false` to enforce locks and prevent operations when a document is locked by another user. [More details](../admin/locked-documents). | | `user` | If you set `overrideAccess` to `false`, you can pass a user to use against the access control checks. | | `showHiddenFields` | Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config. | | `pagination` | Set to false to return all documents and avoid querying for document counts. | | `context` | [Context](../hooks/context), which will then be passed to `context` and `req.context`, which can be read by hooks. Useful if you want to pass additional information to the hooks which shouldn't be necessarily part of the document, for example a `triggerBeforeChange` option which can be read by the BeforeChange hook to determine if it should run or not. | | `disableErrors` | When set to `true`, errors will not be thrown. Instead, the `findByID` operation will return `null`, and the `find` operation will return an empty documents array. | | `disableTransaction` | When set to `true`, a [database transactions](../database/transactions) will not be initialized. | _There are more options available on an operation by operation basis outlined below._ ## Transactions When your database uses transactions you need to thread req through to all local operations. Postgres uses transactions and MongoDB uses transactions when you are using replica sets. Passing req without transactions is still recommended. ```js const post = await payload.find({ collection: 'posts', req, // passing req is recommended }) ``` **Note:** By default, all access control checks are disabled in the Local API, but you can re-enable them if you'd like, as well as pass a specific user to run the operation with. ## Collections The following Collection operations are available through the Local API: ### Create#collection-create ```js // The created Post document is returned const post = await payload.create({ collection: 'posts', // required data: { // required title: 'sure', description: 'maybe', }, locale: 'en', fallbackLocale: false, user: dummyUserDoc, overrideAccess: true, showHiddenFields: false, // If creating verification-enabled auth doc, // you can optionally disable the email that is auto-sent disableVerificationEmail: true, // If your collection supports uploads, you can upload // a file directly through the Local API by providing // its full, absolute file path. filePath: path.resolve(__dirname, './path-to-image.jpg'), // Alternatively, you can directly pass a File, // if file is provided, filePath will be omitted file: uploadedFile, // If you want to create a document that is a duplicate of another document duplicateFromID: 'document-id-to-duplicate', }) ``` ### Find#collection-find ```js // Result will be a paginated set of Posts. // See /docs/queries/pagination for more. const result = await payload.find({ collection: 'posts', // required depth: 2, page: 1, limit: 10, pagination: false, // If you want to disable pagination count, etc. where: {}, // pass a `where` query here sort: '-title', locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` `pagination`, `page`, and `limit` are three related properties [documented here](../queries/pagination). ### Find by ID#collection-find-by-id ```js // Result will be a Post document. const result = await payload.findByID({ collection: 'posts', // required id: '507f1f77bcf86cd799439011', // required depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Count#collection-count ```js // Result will be an object with: // { // totalDocs: 10, // count of the documents satisfies query // } const result = await payload.count({ collection: 'posts', // required locale: 'en', where: {}, // pass a `where` query here user: dummyUser, overrideAccess: false, }) ``` ### FindDistinct#collection-find-distinct ```js // Result will be an object with: // { // values: ['value-1', 'value-2'], // array of distinct values, // field: 'title', // the field // totalDocs: 10, // count of the distinct values satisfies query, // perPage: 10, // count of distinct values per page (based on provided limit) // } const result = await payload.findDistinct({ collection: 'posts', // required locale: 'en', where: {}, // pass a `where` query here user: dummyUser, overrideAccess: false, field: 'title', sort: 'title', }) ``` ### Update by ID#collection-update-by-id ```js // Result will be the updated Post document. const result = await payload.update({ collection: 'posts', // required id: '507f1f77bcf86cd799439011', // required data: { // required title: 'sure', description: 'maybe', }, depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks. showHiddenFields: true, // If your collection supports uploads, you can upload // a file directly through the Local API by providing // its full, absolute file path. filePath: path.resolve(__dirname, './path-to-image.jpg'), // If you are uploading a file and would like to replace // the existing file instead of generating a new filename, // you can set the following property to `true` overwriteExistingFiles: true, }) ``` ### Update Many#collection-update-many ```js // Result will be an object with: // { // docs: [], // each document that was updated // errors: [], // each error also includes the id of the document // } const result = await payload.update({ collection: 'posts', // required where: { // required fieldName: { equals: 'value' }, }, data: { // required title: 'sure', description: 'maybe', }, depth: 0, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks. showHiddenFields: true, // If your collection supports uploads, you can upload // a file directly through the Local API by providing // its full, absolute file path. filePath: path.resolve(__dirname, './path-to-image.jpg'), // If you are uploading a file and would like to replace // the existing file instead of generating a new filename, // you can set the following property to `true` overwriteExistingFiles: true, }) ``` ### Delete#collection-delete ```js // Result will be the now-deleted Post document. const result = await payload.delete({ collection: 'posts', // required id: '507f1f77bcf86cd799439011', // required depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks. showHiddenFields: true, }) ``` ### Delete Many#collection-delete-many ```js // Result will be an object with: // { // docs: [], // each document that is now deleted // errors: [], // any errors that occurred, including the id of the errored on document // } const result = await payload.delete({ collection: 'posts', // required where: { // required fieldName: { equals: 'value' }, }, depth: 0, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks. showHiddenFields: true, }) ``` ## Auth Operations If a collection has [`Authentication`](../authentication/overview) enabled, additional Local API operations will be available: ### Auth ```js // If you're using Next.js, you'll have to import headers from next/headers, like so: // import { headers as nextHeaders } from 'next/headers' // you'll also have to await headers inside your function, or component, like so: // const headers = await nextHeaders() // If you're using Payload outside of Next.js, you'll have to provide headers accordingly. // result will be formatted as follows: // { // permissions: { ... }, // object containing current user's permissions // user: { ... }, // currently logged in user's document // responseHeaders: { ... } // returned headers from the response // } const result = await payload.auth({ headers, canSetHeaders: false }) ``` ### Login ```js // result will be formatted as follows: // { // token: 'o38jf0q34jfij43f3f...', // JWT used for auth // user: { ... } // the user document that just logged in // exp: 1609619861 // the UNIX timestamp when the JWT will expire // } const result = await payload.login({ collection: 'users', // required data: { // required email: 'dev@payloadcms.com', password: 'rip', }, req: req, // optional, pass a Request object to be provided to all hooks depth: 2, locale: 'en', fallbackLocale: false, overrideAccess: false, showHiddenFields: true, }) ``` ### Forgot Password ```js // Returned token will allow for a password reset const token = await payload.forgotPassword({ collection: 'users', // required data: { // required email: 'dev@payloadcms.com', }, req: req, // pass a Request object to be provided to all hooks }) ``` ### Reset Password ```js // Result will be formatted as follows: // { // token: 'o38jf0q34jfij43f3f...', // JWT used for auth // user: { ... } // the user document that just logged in // } const result = await payload.resetPassword({ collection: 'users', // required data: { // required password: req.body.password, // the new password to set token: 'afh3o2jf2p3f...', // the token generated from the forgotPassword operation }, req: req, // optional, pass a Request object to be provided to all hooks }) ``` ### Unlock ```js // Returned result will be a boolean representing success or failure const result = await payload.unlock({ collection: 'users', // required data: { // required email: 'dev@payloadcms.com', }, req: req, // optional, pass a Request object to be provided to all hooks overrideAccess: true, }) ``` ### Verify ```js // Returned result will be a boolean representing success or failure const result = await payload.verifyEmail({ collection: 'users', // required token: 'afh3o2jf2p3f...', // the token saved on the user as `_verificationToken` }) ``` ## Globals The following Global operations are available through the Local API: ### Find#global-find ```js // Result will be the Header Global. const result = await payload.findGlobal({ slug: 'header', // required depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Update#global-update ```js // Result will be the updated Header Global. const result = await payload.updateGlobal({ slug: 'header', // required data: { // required nav: [ { url: 'https://google.com', }, { url: 'https://payloadcms.com', }, ], }, depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, overrideLock: false, // By default, document locks are ignored. Set to false to enforce locks. showHiddenFields: true, }) ``` ## TypeScript Local API calls will automatically infer your [generated types](../typescript/generating-types). Here is an example of usage: ```ts // Properly inferred as `Post` type const post = await payload.create({ collection: 'posts', // Data will now be typed as Post and give you type hints data: { title: 'my title', description: 'my description', }, }) ``` # Using Payload outside Next.js Source: https://payloadcms.com/docs/local-api/outside-nextjs Payload can be used completely outside of Next.js which is helpful in cases like running scripts, using Payload in a separate backend service, or using Payload's Local API to fetch your data directly from your database in other frontend frameworks like SvelteKit, Remix, Nuxt, and similar. **Note:** Payload and all of its official packages are fully ESM. If you want to use Payload within your own projects, make sure you are writing your scripts in ESM format or dynamically importing the Payload Config. ## Importing the Payload Config outside of Next.js Payload provides a convenient way to run standalone scripts, which can be useful for tasks like seeding your database or performing one-off operations. In standalone scripts, you can simply import the Payload Config and use it right away. If you need an initialized copy of Payload, you can then use the `getPayload` function. This can be useful for tasks like seeding your database or performing other one-off operations. ```ts import { getPayload } from 'payload' import config from '@payload-config' const seed = async () => { // Get a local copy of Payload by passing your config const payload = await getPayload({ config }) const user = await payload.create({ collection: 'users', data: { email: 'dev@payloadcms.com', password: 'some-password', }, }) const page = await payload.create({ collection: 'pages', data: { title: 'My Homepage', // other data to seed here }, }) } // Call the function here to run your seed script await seed() ``` You can then execute the script using `payload run`. Example: if you placed this standalone script in `src/seed.ts`, you would execute it like this: ```sh payload run src/seed.ts ``` The `payload run` command does two things for you: 1. It loads the environment variables the same way Next.js loads them, eliminating the need for additional dependencies like `dotenv`. The usage of `dotenv` is not recommended, as Next.js loads environment variables differently. By using `payload run`, you ensure consistent environment variable handling across your Payload and Next.js setup. 2. It initializes tsx, allowing direct execution of TypeScript files manually installing tools like tsx or ts-node. ### Troubleshooting If you encounter import-related errors, you have 2 options: #### Option 1: enable swc mode by appending `--use-swc` to the `payload` command: Example: ```sh payload run src/seed.ts --use-swc ``` Note: Install @swc-node/register in your project first. While swc mode is faster than the default tsx mode, it might break for some imports. #### Option 2: use an alternative runtime like bun While we do not guarantee support for alternative runtimes, you are free to use them and disable Payload's own transpilation by appending the `--disable-transpile` flag to the `payload` command: ```sh bunx --bun payload run src/seed.ts --disable-transpile ``` You will need to have bun installed on your system for this to work. # Using Local API Operations with Server Functions Source: https://payloadcms.com/docs/local-api/server-functions In Next.js, **server functions** (previously called **server actions**) are special functions that run exclusively on the server, enabling secure backend logic execution while being callable from the frontend. These functions bridge the gap between client and server, allowing frontend components to perform backend operations without exposing sensitive logic. ### Why Use Server Functions? - **Executing Backend Logic from the Frontend**: The Local API is designed for server environments and cannot be directly accessed from client-side code. Server functions enable frontend components to trigger backend operations securely. - **Security Benefits**: Instead of exposing a full REST or GraphQL API, server functions restrict access to only the necessary operations, reducing potential security risks. - **Performance Optimizations**: Next.js handles server functions efficiently, offering benefits like caching, optimized database queries, and reduced network overhead compared to traditional API calls. - **Simplified Development Workflow**: Rather than setting up full API routes with authentication and authorization checks, server functions allow for lightweight, direct execution of necessary operations. ### When to Use Server Functions Use server functions whenever you need to call Local API operations from the frontend. Since the Local API is only accessible from the backend, server functions act as a secure bridge, eliminating the need to expose additional API endpoints. ## Examples All Local API operations can be used within server functions, allowing you to interact with Payload's backend securely. For a full list of available operations, see the [Local API](../local-api/overview) overview. In the following examples, we'll cover some common use cases, including: - Creating a document - Updating a document - Handling file uploads when creating or updating a document - Authenticating a user ### Creating a Document First, let's create our server function. Here are some key points for this process: - Begin by adding `'use server'` at the top of the file. - You can still use utilities such as `getPayload()`. - Once the function structure is in place, call the Local API operation `payload.create()` and pass in the relevant data. - It's good practice to wrap this in a `try...catch` block for error handling. - Finally, make sure to return the created document (don't just run the operation). ```ts 'use server' import { getPayload } from 'payload' import config from '@payload-config' export async function createPost(data) { const payload = await getPayload({ config }) try { const post = await payload.create({ collection: 'posts', data, }) return post } catch (error) { throw new Error(`Error creating post: ${error.message}`) } } ``` Now, let's look at how to call the `createPost` function we just created from the frontend in a React component when a user clicks a button: ```ts 'use client'; import React, { useState } from 'react'; import { createPost } from '../server/actions'; // import the server function export const PostForm: React.FC = () => { const [result, setResult] = useState(''); return ( <>

{result}

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

{result}

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

{userInfo}

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

{result}

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

{page?.title}

) } ``` **Note:** For fully working example of this, check of the official [Draft Preview Example](https://github.com/payloadcms/payload/tree/main/examples/draft-preview) in the [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples). # Document Locking Source: https://payloadcms.com/docs/admin/locked-documents Document locking in Payload ensures that only one user at a time can edit a document, preventing data conflicts and accidental overwrites. When a document is locked, other users are prevented from making changes until the lock is released, ensuring data integrity in collaborative environments. The lock is automatically triggered when a user begins editing a document within the Admin Panel and remains in place until the user exits the editing view or the lock expires due to inactivity. ## How it works When a user starts editing a document, Payload locks it for that user. If another user attempts to access the same document, they will be notified that it is currently being edited. They can then choose one of the following options: - View in Read-Only: View the document without the ability to make any changes. - Take Over: Take over editing from the current user, which locks the document for the new editor and notifies the original user. - Return to Dashboard: Navigate away from the locked document and continue with other tasks. The lock will automatically expire after a set period of inactivity, configurable using the `duration` property in the `lockDocuments` configuration, after which others can resume editing. **Note:** If your application does not require document locking, you can disable this feature for any collection or global by setting the `lockDocuments` property to `false`. ### Config Options The `lockDocuments` property exists on both the Collection Config and the Global Config. Document locking is enabled by default, but you can customize the lock duration or turn off the feature for any collection or global. Here's an example configuration for document locking: ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', fields: [ { name: 'title', type: 'text', }, // other fields... ], lockDocuments: { duration: 600, // Duration in seconds }, } ``` #### Locking Options | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`lockDocuments`** | Enables or disables document locking for the collection or global. By default, document locking is enabled. Set to an object to configure, or set to false to disable locking. | | **`duration`** | Specifies the duration (in seconds) for how long a document remains locked without user interaction. The default is 300 seconds (5 minutes). | ### Impact on APIs Document locking affects both the Local and REST APIs, ensuring that if a document is locked, concurrent users will not be able to perform updates or deletes on that document (including globals). If a user attempts to update or delete a locked document, they will receive an error. Once the document is unlocked or the lock duration has expired, other users can proceed with updates or deletes as normal. #### Overriding Locks For operations like `update` and `delete`, Payload includes an `overrideLock` option. This boolean flag, when set to `false`, enforces document locks, ensuring that the operation will not proceed if another user currently holds the lock. By default, `overrideLock` is set to `true`, which means that document locks are ignored, and the operation will proceed even if the document is locked. To enforce locks and prevent updates or deletes on locked documents, set `overrideLock: false`. ```ts const result = await payload.update({ collection: 'posts', id: '123', data: { title: 'New title', }, overrideLock: false, // Enforces the document lock, preventing updates if the document is locked }) ``` This option is particularly useful in scenarios where administrative privileges or specific workflows require you to override the lock and ensure the operation is completed. # Customizing CSS & SCSS Source: https://payloadcms.com/docs/admin/customizing-css Customizing the Payload [Admin Panel](./overview) through CSS alone is one of the easiest and most powerful ways to customize the look and feel of the dashboard. To allow for this level of customization, Payload: 1. Exposes a [root-level stylesheet](#global-css) for you to inject custom selectors 1. Provides a [CSS library](#css-library) that can be easily overridden or extended 1. Uses [BEM naming conventions](http://getbem.com) so that class names are globally accessible To customize the CSS within the Admin UI, determine scope and change you'd like to make, and then add your own CSS or SCSS to the configuration as needed. ## Global CSS Global CSS refers to the CSS that is applied to the entire [Admin Panel](./overview). This is where you can have a significant impact to the look and feel of the Admin UI through just a few lines of code. You can add your own global CSS through the root `custom.scss` file of your app. This file is loaded into the root of the Admin Panel and can be used to inject custom selectors or styles however needed. Here is an example of how you might target the Dashboard View and change the background color: ```scss .dashboard { background-color: red; // highlight-line } ``` **Note:** If you are building [Custom Components](../custom-components/overview), it is best to import your own stylesheets directly into your components, rather than using the global stylesheet. You can continue to use the [CSS library](#css-library) as needed. ### Specificity rules All Payload CSS is encapsulated inside CSS layers under `@layer payload-default`. Any custom css will now have the highest possible specificity. We have also provided a layer `@layer payload` if you want to use layers and ensure that your styles are applied after payload. To override existing styles in a way that the previous rules of specificity would be respected you can use the default layer like so ```css @layer payload-default { // my styles within the Payload specificity } ``` ## Re-using Payload SCSS variables and utilities You can re-use Payload's SCSS variables and utilities in your own stylesheets by importing it from the UI package. ```scss @import '~@payloadcms/ui/scss'; ``` ## CSS Library To make it as easy as possible for you to override default styles, Payload uses [BEM naming conventions](http://getbem.com/) for all CSS within the Admin UI. If you provide your own CSS, you can override any built-in styles easily, including targeting nested components and their various component states. You can also override Payload's built-in [CSS Variables](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties). These variables are widely consumed by the Admin Panel, so modifying them has a significant impact on the look and feel of the Admin UI. The following variables are defined and can be overridden: - [Breakpoints](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss/queries.scss) - [Colors](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss/colors.scss) - Base color shades (white to black by default) - Success / warning / error color shades - Theme-specific colors (background, input background, text color, etc.) - Elevation colors (used to determine how "bright" something should be when compared to the background) - [Sizing](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss/app.scss) - Horizontal gutter - Transition speeds - Font sizes - Etc. For an up-to-date, comprehensive list of all available variables, please refer to the [Source Code](https://github.com/payloadcms/payload/blob/main/packages/ui/src/scss). **Warning:** If you're overriding colors or theme elevations, make sure to consider how [your changes will affect dark mode](#dark-mode). #### Dark Mode Colors are designed to automatically adapt to theme of the [Admin Panel](./overview). By default, Payload automatically overrides all `--theme-elevation` colors and inverts all success / warning / error shades to suit dark mode. We also update some base theme variables like `--theme-bg`, `--theme-text`, etc. # React Hooks Source: https://payloadcms.com/docs/admin/react-hooks Payload provides a variety of powerful [React Hooks](https://react.dev/reference/react-dom/hooks) that can be used within your own [Custom Components](../custom-components/overview), such as [Custom Fields](../fields/overview#custom-components). With them, you can interface with Payload itself to build just about any type of complex customization you can think of. **Reminder:** All Custom Components are [React Server Components](https://react.dev/reference/rsc/server-components) by default. Hooks, on the other hand, are only available in client-side environments. To use hooks, [ensure your component is a client component](../custom-components/overview#client-components). ## useField The `useField` hook is used internally within all field components. It manages sending and receiving a field's state from its parent form. When you build a [Custom Field Component](../fields/overview#custom-components), you will be responsible for sending and receiving the field's `value` to and from the form yourself. To do so, import the `useField` hook as follows: ```tsx 'use client' import type { TextFieldClientComponent } from 'payload' import { useField } from '@payloadcms/ui' export const CustomTextField: TextFieldClientComponent = ({ path }) => { const { value, setValue } = useField({ path }) // highlight-line return (

{path}

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

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

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

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

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

Loading...

if (isError) return

Error occurred while fetching data.

return (

{data?.title}

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

{page.title}

} ``` Each Custom Component receives the following props by default: | Prop | Description | | --------- | ------------------------------------------- | | `payload` | The [Payload](../local-api/overview) class. | | `i18n` | The [i18n](../configuration/i18n) object. | **Reminder:** All Custom Components also receive various other props that are specific to the component being rendered. See [Root Components](#root-components), [Collection Components](../configuration/collections#custom-components), [Global Components](../configuration/globals#custom-components), or [Field Components](../fields/overview#custom-components) for a complete list of all default props per component. ### Custom Props It is also possible to pass custom props to your Custom Components. To do this, you can use either the `clientProps` or `serverProps` properties depending on whether your prop is [serializable](https://react.dev/reference/rsc/use-client#serializable-types), and whether your component is a Server or Client Component. ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { // highlight-line components: { logout: { Button: { path: '/src/components/Logout#MyComponent', clientProps: { myCustomProp: 'Hello, World!', // highlight-line }, }, }, }, }, }) ``` Here is how your component might receive this prop: ```tsx import React from 'react' import { Link } from '@payloadcms/ui' export function MyComponent({ myCustomProp }: { myCustomProp: string }) { return {myCustomProp} } ``` ### Client Components All Custom Components in Payload are [React Server Components](https://react.dev/reference/rsc/server-components) by default, however, it is possible to use [Client Components](https://react.dev/reference/rsc/use-client) by simply adding the `'use client'` directive at the top of your file. Payload will automatically detect and remove all [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) default props before rendering your component. ```tsx // highlight-start 'use client' // highlight-end import React, { useState } from 'react' export function MyClientComponent() { const [count, setCount] = useState(0) return ( ) } ``` **Reminder:** Client Components cannot be passed [non-serializable props](https://react.dev/reference/rsc/use-client#serializable-types). If you are rendering your Client Component _from within_ a Server Component, ensure that its props are serializable. ### Accessing the Payload Config From any Server Component, the [Payload Config](../configuration/overview) can be accessed directly from the `payload` prop: ```tsx import React from 'react' export default async function MyServerComponent({ payload: { config, // highlight-line }, }) { return Go Home } ``` But, the Payload Config is [non-serializable](https://react.dev/reference/rsc/use-client#serializable-types) by design. It is full of custom validation functions and more. This means that the Payload Config, in its entirety, cannot be passed directly to Client Components. For this reason, Payload creates a Client Config and passes it into the Config Provider. This is a serializable version of the Payload Config that can be accessed from any Client Component via the [`useConfig`](../admin/react-hooks#useconfig) hook: ```tsx 'use client' import React from 'react' import { useConfig } from '@payloadcms/ui' export function MyClientComponent() { // highlight-start const { config: { serverURL }, } = useConfig() // highlight-end return Go Home } ``` See [Using Hooks](#using-hooks) for more details. Similarly, all [Field Components](../fields/overview#custom-components) automatically receive their respective Field Config through props. Within Server Components, this prop is named `field`: ```tsx import React from 'react' import type { TextFieldServerComponent } from 'payload' export const MyClientFieldComponent: TextFieldServerComponent = ({ field: { name }, }) => { return

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

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

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

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

{translatedTitle}

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

{localizedPage.title}

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

{`Entity slug: ${slug}`}

} ``` See the [Hooks](../admin/react-hooks) documentation for a full list of available hooks. ### Adding Styles Payload has a robust [CSS Library](../admin/customizing-css) that you can use to style your Custom Components to match to Payload's built-in styling. This will ensure that your Custom Components integrate well into the existing design system. This will make it so they automatically adapt to any theme changes that might occur. To apply custom styles, simply import your own `.css` or `.scss` file into your Custom Component: ```tsx import './index.scss' export function MyComponent() { return
My Custom Component
} ``` Then to colorize your Custom Component's background, for example, you can use the following CSS: ```scss .my-component { background-color: var(--theme-elevation-500); } ``` Payload also exports its [SCSS](https://sass-lang.com) library for reuse which includes mixins, etc. To use this, simply import it as follows into your `.scss` file: ```scss @import '~@payloadcms/ui/scss'; .my-component { @include mid-break { background-color: var(--theme-elevation-900); } } ``` **Note:** You can also drill into Payload's own component styles, or easily apply global, app-wide CSS. More on that [here](../admin/customizing-css). ## Performance An often overlooked aspect of Custom Components is performance. If unchecked, Custom Components can lead to slow load times of the Admin Panel and ultimately a poor user experience. This is different from front-end performance of your public-facing site. For more performance tips, see the [Performance documentation](../performance/overview). ### Follow React and Next.js best practices All Custom Components are built using [React](https://react.dev). For this reason, it is important to follow React best practices. This includes using memoization, streaming, caching, optimizing renders, using hooks appropriately, and more. To learn more, see the [React documentation](https://react.dev/learn). The Admin Panel itself is a [Next.js](https://nextjs.org) application. For this reason, it is _also_ important to follow Next.js best practices. This includes bundling, when to use layouts vs pages, where to place the server/client boundary, and more. To learn more, see the [Next.js documentation](https://nextjs.org/docs). ### Reducing initial HTML size With Server Components, be aware of what is being sent to through the server/client boundary. All props are serialized and sent through the network. This can lead to large HTML sizes and slow initial load times if too much data is being sent to the client. To minimize this, you must be explicit about what props are sent to the client. Prefer server components and only send the necessary props to the client. This will also offset some of the JS execution to the server. **Tip:** Use [React Suspense](https://react.dev/reference/react/Suspense) to progressively load components and improve perceived performance. ### Prevent unnecessary re-renders If subscribing your component to form state, it may be re-rendering more often than necessary. To do this, use the [`useFormFields`](../admin/react-hooks) hook instead of `useFields` when you only need to access specific fields. ```ts 'use client' import { useFormFields } from '@payloadcms/ui' const MyComponent: TextFieldClientComponent = ({ path }) => { const value = useFormFields(([fields, dispatch]) => fields[path]) // ... } ``` # Root Components Source: https://payloadcms.com/docs/custom-components/root-components Root Components are those that affect the [Admin Panel](../admin/overview) at a high-level, such as the logo or the main nav. You can swap out these components with your own [Custom Components](./overview) to create a completely custom look and feel. When combined with [Custom CSS](../admin/customizing-css), you can create a truly unique experience for your users, such as white-labeling the Admin Panel to match your brand. To override Root Components, use the `admin.components` property at the root of your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { // ... }, // highlight-end }, }) ``` ## Config Options The following options are available: | Path | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `actions` | An array of Custom Components to be rendered _within_ the header of the Admin Panel, providing additional interactivity and functionality. [More details](#actions). | | `afterDashboard` | An array of Custom Components to inject into the built-in Dashboard, _after_ the default dashboard contents. [More details](#afterdashboard). | | `afterLogin` | An array of Custom Components to inject into the built-in Login, _after_ the default login form. [More details](#afterlogin). | | `afterNavLinks` | An array of Custom Components to inject into the built-in Nav, _after_ the links. [More details](#afternavlinks). | | `beforeDashboard` | An array of Custom Components to inject into the built-in Dashboard, _before_ the default dashboard contents. [More details](#beforedashboard). | | `beforeLogin` | An array of Custom Components to inject into the built-in Login, _before_ the default login form. [More details](#beforelogin). | | `beforeNavLinks` | An array of Custom Components to inject into the built-in Nav, _before_ the links themselves. [More details](#beforenavlinks). | | `graphics.Icon` | The simplified logo used in contexts like the `Nav` component. [More details](#graphicsicon). | | `graphics.Logo` | The full logo used in contexts like the `Login` view. [More details](#graphicslogo). | | `header` | An array of Custom Components to be injected above the Payload header. [More details](#header). | | `logout.Button` | The button displayed in the sidebar that logs the user out. [More details](#logoutbutton). | | `Nav` | Contains the sidebar / mobile menu in its entirety. [More details](#nav). | | `settingsMenu` | An array of Custom Components to inject into a popup menu accessible via a gear icon above the logout button. [More details](#settingsMenu). | | `providers` | Custom [React Context](https://react.dev/learn/scaling-up-with-reducer-and-context) providers that will wrap the entire Admin Panel. [More details](./custom-providers). | | `views` | Override or create new views within the Admin Panel. [More details](./custom-views). | _For details on how to build Custom Components, see [Building Custom Components](./overview#building-custom-components)._ **Note:** You can also use set [Collection Components](../configuration/collections#custom-components) and [Global Components](../configuration/globals#custom-components) in their respective configs. ## Components ### actions Actions are rendered within the header of the Admin Panel. Actions are typically used to display buttons that add additional interactivity and functionality, although they can be anything you'd like. To add an action, use the `actions` property in your `admin.components` config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { actions: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple Action component: ```tsx export default function MyCustomAction() { return ( ) } ``` **Note:** You can also use add Actions to the [Edit View](./edit-view) and [List View](./list-view) in their respective configs. ### beforeDashboard The `beforeDashboard` property allows you to inject Custom Components into the built-in Dashboard, before the default dashboard contents. To add `beforeDashboard` components, use the `admin.components.beforeDashboard` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { beforeDashboard: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `beforeDashboard` component: ```tsx export default function MyBeforeDashboardComponent() { return
This is a custom component injected before the Dashboard.
} ``` ### afterDashboard Similar to `beforeDashboard`, the `afterDashboard` property allows you to inject Custom Components into the built-in Dashboard, _after_ the default dashboard contents. To add `afterDashboard` components, use the `admin.components.afterDashboard` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { afterDashboard: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `afterDashboard` component: ```tsx export default function MyAfterDashboardComponent() { return
This is a custom component injected after the Dashboard.
} ``` ### beforeLogin The `beforeLogin` property allows you to inject Custom Components into the built-in Login view, _before_ the default login form. To add `beforeLogin` components, use the `admin.components.beforeLogin` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { beforeLogin: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `beforeLogin` component: ```tsx export default function MyBeforeLoginComponent() { return
This is a custom component injected before the Login form.
} ``` ### afterLogin Similar to `beforeLogin`, the `afterLogin` property allows you to inject Custom Components into the built-in Login view, _after_ the default login form. To add `afterLogin` components, use the `admin.components.afterLogin` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { afterLogin: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `afterLogin` component: ```tsx export default function MyAfterLoginComponent() { return
This is a custom component injected after the Login form.
} ``` ### beforeNavLinks The `beforeNavLinks` property allows you to inject Custom Components into the built-in [Nav Component](#nav), _before_ the nav links themselves. To add `beforeNavLinks` components, use the `admin.components.beforeNavLinks` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { beforeNavLinks: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `beforeNavLinks` component: ```tsx export default function MyBeforeNavLinksComponent() { return
This is a custom component injected before the Nav links.
} ``` ### afterNavLinks Similar to `beforeNavLinks`, the `afterNavLinks` property allows you to inject Custom Components into the built-in Nav, _after_ the nav links. To add `afterNavLinks` components, use the `admin.components.afterNavLinks` property in your Payload Config: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... admin: { // highlight-start components: { afterNavLinks: ['/path/to/your/component'], }, // highlight-end }, }) ``` Here is an example of a simple `afterNavLinks` component: ```tsx export default function MyAfterNavLinksComponent() { return

This is a custom component injected after the Nav links.

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

My Custom Header

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

Custom Default Root View

This view uses the Default Template.

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

Custom Default Root View

This view uses the Default Template.

) } ``` ### Securing Custom Views All Custom Views are public by default. It's up to you to secure your custom views. If your view requires a user to be logged in or to have certain access rights, you should handle that within your view component yourself. Here is how you might secure a Custom View: ```tsx import type { AdminViewServerProps } from 'payload' import { Gutter } from '@payloadcms/ui' import React from 'react' export function MyCustomView({ initPageResult }: AdminViewServerProps) { const { req: { user }, } = initPageResult if (!user) { return

You must be logged in to view this page.

} return (

Custom Default Root View

This view uses the Default Template.

) } ``` ## Root Views Root Views are the main views of the [Admin Panel](../admin/overview). These are views that are scoped directly under the `/admin` route, such as the Dashboard or Account views. To [swap out](#replacing-views) Root Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property at the root of your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { components: { views: { // highlight-start dashboard: { Component: '/path/to/Dashboard', }, // highlight-end // Other options include: // - account // - [key: string] // See below for more details }, }, }, }) ``` _For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._ The following options are available: | Property | Description | | ----------- | ----------------------------------------------------------------------------------------------- | | `account` | The Account view is used to show the currently logged in user's Account page. | | `dashboard` | The main landing page of the Admin Panel. | | `[key]` | Any other key can be used to add a completely new Root View. [More details](#adding-new-views). | ## Collection Views Collection Views are views that are scoped under the `/collections` route, such as the Collection List and Document Edit views. To [swap out](#replacing-views) Collection Views with your own, or to [create entirely new ones](#adding-new-views), use the `admin.components.views` property of your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const MyCollectionConfig: CollectionConfig = { // ... admin: { components: { views: { // highlight-start edit: { default: { Component: '/path/to/MyCustomCollectionView', }, }, // highlight-end // Other options include: // - list // - [key: string] // See below for more details }, }, }, } ``` **Reminder:** The `edit` key is comprised of various nested views, known as Document Views, that relate to the same Collection Document. [More details](./document-views). The following options are available: | Property | Description | | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `edit` | The Edit View corresponds to a single Document for any given Collection and consists of various nested views. [More details](./document-views). | | `list` | The List View is used to show a list of Documents for any given Collection. [More details](#list-view). | | `[key]` | Any other key can be used to add a completely new Collection View. [More details](#adding-new-views). | _For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._ ## Global Views Global Views are views that are scoped under the `/globals` route, such as the Edit View. To [swap out](#replacing-views) Global Views with your own or [create entirely new ones](#adding-new-views), use the `admin.components.views` property in your [Global Config](../configuration/globals): ```ts import type { SanitizedGlobalConfig } from 'payload' export const MyGlobalConfig: SanitizedGlobalConfig = { // ... admin: { components: { views: { // highlight-start edit: { default: { Component: '/path/to/MyCustomGlobalView', }, }, // highlight-end // Other options include: // - [key: string] // See below for more details }, }, }, } ``` **Reminder:** The `edit` key is comprised of various nested views, known as Document Views, that relate to the same Global Document. [More details](./document-views). The following options are available: | Property | Description | | -------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `edit` | The Edit View represents a single Document for any given Global and consists of various nested views. [More details](./document-views). | | `[key]` | Any other key can be used to add a completely new Global View. [More details](#adding-new-views). | _For details on how to build Custom Views, including all available props, see [Building Custom Views](#building-custom-views)._ # Document Views Source: https://payloadcms.com/docs/custom-components/document-views Document Views consist of multiple, individual views that together represent any single [Collection](../configuration/collections) or [Global](../configuration/globals) Document. All Document Views and are scoped under the `/collections/:collectionSlug/:id` or the `/globals/:globalSlug` route, respectively. There are a number of default Document Views, such as the [Edit View](./edit-view) and API View, but you can also create [entirely new views](./custom-views#adding-new-views) as needed. All Document Views share a layout and can be given their own tab-based navigation, if desired. To customize Document Views, use the `admin.components.views.edit[key]` property in your [Collection Config](../configuration/collections) or [Global Config](../configuration/globals): ```ts import type { CollectionConfig } from 'payload' export const MyCollectionOrGlobalConfig: CollectionConfig = { // ... admin: { components: { views: { // highlight-start edit: { default: { Component: '/path/to/MyCustomEditView', }, // Other options include: // - root // - api // - versions // - version // - [key: string] // See below for more details }, // highlight-end }, }, }, } ``` ## Config Options The following options are available: | Property | Description | | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `root` | The Root View overrides all other nested views and routes. No document controls or tabs are rendered when this key is set. [More details](#document-root). | | `default` | The Default View is the primary view in which your document is edited. It is rendered within the "Edit" tab. [More details](./edit-view). | | `versions` | The Versions View is used to navigate the version history of a single document. It is rendered within the "Versions" tab. [More details](../versions/overview). | | `version` | The Version View is used to edit a single version of a document. It is rendered within the "Version" tab. [More details](../versions/overview). | | `api` | The API View is used to display the REST API JSON response for a given document. It is rendered within the "API" tab. | | `livePreview` | The LivePreview view is used to display the Live Preview interface. It is rendered within the "Live Preview" tab. [More details](../live-preview/overview). | | `[key]` | Any other key can be used to add a completely new Document View. | _For details on how to build Custom Views, including all available props, see [Building Custom Views](./custom-views#building-custom-views)._ ### Document Root The Document Root is mounted on the top-level route for a Document. Setting this property will completely take over the entire Document View layout, including the title, [Document Tabs](#document-tabs), _and all other nested Document Views_ including the [Edit View](./edit-view), API View, etc. When setting a Document Root, you are responsible for rendering all necessary components and controls, as no document controls or tabs would be rendered. To replace only the Edit View precisely, use the `edit.default` key instead. To override the Document Root, use the `views.edit.root` property in your [Collection Config](../configuration/collections) or [Global Config](../configuration/globals): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { slug: 'my-collection', admin: { components: { views: { edit: { // highlight-start root: { Component: '/path/to/MyCustomRootComponent', // highlight-line }, // highlight-end }, }, }, }, } ``` ### Edit View The Edit View is where users interact with individual Collection and Global Documents. This is where they can view, edit, and save their content. The Edit View is keyed under the `default` property in the `views.edit` object. For more information on customizing the Edit View, see the [Edit View](./edit-view) documentation. ## Document Tabs Each Document View can be given a tab for navigation, if desired. Tabs are highly configurable, from as simple as changing the label to swapping out the entire component, they can be modified in any way. To add or customize tabs in the Document View, use the `views.edit.[key].tab` property in your [Collection Config](../configuration/collections) or [Global Config](../configuration/globals): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { slug: 'my-collection', admin: { components: { views: { edit: { myCustomView: { Component: '/path/to/MyCustomView', path: '/my-custom-tab', // highlight-start tab: { Component: '/path/to/MyCustomTabComponent', }, // highlight-end }, anotherCustomView: { Component: '/path/to/AnotherCustomView', path: '/another-custom-view', // highlight-start tab: { label: 'Another Custom View', href: '/another-custom-view', order: '100', }, // highlight-end }, }, }, }, }, } ``` **Note:** This applies to _both_ Collections _and_ Globals. The following options are available for tabs: | Property | Description | | ----------- | ------------------------------------------------------------------------------------------------------------- | | `label` | The label to display in the tab. | | `href` | The URL to navigate to when the tab is clicked. This is optional and defaults to the tab's `path`. | | `order` | The order in which the tab appears in the navigation. Can be set on default and custom tabs. | | `Component` | The component to render in the tab. This can be a Server or Client component. [More details](#tab-components) | ### Tab Components If changing the label or href is not enough, you can also replace the entire tab component with your own custom component. This can be done by setting the `tab.Component` property to the path of your custom component. Here is an example of how to scaffold a custom Document Tab: #### Server Component ```tsx import React from 'react' import type { DocumentTabServerProps } from 'payload' import { Link } from '@payloadcms/ui' export function MyCustomTabComponent(props: DocumentTabServerProps) { return ( This is a custom Document Tab (Server) ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import type { DocumentTabClientProps } from 'payload' import { Link } from '@payloadcms/ui' export function MyCustomTabComponent(props: DocumentTabClientProps) { return ( This is a custom Document Tab (Client) ) } ``` # Edit View Source: https://payloadcms.com/docs/custom-components/edit-view The Edit View is where users interact with individual [Collection](../configuration/collections) and [Global](../configuration/globals) Documents within the [Admin Panel](../admin/overview). The Edit View contains the actual form in which submits the data to the server. This is where they can view, edit, and save their content. It contains controls for saving, publishing, and previewing the document, all of which can be customized to a high degree. The Edit View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view. **Note:** The Edit View is one of many [Document Views](./document-views) in the Payload Admin Panel. Each Document View is responsible for a different aspect of the interacting with a single Document. ## Custom Edit View To swap out the entire Edit View with a [Custom View](./custom-views), use the `views.edit.default` property in your [Collection Config](../configuration/collections) or [Global Config](../configuration/globals): ```tsx import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { components: { views: { edit: { // highlight-start default: { Component: '/path/to/MyCustomEditViewComponent', }, // highlight-end }, }, }, }, }) ``` Here is an example of a custom Edit View: #### Server Component ```tsx import React from 'react' import type { DocumentViewServerProps } from 'payload' export function MyCustomServerEditView(props: DocumentViewServerProps) { return
This is a custom Edit View (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { DocumentViewClientProps } from 'payload' export function MyCustomClientEditView(props: DocumentViewClientProps) { return
This is a custom Edit View (Client)
} ``` _For details on how to build Custom Views, including all available props, see [Building Custom Views](./custom-views#building-custom-views)._ ## Custom Components In addition to swapping out the entire Edit View with a [Custom View](./custom-views), you can also override individual components. This allows you to customize specific parts of the Edit View without swapping out the entire view. **Important:** Collection and Globals are keyed to a different property in the `admin.components` object have slightly different options. Be sure to use the correct key for the entity you are working with. #### Collections To override Edit View components for a Collection, use the `admin.components.edit` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start edit: { // ... }, // highlight-end }, }, } ``` The following options are available: | Path | Description | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). | | `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document control bar. [More details](#editmenuitems). | | `SaveButton` | A button that saves the current document. [More details](#savebutton). | | `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). | | `PublishButton` | A button that publishes the current document. [More details](#publishbutton). | | `PreviewButton` | A button that previews the current document. [More details](#previewbutton). | | `Description` | A description of the Collection. [More details](#description). | | `Upload` | A file upload component. [More details](#upload). | #### Globals To override Edit View components for Globals, use the `admin.components.elements` property in your [Global Config](../configuration/globals): ```ts import type { GlobalConfig } from 'payload' export const MyGlobal: GlobalConfig = { // ... admin: { components: { // highlight-start elements: { // ... }, // highlight-end }, }, } ``` The following options are available: | Path | Description | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------- | | `beforeDocumentControls` | Inject custom components before the Save / Publish buttons. [More details](#beforedocumentcontrols). | | `editMenuItems` | Inject custom components within the 3-dot menu dropdown located in the document control bar. [More details](#editmenuitems). | | `SaveButton` | A button that saves the current document. [More details](#savebutton). | | `SaveDraftButton` | A button that saves the current document as a draft. [More details](#savedraftbutton). | | `PublishButton` | A button that publishes the current document. [More details](#publishbutton). | | `PreviewButton` | A button that previews the current document. [More details](#previewbutton). | | `Description` | A description of the Global. [More details](#description). | ### SaveButton The `SaveButton` property allows you to render a custom Save Button in the Edit View. To add a `SaveButton` component, use the `components.edit.SaveButton` property in your [Collection Config](../configuration/collections) or `components.elements.SaveButton` in your [Global Config](../configuration/globals): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { edit: { // highlight-start SaveButton: '/path/to/MySaveButton', // highlight-end }, }, }, } ``` Here's an example of a custom `SaveButton` component: #### Server Component ```tsx import React from 'react' import { SaveButton } from '@payloadcms/ui' import type { SaveButtonServerProps } from 'payload' export function MySaveButton(props: SaveButtonServerProps) { return } ``` #### Client Component ```tsx 'use client' import React from 'react' import { SaveButton } from '@payloadcms/ui' import type { SaveButtonClientProps } from 'payload' export function MySaveButton(props: SaveButtonClientProps) { return } ``` ### beforeDocumentControls The `beforeDocumentControls` property allows you to render custom components just before the default document action buttons (like Save, Publish, or Preview). This is useful for injecting custom buttons, status indicators, or any other UI elements before the built-in controls. To add `beforeDocumentControls` components, use the `components.edit.beforeDocumentControls` property in you [Collection Config](../configuration/collections) or `components.elements.beforeDocumentControls` in your [Global Config](../configuration/globals): #### Collections ``` export const MyCollection: CollectionConfig = { admin: { components: { edit: { // highlight-start beforeDocumentControls: ['/path/to/CustomComponent'], // highlight-end }, }, }, } ``` #### Globals ``` export const MyGlobal: GlobalConfig = { admin: { components: { elements: { // highlight-start beforeDocumentControls: ['/path/to/CustomComponent'], // highlight-end }, }, }, } ``` Here's an example of a custom `beforeDocumentControls` component: #### Server Component ```tsx import React from 'react' import type { BeforeDocumentControlsServerProps } from 'payload' export function MyCustomDocumentControlButton( props: BeforeDocumentControlsServerProps, ) { return
This is a custom beforeDocumentControl button (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { BeforeDocumentControlsClientProps } from 'payload' export function MyCustomDocumentControlButton( props: BeforeDocumentControlsClientProps, ) { return
This is a custom beforeDocumentControl button (Client)
} ``` ### editMenuItems The `editMenuItems` property allows you to inject custom components into the 3-dot menu dropdown located in the document controls bar. This dropdown contains default options including `Create New`, `Duplicate`, `Delete`, and other options when additional features are enabled. Any custom components you add will appear below these default items. To add `editMenuItems`, use the `components.edit.editMenuItems` property in your [Collection Config](../configuration/collections): #### Config Example ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', admin: { components: { edit: { // highlight-start editMenuItems: ['/path/to/CustomEditMenuItem'], // highlight-end }, }, }, } ``` Here's an example of a custom `editMenuItems` component: #### Server Component ```tsx import React from 'react' import type { EditMenuItemsServerProps } from 'payload' export const EditMenuItems = async (props: EditMenuItemsServerProps) => { const href = `/custom-action?id=${props.id}` return ( <> Custom Edit Menu Item Another Custom Edit Menu Item - add as many as you need! ) } ``` #### Client Component ```tsx 'use client' import React from 'react' import { PopupList } from '@payloadcms/ui' import type { EditViewMenuItemClientProps } from 'payload' export const EditMenuItems = (props: EditViewMenuItemClientProps) => { const handleClick = () => { console.log('Custom button clicked!') } return ( Custom Edit Menu Item Another Custom Edit Menu Item - add as many as you need! ) } ``` **Styling:** Use Payload's built-in `PopupList.Button` to ensure your menu items automatically match the default dropdown styles. If you want a different look, you can customize the appearance by passing your own `className` to `PopupList.Button`, or use a completely custom button built with a standard HTML `button` element or any other component that fits your design preferences. ### SaveDraftButton The `SaveDraftButton` property allows you to render a custom Save Draft Button in the Edit View. To add a `SaveDraftButton` component, use the `components.edit.SaveDraftButton` property in your [Collection Config](../configuration/collections) or `components.elements.SaveDraftButton` in your [Global Config](../configuration/globals): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { edit: { // highlight-start SaveDraftButton: '/path/to/MySaveDraftButton', // highlight-end }, }, }, } ``` Here's an example of a custom `SaveDraftButton` component: #### Server Component ```tsx import React from 'react' import { SaveDraftButton } from '@payloadcms/ui' import type { SaveDraftButtonServerProps } from 'payload' export function MySaveDraftButton(props: SaveDraftButtonServerProps) { return } ``` #### Client Component ```tsx 'use client' import React from 'react' import { SaveDraftButton } from '@payloadcms/ui' import type { SaveDraftButtonClientProps } from 'payload' export function MySaveDraftButton(props: SaveDraftButtonClientProps) { return } ``` ### PublishButton The `PublishButton` property allows you to render a custom Publish Button in the Edit View. To add a `PublishButton` component, use the `components.edit.PublishButton` property in your [Collection Config](../configuration/collections) or `components.elements.PublishButton` in your [Global Config](../configuration/globals): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { edit: { // highlight-start PublishButton: '/path/to/MyPublishButton', // highlight-end }, }, }, } ``` Here's an example of a custom `PublishButton` component: #### Server Component ```tsx import React from 'react' import { PublishButton } from '@payloadcms/ui' import type { PublishButtonClientProps } from 'payload' export function MyPublishButton(props: PublishButtonServerProps) { return } ``` #### Client Component ```tsx 'use client' import React from 'react' import { PublishButton } from '@payloadcms/ui' import type { PublishButtonClientProps } from 'payload' export function MyPublishButton(props: PublishButtonClientProps) { return } ``` ### PreviewButton The `PreviewButton` property allows you to render a custom Preview Button in the Edit View. To add a `PreviewButton` component, use the `components.edit.PreviewButton` property in your [Collection Config](../configuration/collections) or `components.elements.PreviewButton` in your [Global Config](../configuration/globals): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { edit: { // highlight-start PreviewButton: '/path/to/MyPreviewButton', // highlight-end }, }, }, } ``` Here's an example of a custom `PreviewButton` component: #### Server Component ```tsx import React from 'react' import { PreviewButton } from '@payloadcms/ui' import type { PreviewButtonServerProps } from 'payload' export function MyPreviewButton(props: PreviewButtonServerProps) { return } ``` #### Client Component ```tsx 'use client' import React from 'react' import { PreviewButton } from '@payloadcms/ui' import type { PreviewButtonClientProps } from 'payload' export function MyPreviewButton(props: PreviewButtonClientProps) { return } ``` ### Description The `Description` property allows you to render a custom description of the Collection or Global in the Edit View. To add a `Description` component, use the `components.edit.Description` property in your [Collection Config](../configuration/collections) or `components.elements.Description` in your [Global Config](../configuration/globals): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start Description: '/path/to/MyDescriptionComponent', // highlight-end }, }, } ``` **Note:** The `Description` component is shared between the Edit View and the [List View](./list-view). Here's an example of a custom `Description` component: #### Server Component ```tsx import React from 'react' import type { ViewDescriptionServerProps } from 'payload' export function MyDescriptionComponent(props: ViewDescriptionServerProps) { return
This is a custom description component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { ViewDescriptionClientProps } from 'payload' export function MyDescriptionComponent(props: ViewDescriptionClientProps) { return
This is a custom description component (Client)
} ``` ### Upload The `Upload` property allows you to render a custom file upload component in the Edit View. To add an `Upload` component, use the `components.edit.Upload` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { edit: { // highlight-start Upload: '/path/to/MyUploadComponent', // highlight-end }, }, }, } ``` **Note:** The Upload component is only available for Collections. Here's an example of a custom `Upload` component: ```tsx import React from 'react' export function MyUploadComponent() { return } ``` # List View Source: https://payloadcms.com/docs/custom-components/list-view The List View is where users interact with a list of [Collection](../configuration/collections) Documents within the [Admin Panel](../admin/overview). This is where they can view, sort, filter, and paginate their documents to find exactly what they're looking for. This is also where users can perform bulk operations on multiple documents at once, such as deleting, editing, or publishing many. The List View can be swapped out in its entirety for a Custom View, or it can be injected with a number of Custom Components to add additional functionality or presentational elements without replacing the entire view. **Note:** Only [Collections](../configuration/collections) have a List View. [Globals](../configuration/globals) do not have a List View as they are single documents. ## Custom List View To swap out the entire List View with a [Custom View](./custom-views), use the `admin.components.views.list` property in your [Payload Config](../configuration/overview): ```tsx import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { components: { views: { // highlight-start list: '/path/to/MyCustomListView', // highlight-end }, }, }, }) ``` Here is an example of a custom List View: #### Server Component ```tsx import React from 'react' import type { ListViewServerProps } from 'payload' import { DefaultListView } from '@payloadcms/ui' export function MyCustomServerListView(props: ListViewServerProps) { return
This is a custom List View (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { ListViewClientProps } from 'payload' export function MyCustomClientListView(props: ListViewClientProps) { return
This is a custom List View (Client)
} ``` _For details on how to build Custom Views, including all available props, see [Building Custom Views](./custom-views#building-custom-views)._ ## Custom Components In addition to swapping out the entire List View with a [Custom View](./custom-views), you can also override individual components. This allows you to customize specific parts of the List View without swapping out the entire view for your own. To override List View components for a Collection, use the `admin.components` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { // highlight-start components: { // ... }, // highlight-end }, } ``` The following options are available: | Path | Description | | ----------------- | ------------------------------------------------------------------------------------------------------------------------- | | `beforeList` | An array of custom components to inject before the list of documents in the List View. [More details](#beforelist). | | `beforeListTable` | An array of custom components to inject before the table of documents in the List View. [More details](#beforelisttable). | | `afterList` | An array of custom components to inject after the list of documents in the List View. [More details](#afterlist). | | `afterListTable` | An array of custom components to inject after the table of documents in the List View. [More details](#afterlisttable). | | `listMenuItems` | An array of components to render within a menu next to the List Controls (after the Columns and Filters options) | | `Description` | A component to render a description of the Collection. [More details](#description). | ### beforeList The `beforeList` property allows you to inject custom components before the list of documents in the List View. To add `beforeList` components, use the `components.beforeList` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start beforeList: ['/path/to/MyBeforeListComponent'], // highlight-end }, }, } ``` Here's an example of a custom `beforeList` component: #### Server Component ```tsx import React from 'react' import type { BeforeListServerProps } from 'payload' export function MyBeforeListComponent(props: BeforeListServerProps) { return
This is a custom beforeList component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { BeforeListClientProps } from 'payload' export function MyBeforeListComponent(props: BeforeListClientProps) { return
This is a custom beforeList component (Client)
} ``` ### beforeListTable The `beforeListTable` property allows you to inject custom components before the table of documents in the List View. To add `beforeListTable` components, use the `components.beforeListTable` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start beforeListTable: ['/path/to/MyBeforeListTableComponent'], // highlight-end }, }, } ``` Here's an example of a custom `beforeListTable` component: #### Server Component ```tsx import React from 'react' import type { BeforeListTableServerProps } from 'payload' export function MyBeforeListTableComponent(props: BeforeListTableServerProps) { return
This is a custom beforeListTable component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { BeforeListTableClientProps } from 'payload' export function MyBeforeListTableComponent(props: BeforeListTableClientProps) { return
This is a custom beforeListTable component (Client)
} ``` ### afterList The `afterList` property allows you to inject custom components after the list of documents in the List View. To add `afterList` components, use the `components.afterList` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start afterList: ['/path/to/MyAfterListComponent'], // highlight-end }, }, } ``` Here's an example of a custom `afterList` component: #### Server Component ```tsx import React from 'react' import type { AfterListServerProps } from 'payload' export function MyAfterListComponent(props: AfterListServerProps) { return
This is a custom afterList component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { AfterListClientProps } from 'payload' export function MyAfterListComponent(props: AfterListClientProps) { return
This is a custom afterList component (Client)
} ``` ### afterListTable The `afterListTable` property allows you to inject custom components after the table of documents in the List View. To add `afterListTable` components, use the `components.afterListTable` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start afterListTable: ['/path/to/MyAfterListTableComponent'], // highlight-end }, }, } ``` Here's an example of a custom `afterListTable` component: #### Server Component ```tsx import React from 'react' import type { AfterListTableServerProps } from 'payload' export function MyAfterListTableComponent(props: AfterListTableServerProps) { return
This is a custom afterListTable component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { AfterListTableClientProps } from 'payload' export function MyAfterListTableComponent(props: AfterListTableClientProps) { return
This is a custom afterListTable component (Client)
} ``` ### Description The `Description` property allows you to render a custom description of the Collection in the List View. To add a `Description` component, use the `components.Description` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... admin: { components: { // highlight-start Description: '/path/to/MyDescriptionComponent', // highlight-end }, }, } ``` **Note:** The `Description` component is shared between the List View and the [Edit View](./edit-view). Here's an example of a custom `Description` component: #### Server Component ```tsx import React from 'react' import type { ViewDescriptionServerProps } from 'payload' export function MyDescriptionComponent(props: ViewDescriptionServerProps) { return
This is a custom Collection description component (Server)
} ``` #### Client Component ```tsx 'use client' import React from 'react' import type { ViewDescriptionClientProps } from 'payload' export function MyDescriptionComponent(props: ViewDescriptionClientProps) { return
This is a custom Collection description component (Client)
} ``` # Authentication Overview Source: https://payloadcms.com/docs/authentication/overview Authentication is a critical part of any application. Payload provides a secure, portable way to manage user accounts out of the box. Payload Authentication is designed to be used in both the [Admin Panel](../admin/overview), as well as your own external applications, completely eliminating the need for paid, third-party platforms and services. Here are some common use cases of Authentication in your own applications: - Customer accounts for an e-commerce app - User accounts for a SaaS product - P2P apps or social sites where users need to log in and manage their profiles - Online games where players need to track their progress over time When Authentication is enabled on a [Collection](../configuration/collections), Payload injects all necessary functionality to support the entire user flow. This includes all [auth-related operations](./operations) like account creation, logging in and out, and resetting passwords, all [auth-related emails](./email) like email verification and password reset, as well as any necessary UI to manage users from the Admin Panel. To enable Authentication on a Collection, use the `auth` property in the [Collection Config](../configuration/collections#config-options): ```ts import type { CollectionConfig } from 'payload' export const Users: CollectionConfig = { // ... auth: true, // highlight-line } ``` ![Authentication Admin Panel functionality](https://payloadcms.com/images/docs/auth-overview.jpg) _Admin Panel screenshot depicting an Admins Collection with Auth enabled_ ## Config Options Any [Collection](../configuration/collections) can opt-in to supporting Authentication. Once enabled, each Document that is created within the Collection can be thought of as a "user". This enables a complete authentication workflow on your Collection, such as logging in and out, resetting their password, and more. **Note:** By default, Payload provides an auth-enabled `User` Collection which is used to access the Admin Panel. [More details](../admin/overview#the-admin-user-collection). To enable Authentication on a Collection, use the `auth` property in the [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const Admins: CollectionConfig = { // ... // highlight-start auth: { tokenExpiration: 7200, // How many seconds to keep the user logged in verify: true, // Require email verification before being allowed to authenticate maxLoginAttempts: 5, // Automatically lock a user out after X amount of failed logins lockTime: 600 * 1000, // Time period to allow the max login attempts // More options are available }, // highlight-end } ``` **Tip:** For default auth behavior, set `auth: true`. This is a good starting point for most applications. **Note:** Auth-enabled Collections will be automatically injected with the `hash`, `salt`, and `email` fields. [More details](../fields/overview#field-names). The following options are available: | Option | Description | | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`cookies`** | Set cookie options, including `secure`, `sameSite`, and `domain`. For advanced users. | | **`depth`** | How many levels deep a `user` document should be populated when creating the JWT and binding the `user` to the `req`. Defaults to `0` and should only be modified if absolutely necessary, as this will affect performance. | | **`disableLocalStrategy`** | Advanced - disable Payload's built-in local auth strategy. Only use this property if you have replaced Payload's auth mechanisms with your own. | | **`forgotPassword`** | Customize the way that the `forgotPassword` operation functions. [More details](./email#forgot-password). | | **`lockTime`** | Set the time (in milliseconds) that a user should be locked out if they fail authentication more times than `maxLoginAttempts` allows for. | | **`loginWithUsername`** | Ability to allow users to login with username/password. [More](../authentication/overview#login-with-username) | | **`maxLoginAttempts`** | Only allow a user to attempt logging in X amount of times. Automatically locks out a user from authenticating if this limit is passed. Set to `0` to disable. | | **`removeTokenFromResponses`** | Set to true if you want to remove the token from the returned authentication API responses such as login or refresh. | | **`strategies`** | Advanced - an array of custom authentication strategies to extend this collection's authentication with. [More details](./custom-strategies). | | **`tokenExpiration`** | How long (in seconds) to keep the user logged in. JWTs and HTTP-only cookies will both expire at the same time. | | **`useAPIKey`** | Payload Authentication provides for API keys to be set on each user within an Authentication-enabled Collection. [More details](./api-keys). | | **`useSessions`** | True by default. Set to `false` to use stateless JWTs for authentication instead of sessions. | | **`verify`** | Set to `true` or pass an object with verification options to require users to verify by email before they are allowed to log into your app. [More details](./email#email-verification). | ### Login With Username You can allow users to login with their username instead of their email address by setting the `loginWithUsername` property to `true`. Example: ```ts { slug: 'customers', auth: { loginWithUsername: true, }, } ``` Or, you can pass an object with additional options: ```ts { slug: 'customers', auth: { loginWithUsername: { allowEmailLogin: true, // default: false requireEmail: false, // default: false }, }, } ``` **`allowEmailLogin`** If set to `true`, users can log in with either their username or email address. If set to `false`, users can only log in with their username. **`requireEmail`** If set to `true`, an email address is required when creating a new user. If set to `false`, email is not required upon creation. ## Auto-Login For testing and demo purposes you may want to skip forcing the user to login in order to access your application. Typically, all users should be required to login, however, you can speed up local development time by enabling auto-login. To enable auto-login, set the `autoLogin` property in the [Payload Config](../admin/overview#admin-options): ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... // highlight-start admin: { autoLogin: process.env.NODE_ENV === 'development' ? { email: 'test@example.com', password: 'test', prefillOnly: true, } : false, }, // highlight-end }) ``` **Warning:** The recommended way to use this feature is behind an [Environment Variable](../configuration/environment-vars). This will ensure it is _disabled_ in production. The following options are available: | Option | Description | | ----------------- | --------------------------------------------------------------------------------------------------------------- | | **`username`** | The username of the user to login as | | **`email`** | The email address of the user to login as | | **`password`** | The password of the user to login as. This is only needed if `prefillOnly` is set to true | | **`prefillOnly`** | If set to true, the login credentials will be prefilled but the user will still need to click the login button. | ## Auto-Refresh Turning this property on will allow users to stay logged in indefinitely while their browser is open and on the admin panel, by automatically refreshing their authentication token before it expires. To enable auto-refresh for user tokens, set `autoRefresh: true` in the [Payload Config](../admin/overview#admin-options) to: ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... // highlight-start admin: { autoRefresh: true, }, // highlight-end }) ``` ## Operations All auth-related operations are available via Payload's REST, Local, and GraphQL APIs. These operations are automatically added to your Collection when you enable Authentication. [More details](./operations). ## Strategies Out of the box Payload ships with three powerful Authentication strategies: - [HTTP-Only Cookies](./cookies) - [JSON Web Tokens (JWT)](./jwt) - [API-Keys](./api-keys) Each of these strategies can work together or independently. You can also create your own custom strategies to fit your specific needs. [More details](./custom-strategies). ### HTTP-Only Cookies [HTTP-only cookies](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) are a highly secure method of storing identifiable data on a user's device so that Payload can automatically recognize a returning user until their cookie expires. They are totally protected from common XSS attacks and **cannot be read by JavaScript in the browser**, unlike JWT's. [More details](./cookies). ### JSON Web Tokens JWT (JSON Web Tokens) can also be utilized to perform authentication. Tokens are generated on `login`, `refresh` and `me` operations and can be attached to future requests to authenticate users. [More details](./jwt). ### API Keys API Keys can be enabled on auth collections. These are particularly useful when you want to authenticate against Payload from a third party service. [More details](./api-keys). ### Custom Strategies There are cases where these may not be enough for your application. Payload is extendable by design so you can wire up your own strategy when you need to. [More details](./custom-strategies). ### Access Control Default auth fields including `email`, `username`, and `password` can be overridden by defining a custom field with the same name in your collection config. This allows you to customize the field — including access control — while preserving the underlying auth functionality. For example, you might want to restrict the `email` field from being updated once it is created, or only allow it to be read by certain user roles. You can achieve this by redefining the field and setting access rules accordingly. Here's an example of how to restrict access to default auth fields: ```ts import type { CollectionConfig } from 'payload' export const Auth: CollectionConfig = { slug: 'users', auth: true, fields: [ { name: 'email', // or 'username' type: 'text', access: { create: () => true, read: () => false, update: () => false, }, }, { name: 'password', // this will be applied to all password-related fields including new password, confirm password. type: 'text', hidden: true, // needed only for the password field to prevent duplication in the Admin panel access: { update: () => false, }, }, ], } ``` **Note:** - Access functions will apply across the application — I.e. if `read` access is disabled on `email`, it will not appear in the Admin panel UI or API. - Restricting `read` on the `email` or `username` disables the **Unlock** action in the Admin panel as this function requires access to a user-identifying field. - When overriding the `password` field, you may need to include `hidden: true` to prevent duplicate fields being displayed in the Admin panel. # Authentication Operations Source: https://payloadcms.com/docs/authentication/operations Enabling [Authentication](./overview) on a [Collection](../configuration/collections) automatically exposes additional auth-based operations in the [Local API](../local-api/overview), [REST API](../rest-api/overview), and [GraphQL API](../graphql/overview). ## Access The Access operation returns what a logged in user can and can't do with the collections and globals that are registered via your config. This data can be immensely helpful if your app needs to show and hide certain features based on [Access Control](../access-control/overview), just as the [Admin Panel](../admin/overview) does. **REST API endpoint**: `GET http://localhost:3000/api/access` Example response: ```ts { canAccessAdmin: true, collections: { pages: { create: { permission: true, }, read: { permission: true, }, update: { permission: true, }, delete: { permission: true, }, fields: { title: { create: { permission: true, }, read: { permission: true, }, update: { permission: true, }, } } } } } ``` **Example GraphQL Query**: ```graphql query { Access { pages { read { permission } } } } ``` Document access can also be queried on a collection/global basis. Access on a global can be queried like `http://localhost:3000/api/global-slug/access`, Collection document access can be queried like `http://localhost:3000/api/collection-slug/access/:id`. ## Me Returns either a logged in user with token or null when there is no logged in user. **REST API endpoint**: `GET http://localhost:3000/api/[collection-slug]/me` Example response: ```ts { user: { // The JWT "payload" ;) from the logged in user email: 'dev@payloadcms.com', createdAt: "2020-12-27T21:16:45.645Z", updatedAt: "2021-01-02T18:37:41.588Z", id: "5ae8f9bde69e394e717c8832" }, token: '34o4345324...', // The token that can be used to authenticate the user exp: 1609619861, // Unix timestamp representing when the user's token will expire } ``` **Example GraphQL Query**: ```graphql query { me[collection-singular-label] { user { email } exp } } ``` ## Login Accepts an `email` and `password`. On success, it will return the logged in user as well as a token that can be used to authenticate. In the GraphQL and REST APIs, this operation also automatically sets an HTTP-only cookie including the user's token. If you pass a `res` to the Local API operation, Payload will set a cookie there as well. **Example REST API login**: ```ts const res = await fetch('http://localhost:3000/api/[collection-slug]/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'dev@payloadcms.com', password: 'this-is-not-our-password...or-is-it?', }), }) const json = await res.json() // JSON will be equal to the following: /* { user: { email: 'dev@payloadcms.com', createdAt: "2020-12-27T21:16:45.645Z", updatedAt: "2021-01-02T18:37:41.588Z", id: "5ae8f9bde69e394e717c8832" }, token: '34o4345324...', exp: 1609619861 } */ ``` **Example GraphQL Mutation**: ```graphql mutation { login[collection-singular-label](email: "dev@payloadcms.com", password: "yikes") { user { email } exp token } } ``` **Example Local API login**: ```ts const result = await payload.login({ collection: 'collection-slug', data: { email: 'dev@payloadcms.com', password: 'get-out', }, }) ``` **Server Functions:** Payload offers a ready-to-use `login` server function that utilizes the Local API. For integration details and examples, check out the [Server Function docs](../local-api/server-functions#reusable-payload-server-functions). ## Logout As Payload sets HTTP-only cookies, logging out cannot be done by just removing a cookie in JavaScript, as HTTP-only cookies are inaccessible by JS within the browser. So, Payload exposes a `logout` operation to delete the token in a safe way. **Example REST API logout**: ```ts const res = await fetch( 'http://localhost:3000/api/[collection-slug]/logout?allSessions=false', { method: 'POST', headers: { 'Content-Type': 'application/json', }, }, ) ``` **Example GraphQL Mutation**: ``` mutation { logoutUser(allSessions: false) } ``` **Server Functions:** Payload provides a ready-to-use `logout` server function that manages the user's cookie for a seamless logout. For integration details and examples, check out the [Server Function docs](../local-api/server-functions#reusable-payload-server-functions). #### Logging out with sessions enabled By default, logging out will only end the session pertaining to the JWT that was used to log out with. However, you can pass `allSessions: true` to the logout operation in order to end all sessions for the user logging out. ## Refresh Allows for "refreshing" JWTs. If your user has a token that is about to expire, but the user is still active and using the app, you might want to use the `refresh` operation to receive a new token by executing this operation via the authenticated user. This operation requires a non-expired token to send back a new one. If the user's token has already expired, you will need to allow them to log in again to retrieve a new token. If successful, this operation will automatically renew the user's HTTP-only cookie and will send back the updated token in JSON. **Example REST API token refresh**: ```ts const res = await fetch( 'http://localhost:3000/api/[collection-slug]/refresh-token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, }, ) const json = await res.json() // JSON will be equal to the following: /* { user: { email: 'dev@payloadcms.com', createdAt: "2020-12-27T21:16:45.645Z", updatedAt: "2021-01-02T18:37:41.588Z", id: "5ae8f9bde69e394e717c8832" }, refreshedToken: '34o4345324...', exp: 1609619861 } */ ``` **Example GraphQL Mutation**: ``` mutation { refreshToken[collection-singular-label] { user { email } refreshedToken } } ``` **Server Functions:** Payload exports a ready-to-use `refresh` server function that automatically renews the user's token and updates the associated cookie. For integration details and examples, check out the [Server Function docs](../local-api/server-functions#reusable-payload-server-functions). ## Verify by Email If your collection supports email verification, the Verify operation will be exposed which accepts a verification token and sets the user's `_verified` property to `true`, thereby allowing the user to authenticate with the Payload API. **Example REST API user verification**: ```ts const res = await fetch( `http://localhost:3000/api/[collection-slug]/verify/${TOKEN_HERE}`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }, ) ``` **Example GraphQL Mutation**: ```graphql mutation { verifyEmail[collection-singular-label](token: "TOKEN_HERE") } ``` **Example Local API verification**: ```ts const result = await payload.verifyEmail({ collection: 'collection-slug', token: 'TOKEN_HERE', }) ``` **Note:** the token you need to pass to the `verifyEmail` function is unique to verification and is not the same as the token that you can retrieve from the `forgotPassword` operation. It can be found on the user document, as a hidden `_verificationToken` field. If you'd like to retrieve this token, you can use the Local API's `find` or `findByID` methods, setting `showHiddenFields: true`. **Note:** if you do not have a `config.serverURL` set, Payload will attempt to create one for you if the user was created via REST or GraphQL by looking at the incoming `req`. But this is not supported if you are creating the user via the Local API's `payload.create()` method. If this applies to you, and you do not have a `serverURL` set, you may want to override your `verify.generateEmailHTML` function to provide a full URL to link the user to a proper verification page. ## Unlock If a user locks themselves out and you wish to deliberately unlock them, you can utilize the Unlock operation. The [Admin Panel](../admin/overview) features an Unlock control automatically for all collections that feature max login attempts, but you can programmatically unlock users as well by using the Unlock operation. To restrict who is allowed to unlock users, you can utilize the [`unlock`](../access-control/collections#unlock) access control function. **Example REST API unlock**: ```ts const res = await fetch(`http://localhost:3000/api/[collection-slug]/unlock`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, }) ``` **Example GraphQL Mutation**: ``` mutation { unlock[collection-singular-label] } ``` **Example Local API unlock**: ```ts const result = await payload.unlock({ collection: 'collection-slug', }) ``` ## Forgot Password Payload comes with built-in forgot password functionality. Submitting an email address to the Forgot Password operation will generate an email and send it to the respective email address with a link to reset their password. The link to reset the user's password contains a token which is what allows the user to securely reset their password. By default, the Forgot Password operations send users to the [Admin Panel](../admin/overview) to reset their password, but you can customize the generated email to send users to the frontend of your app instead by [overriding the email HTML](../authentication/email#forgot-password). **Example REST API Forgot Password**: ```ts const res = await fetch( `http://localhost:3000/api/[collection-slug]/forgot-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'dev@payloadcms.com', }), }, ) ``` **Example GraphQL Mutation**: ``` mutation { forgotPassword[collection-singular-label](email: "dev@payloadcms.com") } ``` **Example Local API forgot password**: ```ts const token = await payload.forgotPassword({ collection: 'collection-slug', data: { email: 'dev@payloadcms.com', }, disableEmail: false, // you can disable the auto-generation of email via Local API }) ``` **Note:** if you do not have a `config.serverURL` set, Payload will attempt to create one for you if the `forgot-password` operation was triggered via REST or GraphQL by looking at the incoming `req`. But this is not supported if you are calling `payload.forgotPassword()` via the Local API. If you do not have a `serverURL` set, you may want to override your `auth.forgotPassword.generateEmailHTML` function to provide a full URL to link the user to a proper reset-password page. **Tip:** You can stop the reset-password email from being sent via using the Local API. This is helpful if you need to create user accounts programmatically, but not set their password for them. This effectively generates a reset password token which you can then use to send to a page you create, allowing a user to "complete" their account by setting their password. In the background, you'd use the token to "reset" their password. ## Reset Password After a user has "forgotten" their password and a token is generated, that token can be used to send to the reset password operation along with a new password which will allow the user to reset their password securely. **Example REST API Reset Password**: ```ts const res = await fetch(`http://localhost:3000/api/[collection-slug]/reset-password`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ token: 'TOKEN_GOES_HERE' password: 'not-today', }), }); const json = await res.json(); // JSON will be equal to the following: /* { user: { email: 'dev@payloadcms.com', createdAt: "2020-12-27T21:16:45.645Z", updatedAt: "2021-01-02T18:37:41.588Z", id: "5ae8f9bde69e394e717c8832" }, token: '34o4345324...', exp: 1609619861 } */ ``` **Example GraphQL Mutation**: ```graphql mutation { resetPassword[collection-singular-label](token: "TOKEN_GOES_HERE", password: "not-today") } ``` # Authentication Emails Source: https://payloadcms.com/docs/authentication/email [Authentication](./overview) ties directly into the [Email](../email/overview) functionality that Payload provides. This allows you to send emails to users for verification, password resets, and more. While Payload provides default email templates for these actions, you can customize them to fit your brand. ## Email Verification Email Verification forces users to prove they have access to the email address they can authenticate. This will help to reduce spam accounts and ensure that users are who they say they are. To enable Email Verification, use the `auth.verify` property on your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { verify: true, // highlight-line }, } ``` **Tip:** Verification emails are fully customizable. [More details](#generateemailhtml). The following options are available: | Option | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`generateEmailHTML`** | Allows for overriding the HTML within emails that are sent to users indicating how to validate their account. [More details](#generateemailhtml). | | **`generateEmailSubject`** | Allows for overriding the subject of the email that is sent to users indicating how to validate their account. [More details](#generateemailsubject). | #### generateEmailHTML Function that accepts one argument, containing `{ req, token, user }`, that allows for overriding the HTML within emails that are sent to users indicating how to validate their account. The function should return a string that supports HTML, which can optionally be a full HTML email. ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { verify: { // highlight-start generateEmailHTML: ({ req, token, user }) => { // Use the token provided to allow your user to verify their account const url = `https://yourfrontend.com/verify?token=${token}` return `Hey ${user.email}, verify your email by clicking here: ${url}` }, // highlight-end }, }, } ``` **Important:** If you specify a different URL to send your users to for email verification, such as a page on the frontend of your app or similar, you need to handle making the call to the Payload REST or GraphQL verification operation yourself on your frontend, using the token that was provided for you. Above, it was passed via query parameter. #### generateEmailSubject Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function arguments are the same but you can only return a string - not HTML. ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { verify: { // highlight-start generateEmailSubject: ({ req, user }) => { return `Hey ${user.email}, reset your password!` }, // highlight-end }, }, } ``` ## Forgot Password You can customize how the Forgot Password workflow operates with the following options on the `auth.forgotPassword` property: ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { forgotPassword: { // highlight-line // ... }, }, } ``` The following options are available: | Option | Description | | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | **`expiration`** | Configure how long password reset tokens remain valid, specified in milliseconds. | | **`generateEmailHTML`** | Allows for overriding the HTML within emails that are sent to users attempting to reset their password. [More details](#generateEmailHTML). | | **`generateEmailSubject`** | Allows for overriding the subject of the email that is sent to users attempting to reset their password. [More details](#generateEmailSubject). | #### generateEmailHTML This function allows for overriding the HTML within emails that are sent to users attempting to reset their password. The function should return a string that supports HTML, which can be a full HTML email. ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { forgotPassword: { // highlight-start generateEmailHTML: ({ req, token, user }) => { // Use the token provided to allow your user to reset their password const resetPasswordURL = `https://yourfrontend.com/reset-password?token=${token}` return `

Here is my custom email template!

Hello, ${user.email}!

Click below to reset your password.

${resetPasswordURL}

` }, // highlight-end }, }, } ``` **Important:** If you specify a different URL to send your users to for resetting their password, such as a page on the frontend of your app or similar, you need to handle making the call to the Payload REST or GraphQL reset-password operation yourself on your frontend, using the token that was provided for you. Above, it was passed via query parameter. **Tip:** HTML templating can be used to create custom email templates, inline CSS automatically, and more. You can make a reusable function that standardizes all email sent from Payload, which makes sending custom emails more DRY. Payload doesn't ship with an HTML templating engine, so you are free to choose your own. The following arguments are passed to the `generateEmailHTML` function: | Argument | Description | | -------- | ----------------------------------------------------------------- | | `req` | The request object. | | `token` | The token that is generated for the user to reset their password. | | `user` | The user document that is attempting to reset their password. | #### generateEmailSubject Similarly to the above `generateEmailHTML`, you can also customize the subject of the email. The function arguments are the same but you can only return a string - not HTML. ```ts import type { CollectionConfig } from 'payload' export const Customers: CollectionConfig = { // ... auth: { forgotPassword: { // highlight-start generateEmailSubject: ({ req, user }) => { return `Hey ${user.email}, reset your password!` }, // highlight-end }, }, } ``` The following arguments are passed to the `generateEmailSubject` function: | Argument | Description | | -------- | ------------------------------------------------------------- | | `req` | The request object. | | `user` | The user document that is attempting to reset their password. | # Cookie Strategy Source: https://payloadcms.com/docs/authentication/cookies Payload offers the ability to [Authenticate](./overview) via HTTP-only cookies. These can be read from the responses of `login`, `logout`, `refresh`, and `me` auth operations. **Tip:** You can access the logged-in user from within [Access Control](../access-control/overview) and [Hooks](../hooks/overview) through the `req.user` argument. [More details](./token-data). ### Automatic browser inclusion Modern browsers automatically include `http-only` cookies when making requests directly to URLs—meaning that if you are running your API on `https://example.com`, and you have logged in and visit `https://example.com/test-page`, your browser will automatically include the Payload authentication cookie for you. ### HTTP Authentication However, if you use `fetch` or similar APIs to retrieve Payload resources from its REST or GraphQL API, you must specify to include credentials (cookies). Fetch example, including credentials: ```ts const response = await fetch('http://localhost:3000/api/pages', { credentials: 'include', }) const pages = await response.json() ``` For more about including cookies in requests from your app to your Payload API, [read the MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Sending_a_request_with_credentials_included). **Tip:** To make sure you have a Payload cookie set properly in your browser after logging in, you can use the browsers Developer Tools > Application > Cookies > [your-domain-here]. The Developer tools will still show HTTP-only cookies. ### CSRF Attacks CSRF (cross-site request forgery) attacks are common and dangerous. By using an HTTP-only cookie, Payload removes many XSS vulnerabilities, however, CSRF attacks can still be possible. For example, let's say you have a popular app `https://payload-finances.com` that allows users to manage finances, send and receive money. As Payload is using HTTP-only cookies, that means that browsers automatically will include cookies when sending requests to your domain - **no matter what page created the request**. So, if a user of `https://payload-finances.com` is logged in and is browsing around on the internet, they might stumble onto a page with malicious intent. Let's look at an example: ```ts // malicious-intent.com // makes an authenticated request as on your behalf const maliciousRequest = await fetch(`https://payload-finances.com/api/me`, { credentials: 'include', }).then((res) => await res.json()) ``` In this scenario, if your cookie was still valid, malicious-intent.com would be able to make requests like the one above on your behalf. This is a CSRF attack. ### CSRF Prevention Define domains that you trust and are willing to accept Payload HTTP-only cookie based requests from. Use the `csrf` option on the base Payload Config to do this: ```ts // payload.config.ts import { buildConfig } from 'payload' const config = buildConfig({ serverURL: 'https://my-payload-instance.com', // highlight-start csrf: [ // whitelist of domains to allow cookie auth from 'https://your-frontend-app.com', 'https://your-other-frontend-app.com', // `config.serverURL` is added by default if defined ], // highlight-end collections: [ // collections here ], }) export default config ``` #### Cross domain authentication If your frontend is on a different domain than your Payload API then you will not be able to use HTTP-only cookies for authentication by default as they will be considered third-party cookies by the browser. There are a few strategies to get around this: ##### 1. Use subdomains Cookies can cross subdomains without being considered third party cookies, for example if your API is at api.example.com then you can authenticate from example.com. ##### 2. Configure cookies If option 1 isn't possible, then you can get around this limitation by [configuring your cookies](./overview#config-options) on your authentication collection to achieve the following setup: ``` SameSite: None // allows the cookie to cross domains Secure: true // ensures it's sent over HTTPS only HttpOnly: true // ensures it's not accessible via client side JavaScript ``` Configuration example: ```ts { slug: 'users', auth: { cookies: { sameSite: 'None', secure: true, } }, fields: [ // your auth fields here ] }, ``` If you're configuring [cors](../production/preventing-abuse#cross-origin-resource-sharing-cors) in your Payload config, you won't be able to use a wildcard anymore, you'll need to specify the list of allowed domains. **Good to know:** Setting up `secure: true` will not work if you're developing on `http://localhost` or any non-https domain. For local development you should conditionally set this to `false` based on the environment. # JWT Strategy Source: https://payloadcms.com/docs/authentication/jwt Payload offers the ability to [Authenticate](./overview) via JSON Web Tokens (JWT). These can be read from the responses of `login`, `logout`, `refresh`, and `me` auth operations. **Tip:** You can access the logged-in user from within [Access Control](../access-control/overview) and [Hooks](../hooks/overview) through the `req.user` argument. [More details](./token-data). ### Identifying Users Via The Authorization Header In addition to authenticating via an HTTP-only cookie, you can also identify users via the `Authorization` header on an HTTP request. Example: ```ts const user = await fetch('http://localhost:3000/api/users/login', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ email: 'dev@payloadcms.com', password: 'password', }), }).then((req) => await req.json()) const request = await fetch('http://localhost:3000', { headers: { Authorization: `JWT ${user.token}`, }, }) ``` ### Omitting The Token In some cases you may want to prevent the token from being returned from the auth operations. You can do that by setting `removeTokenFromResponses` to `true` like so: ```ts import type { CollectionConfig } from 'payload' export const UsersWithoutJWTs: CollectionConfig = { slug: 'users-without-jwts', auth: { removeTokenFromResponses: true, // highlight-line }, } ``` ## External JWT Validation When validating Payload-generated JWT tokens in external services, use the processed secret rather than your original secret key: ```ts import crypto from 'node:crypto' const secret = crypto .createHash('sha256') .update(process.env.PAYLOAD_SECRET) .digest('hex') .slice(0, 32) ``` **Note:** Payload processes your secret using SHA-256 hash and takes the first 32 characters. This processed value is what's used for JWT operations, not your original secret. # API Key Strategy Source: https://payloadcms.com/docs/authentication/api-keys To integrate with third-party APIs or services, you might need the ability to generate API keys that can be used to identify as a certain user within Payload. API keys are generated on a user-by-user basis, similar to email and passwords, and are meant to represent a single user. For example, if you have a third-party service or external app that needs to be able to perform protected actions against Payload, first you need to create a user within Payload, i.e. `dev@thirdparty.com`. From your external application you will need to authenticate with that user, you have two options: 1. Log in each time with that user and receive an expiring token to request with. 1. Generate a non-expiring API key for that user to request with. **Tip:** This is particularly useful as you can create a "user" that reflects an integration with a specific external service and assign a "role" or specific access only needed by that service/integration. Technically, both of these options will work for third-party integrations but the second option with API key is simpler, because it reduces the amount of work that your integrations need to do to be authenticated properly. To enable API keys on a collection, set the `useAPIKey` auth option to `true`. From there, a new interface will appear in the [Admin Panel](../admin/overview) for each document within the collection that allows you to generate an API key for each user in the Collection. ```ts import type { CollectionConfig } from 'payload' export const ThirdPartyAccess: CollectionConfig = { slug: 'third-party-access', auth: { useAPIKey: true, // highlight-line }, fields: [], } ``` User API keys are encrypted within the database, meaning that if your database is compromised, your API keys will not be. **Important:** If you change your `PAYLOAD_SECRET`, you will need to regenerate your API keys. The secret key is used to encrypt the API keys, so if you change the secret, existing API keys will no longer be valid. ### HTTP Authentication To authenticate REST or GraphQL API requests using an API key, set the `Authorization` header. The header is case-sensitive and needs the slug of the `auth.useAPIKey` enabled collection, then " API-Key ", followed by the `apiKey` that has been assigned. Payload's built-in middleware will then assign the user document to `req.user` and handle requests with the proper [Access Control](../access-control/overview). By doing this, Payload recognizes the request being made as a request by the user associated with that API key. **For example, using Fetch:** ```ts import Users from '../collections/Users' const response = await fetch('http://localhost:3000/api/pages', { headers: { Authorization: `${Users.slug} API-Key ${YOUR_API_KEY}`, }, }) ``` Payload ensures that the same, uniform [Access Control](../access-control/overview) is used across all authentication strategies. This enables you to utilize your existing Access Control configurations with both API keys and the standard email/password authentication. This consistency can aid in maintaining granular control over your API keys. ### API Key Only Auth If you want to use API keys as the only authentication method for a collection, you can disable the default local strategy by setting `disableLocalStrategy` to `true` on the collection's `auth` property. This will disable the ability to authenticate with email and password, and will only allow for authentication via API key. ```ts import type { CollectionConfig } from 'payload' export const ThirdPartyAccess: CollectionConfig = { slug: 'third-party-access', auth: { useAPIKey: true, disableLocalStrategy: true, // highlight-line }, } ``` # Custom Strategies Source: https://payloadcms.com/docs/authentication/custom-strategies This is an advanced feature, so only attempt this if you are an experienced developer. Otherwise, just let Payload's built-in authentication handle user auth for you. ### Creating a strategy At the core, a strategy is a way to authenticate a user making a request. As of `3.0` we moved away from [Passport](https://www.passportjs.org) in favor of pulling back the curtain and putting you in full control. A strategy is made up of the following: | Parameter | Description | | --------------------- | ------------------------------------------------------------------------- | | **`name`** \* | The name of your strategy | | **`authenticate`** \* | A function that takes in the parameters below and returns a user or null. | The `authenticate` function is passed the following arguments: | Argument | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------- | | **`canSetHeaders`** \* | Whether or not the strategy is being executed from a context where response headers can be set. Default is `false`. | | **`headers`** \* | The headers on the incoming request. Useful for retrieving identifiable information on a request. | | **`payload`** \* | The Payload class. Useful for authenticating the identifiable information against Payload. | | **`isGraphQL`** | Whether or not the strategy is being executed within the GraphQL endpoint. Default is `false`. | ### Example Strategy At its core a strategy simply takes information from the incoming request and returns a user. This is exactly how Payload's built-in strategies function. Your `authenticate` method should return an object containing a Payload user document and any optional headers that you'd like Payload to set for you when we return a response. ```ts import type { CollectionConfig } from 'payload' export const Users: CollectionConfig = { slug: 'users', auth: { disableLocalStrategy: true, // highlight-start strategies: [ { name: 'custom-strategy', authenticate: async ({ payload, headers }) => { const usersQuery = await payload.find({ collection: 'users', where: { code: { equals: headers.get('code'), }, secret: { equals: headers.get('secret'), }, }, }) return { // Send the user with the collection slug back to authenticate, // or send null if no user should be authenticated user: usersQuery.docs[0] ? { collection: 'users', ...usersQuery.docs[0], } : null, // Optionally, you can return headers // that you'd like Payload to set here when // it returns the response responseHeaders: new Headers({ 'some-header': 'my header value' }) } } } ] // highlight-end }, fields: [ { name: 'code', type: 'text', index: true, unique: true, }, { name: 'secret', type: 'text', }, ] } ``` # Token Data Source: https://payloadcms.com/docs/authentication/token-data During the lifecycle of a request you will be able to access the data you have configured to be stored in the JWT by accessing `req.user`. The user object is automatically appended to the request for you. ### Defining Token Data You can specify what data gets encoded to the Cookie/JWT-Token by setting `saveToJWT` property on fields within your auth collection. ```ts import type { CollectionConfig } from 'payload' export const Users: CollectionConfig = { slug: 'users', auth: true, fields: [ { // will be stored in the JWT saveToJWT: true, type: 'select', name: 'role', options: ['super-admin', 'user'], }, { // the entire object will be stored in the JWT // tab fields can do the same thing! saveToJWT: true, type: 'group', name: 'group1', fields: [ { type: 'text', name: 'includeField', }, { // will be omitted from the JWT saveToJWT: false, type: 'text', name: 'omitField', }, ], }, { type: 'group', name: 'group2', fields: [ { // will be stored in the JWT // but stored at the top level saveToJWT: true, type: 'text', name: 'includeField', }, { type: 'text', name: 'omitField', }, ], }, ], } ``` **Tip:** If you wish to use a different key other than the field `name`, you can define `saveToJWT` as a string. ### Using Token Data This is especially helpful when writing [Hooks](../hooks/overview) and [Access Control](../access-control/overview) that depend on user defined fields. ```ts import type { CollectionConfig } from 'payload' export const Invoices: CollectionConfig = { slug: 'invoices', access: { read: ({ req, data }) => { if (!req?.user) return false // highlight-start if ({ req.user?.role === 'super-admin'}) { return true } // highlight-end return data.owner === req.user.id } } fields: [ { name: 'owner', relationTo: 'users' }, // ... other fields ], } ``` # Rich Text Editor Source: https://payloadcms.com/docs/rich-text/overview This documentation is about our new editor, based on Lexical (Meta's rich text editor). The previous default editor was based on Slate and is still supported. You can read [its documentation](../rich-text/slate), or the optional [migration guide](../rich-text/migration) to migrate from Slate to Lexical (recommended). The editor is the most important property of the [rich text field](../fields/rich-text). As a key part of Payload, we are proud to offer you the best editing experience you can imagine. With healthy defaults out of the box, but also with the flexibility to customize every detail: from the “/” menu and toolbars (whether inline or fixed) to inserting any component or subfield you can imagine. To use the rich text editor, first you need to install it: ```bash pnpm install @payloadcms/richtext-lexical ``` Once you have it installed, you can pass it to your top-level Payload Config as follows: ```ts import { buildConfig } from 'payload' import { lexicalEditor } from '@payloadcms/richtext-lexical' export default buildConfig({ collections: [ // your collections here ], // Pass the Lexical editor to the root config editor: lexicalEditor({}), }) ``` You can also override Lexical settings on a field-by-field basis as follows: ```ts import type { CollectionConfig } from 'payload' import { lexicalEditor } from '@payloadcms/richtext-lexical' export const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'content', type: 'richText', // Pass the Lexical editor here and override base settings as necessary editor: lexicalEditor({}), }, ], } ``` ## Extending the lexical editor with Features Lexical has been designed with extensibility in mind. Whether you're aiming to introduce new functionalities or tweak the existing ones, Lexical makes it seamless for you to bring those changes to life. ### Features: The Building Blocks At the heart of Lexical's customization potential are "features". While Lexical ships with a set of default features we believe are essential for most use cases, the true power lies in your ability to redefine, expand, or prune these as needed. If you remove all the default features, you're left with a blank editor. You can then add in only the features you need, or you can build your own custom features from scratch. ### Integrating New Features To weave in your custom features, utilize the `features` prop when initializing the Lexical Editor. Here's a basic example of how this is done: ```ts import { BlocksFeature, LinkFeature, UploadFeature, lexicalEditor, } from '@payloadcms/richtext-lexical' import { Banner } from '../blocks/Banner' import { CallToAction } from '../blocks/CallToAction' { editor: lexicalEditor({ features: ({ defaultFeatures, rootFeatures }) => [ ...defaultFeatures, LinkFeature({ // Example showing how to customize the built-in fields // of the Link feature fields: ({ defaultFields }) => [ ...defaultFields, { name: 'rel', label: 'Rel Attribute', type: 'select', hasMany: true, options: ['noopener', 'noreferrer', 'nofollow'], admin: { description: 'The rel attribute defines the relationship between a linked resource and the current document. This is a custom link field.', }, }, ], }), UploadFeature({ collections: { uploads: { // Example showing how to customize the built-in fields // of the Upload feature fields: [ { name: 'caption', type: 'richText', editor: lexicalEditor(), }, ], }, }, }), // This is incredibly powerful. You can re-use your Payload blocks // directly in the Lexical editor as follows: BlocksFeature({ blocks: [Banner, CallToAction], }), ], }) } ``` `features` can be both an array of features, or a function returning an array of features. The function provides the following props: | Prop | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`defaultFeatures`** | This opinionated array contains all "recommended" default features. You can see which features are included in the default features in the table below. | | **`rootFeatures`** | This array contains all features that are enabled in the root richText editor (the one defined in the payload.config.ts). If this field is the root richText editor, or if the root richText editor is not a lexical editor, this array will be empty. | ## Official Features You can find more information about the official features in our [official features docs](../rich-text/official-features). ## Creating your own, custom Feature You can find more information about creating your own feature in our [building custom feature docs](../rich-text/custom-features). ## TypeScript Every single piece of saved data is 100% fully typed within lexical. It provides a type for every single node, which can be imported from `@payloadcms/richtext-lexical` - each type is prefixed with `Serialized`, e.g., `SerializedUploadNode`. To fully type the entire editor JSON, you can use our `TypedEditorState` helper type, which accepts a union of all possible node types as a generic. We don't provide a type that already contains all possible node types because they depend on which features you have enabled in your editor. Here is an example: ```ts import type { SerializedAutoLinkNode, SerializedBlockNode, SerializedHorizontalRuleNode, SerializedLinkNode, SerializedListItemNode, SerializedListNode, SerializedParagraphNode, SerializedQuoteNode, SerializedRelationshipNode, SerializedTextNode, SerializedUploadNode, TypedEditorState, SerializedHeadingNode, } from '@payloadcms/richtext-lexical' const editorState: TypedEditorState< | SerializedAutoLinkNode | SerializedBlockNode | SerializedHorizontalRuleNode | SerializedLinkNode | SerializedListItemNode | SerializedListNode | SerializedParagraphNode | SerializedQuoteNode | SerializedRelationshipNode | SerializedTextNode | SerializedUploadNode | SerializedHeadingNode > = { root: { type: 'root', direction: 'ltr', format: '', indent: 0, version: 1, children: [ { children: [ { detail: 0, format: 0, mode: 'normal', style: '', text: 'Some text. Every property here is fully-typed', type: 'text', version: 1, }, ], direction: 'ltr', format: '', indent: 0, type: 'paragraph', textFormat: 0, version: 1, }, ], }, } ``` Alternatively, you can use the `DefaultTypedEditorState` type, which includes all types for all nodes included in the `defaultFeatures`: ```ts import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical' const editorState: DefaultTypedEditorState = { root: { type: 'root', direction: 'ltr', format: '', indent: 0, version: 1, children: [ { children: [ { detail: 0, format: 0, mode: 'normal', style: '', text: 'Some text. Every property here is fully-typed', type: 'text', version: 1, }, ], direction: 'ltr', format: '', indent: 0, type: 'paragraph', textFormat: 0, version: 1, }, ], }, } ``` Just like `TypedEditorState`, the `DefaultTypedEditorState` also accepts an optional node type union as a generic. Here, this would **add** the specified node types to the default ones. Example: ```ts DefaultTypedEditorState ``` This is a type-safe representation of the editor state. If you look at the auto suggestions of a node's `type` property, you will see all the possible node types you can use. Make sure to only use types exported from `@payloadcms/richtext-lexical`, not from the lexical core packages. We only have control over the types we export and can make sure they're correct, even though the lexical core may export types with identical names. ### Automatic type generation Lexical does not generate accurate type definitions for your richText fields for you yet - this will be improved in the future. Currently, it only outputs the rough shape of the editor JSON, which you can enhance using type assertions. ## Admin customization The Rich Text Field editor configuration has an `admin` property with the following options: | Property | Description | | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | | **`placeholder`** | Set this property to define a placeholder string for the field. | | **`hideGutter`** | Set this property to `true` to hide this field's gutter within the Admin Panel. | | **`hideInsertParagraphAtEnd`** | Set this property to `true` to hide the "+" button that appears at the end of the editor. | | **`hideDraggableBlockElement`** | Set this property to `true` to hide the draggable element that appears when you hover a node in the editor. | | **`hideAddBlockButton`** | Set this property to `true` to hide the "+" button that appears when you hover a node in the editor. | ### Disable the gutter You can disable the gutter (the vertical line padding between the editor and the left edge of the screen) by setting the `hideGutter` prop to `true`: ```ts { name: 'richText', type: 'richText', editor: lexicalEditor({ admin: { hideGutter: true }, }), } ``` ### Customize the placeholder You can customize the placeholder (the text that appears in the editor when it's empty) by setting the `placeholder` prop: ```ts { name: 'richText', type: 'richText', editor: lexicalEditor({ admin: { placeholder: 'Type your content here...' }, }), } ``` ## Detecting empty editor state When you first type into a rich text field and subsequently delete everything through the admin panel, its value changes from `null` to a JSON object containing an empty paragraph. If needed, you can reset the field value to `null` programmatically - for example, by using a custom hook to detect when the editor is empty. This also applies to fields like `text` and `textArea`, which could be stored as either `null` or an empty value in the database. Since the empty value for richText is a JSON object, checking for emptiness is a bit more involved - so Payload provides a utility for it: ```ts import { hasText } from '@payloadcms/richtext-lexical/shared' hasText(richtextData) ``` # Lexical Converters Source: https://payloadcms.com/docs/rich-text/converters Richtext fields save data in JSON - this is great for storage and flexibility and allows you to easily to convert it to other formats: - [Converting JSX](../rich-text/converting-jsx) - [Converting HTML](../rich-text/converting-html) - [Converting Plaintext](../rich-text/converting-plaintext) - [Converting Markdown and MDX](../rich-text/converting-markdown) ## Retrieving the Editor Config Some converters require access to the Lexical editor config, which defines available features and behaviors. Payload provides multiple ways to obtain the editor config through the `editorConfigFactory` from `@payloadcms/richtext-lexical`. ### Importing the Factory First, import the necessary utilities: ```ts import type { SanitizedConfig } from 'payload' import { editorConfigFactory } from '@payloadcms/richtext-lexical' // Your Payload Config needs to be available in order to retrieve the default editor config const config: SanitizedConfig = {} as SanitizedConfig ``` ### Option 1: Default Editor Config If you require the default editor config: ```ts const defaultEditorConfig = await editorConfigFactory.default({ config }) ``` ### Option 2: Extract from a Lexical Field When a lexical field config is available, you can extract the editor config directly: ```ts const fieldEditorConfig = editorConfigFactory.fromField({ field: config.collections[0].fields[1], }) ``` ### Option 3: Create a Custom Editor Config You can create a custom editor configuration by specifying additional features: ```ts import { FixedToolbarFeature } from '@payloadcms/richtext-lexical' const customEditorConfig = await editorConfigFactory.fromFeatures({ config, features: ({ defaultFeatures }) => [ ...defaultFeatures, FixedToolbarFeature(), ], }) ``` ### Option 4: Extract from an Instantiated Editor If you've created a global or reusable Lexical editor instance, you can access its configuration. This method is typically less efficient and not recommended: ```ts const editor = lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, FixedToolbarFeature(), ], }) const instantiatedEditorConfig = await editorConfigFactory.fromEditor({ config, editor, }) ``` For better efficiency, consider extracting the `features` into a separate variable and using `fromFeatures` instead of this method. ### Example - Retrieving the editor config from an existing field If you have access to the sanitized collection config, you can access the lexical sanitized editor config, as every lexical richText field returns it. Here is an example how you can retrieve it from another field's afterRead hook: ```ts import type { CollectionConfig, RichTextField } from 'payload' import { editorConfigFactory, getEnabledNodes, lexicalEditor, } from '@payloadcms/richtext-lexical' export const MyCollection: CollectionConfig = { slug: 'slug', fields: [ { name: 'text', type: 'text', hooks: { afterRead: [ ({ siblingFields, value }) => { const field: RichTextField = siblingFields.find( (field) => 'name' in field && field.name === 'richText', ) as RichTextField const editorConfig = editorConfigFactory.fromField({ field, }) // Now you can use the editor config return value }, ], }, }, { name: 'richText', type: 'richText', editor: lexicalEditor(), }, ], } ``` # Converting JSX Source: https://payloadcms.com/docs/rich-text/converting-jsx ## Richtext to JSX To convert richtext to JSX, import the `RichText` component from `@payloadcms/richtext-lexical/react` and pass the richtext content to it: ```tsx import React from 'react' import { RichText } from '@payloadcms/richtext-lexical/react' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' export const MyComponent = ({ data }: { data: SerializedEditorState }) => { return } ``` The `RichText` component includes built-in converters for common Lexical nodes. You can add or override converters via the `converters` prop for custom blocks, custom nodes, or any modifications you need. See the [website template](https://github.com/payloadcms/payload/blob/main/templates/website/src/components/RichText/index.tsx) for a working example. When fetching data, ensure your `depth` setting is high enough to fully populate Lexical nodes such as uploads. The JSX converter requires fully populated data to work correctly. ### Internal Links By default, Payload doesn't know how to convert **internal** links to JSX, as it doesn't know what the corresponding URL of the internal link is. You'll notice that you get a "found internal link, but internalDocToHref is not provided" error in the console when you try to render content with internal links. To fix this, you need to pass the `internalDocToHref` prop to `LinkJSXConverter`. This prop is a function that receives the link node and returns the URL of the document. ```tsx import type { DefaultNodeTypes, SerializedLinkNode, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { type JSXConvertersFunction, LinkJSXConverter, RichText, } from '@payloadcms/richtext-lexical/react' import React from 'react' const internalDocToHref = ({ linkNode }: { linkNode: SerializedLinkNode }) => { const { relationTo, value } = linkNode.fields.doc! if (typeof value !== 'object') { throw new Error('Expected value to be an object') } const slug = value.slug switch (relationTo) { case 'posts': return `/posts/${slug}` case 'categories': return `/category/${slug}` case 'pages': return `/${slug}` default: return `/${relationTo}/${slug}` } } const jsxConverters: JSXConvertersFunction = ({ defaultConverters, }) => ({ ...defaultConverters, ...LinkJSXConverter({ internalDocToHref }), }) export const MyComponent: React.FC<{ lexicalData: SerializedEditorState }> = ({ lexicalData }) => { return } ``` ### Lexical Blocks If your rich text includes custom Blocks or Inline Blocks, you must supply custom converters that match each block's slug. This converter is not included by default, as Payload doesn't know how to render your custom blocks. For example: ```tsx 'use client' import type { MyInlineBlock, MyNumberBlock, MyTextBlock } from '@/payload-types' import type { DefaultNodeTypes, SerializedBlockNode, SerializedInlineBlockNode, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { type JSXConvertersFunction, RichText, } from '@payloadcms/richtext-lexical/react' import React from 'react' // Extend the default node types with your custom blocks for full type safety type NodeTypes = | DefaultNodeTypes | SerializedBlockNode | SerializedInlineBlockNode const jsxConverters: JSXConvertersFunction = ({ defaultConverters, }) => ({ ...defaultConverters, blocks: { // Each key should match your block's slug myNumberBlock: ({ node }) =>
{node.fields.number}
, myTextBlock: ({ node }) => (
{node.fields.text}
), }, inlineBlocks: { // Each key should match your inline block's slug myInlineBlock: ({ node }) => {node.fields.text}, }, }) export const MyComponent: React.FC<{ lexicalData: SerializedEditorState }> = ({ lexicalData }) => { return } ``` ### Overriding Converters You can override any of the default JSX converters by passing your custom converter, keyed to the node type, to the `converters` prop / the converters function. Example - overriding the upload node converter to use next/image: ```tsx 'use client' import type { DefaultNodeTypes, SerializedUploadNode, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { type JSXConvertersFunction, RichText, } from '@payloadcms/richtext-lexical/react' import Image from 'next/image' import React from 'react' type NodeTypes = DefaultNodeTypes // Custom upload converter component that uses next/image const CustomUploadComponent: React.FC<{ node: SerializedUploadNode }> = ({ node }) => { if (node.relationTo === 'uploads') { const uploadDoc = node.value if (typeof uploadDoc !== 'object') { return null } const { alt, height, url, width } = uploadDoc return {alt} } return null } const jsxConverters: JSXConvertersFunction = ({ defaultConverters, }) => ({ ...defaultConverters, // Override the default upload converter upload: ({ node }) => { return }, }) export const MyComponent: React.FC<{ lexicalData: SerializedEditorState }> = ({ lexicalData }) => { return } ``` # Converting HTML Source: https://payloadcms.com/docs/rich-text/converting-html ## Rich Text to HTML There are two main approaches to convert your Lexical-based rich text to HTML: 1. **Generate HTML on-demand (Recommended)**: Convert JSON to HTML wherever you need it, on-demand. 2. **Generate HTML within your Collection**: Create a new field that automatically converts your saved JSON content to HTML. This is not recommended because it adds overhead to the Payload API. ### On-demand To convert JSON to HTML on-demand, use the `convertLexicalToHTML` function from `@payloadcms/richtext-lexical/html`. Here's an example of how to use it in a React component in your frontend: ```tsx 'use client' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { convertLexicalToHTML } from '@payloadcms/richtext-lexical/html' import React from 'react' export const MyComponent = ({ data }: { data: SerializedEditorState }) => { const html = convertLexicalToHTML({ data }) return
} ``` #### Dynamic Population (Advanced) By default, `convertLexicalToHTML` expects fully populated data (e.g. uploads, links, etc.). If you need to dynamically fetch and populate those nodes, use the async variant, `convertLexicalToHTMLAsync`, from `@payloadcms/richtext-lexical/html-async`. You must provide a `populate` function: ```tsx 'use client' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { getRestPopulateFn } from '@payloadcms/richtext-lexical/client' import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async' import React, { useEffect, useState } from 'react' export const MyComponent = ({ data }: { data: SerializedEditorState }) => { const [html, setHTML] = useState(null) useEffect(() => { async function convert() { const html = await convertLexicalToHTMLAsync({ data, populate: getRestPopulateFn({ apiURL: `http://localhost:3000/api`, }), }) setHTML(html) } void convert() }, [data]) return html &&
} ``` Using the REST populate function will send a separate request for each node. If you need to populate a large number of nodes, this may be slow. For improved performance on the server, you can use the `getPayloadPopulateFn` function: ```tsx import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { getPayloadPopulateFn } from '@payloadcms/richtext-lexical' import { convertLexicalToHTMLAsync } from '@payloadcms/richtext-lexical/html-async' import { getPayload } from 'payload' import React from 'react' import config from '../../config.js' export const MyRSCComponent = async ({ data, }: { data: SerializedEditorState }) => { const payload = await getPayload({ config, }) const html = await convertLexicalToHTMLAsync({ data, populate: await getPayloadPopulateFn({ currentDepth: 0, depth: 1, payload, }), }) return html &&
} ``` ### HTML field The `lexicalHTMLField()` helper converts JSON to HTML and saves it in a field that is updated every time you read it via an `afterRead` hook. It's generally not recommended, as it creates a column with duplicate content in another format. Consider using the [on-demand HTML converter above](../rich-text/converting-html#on-demand-recommended) or the [JSX converter](../rich-text/converting-jsx) unless you have a good reason. ```ts import type { HTMLConvertersFunction } from '@payloadcms/richtext-lexical/html' import type { MyTextBlock } from '@/payload-types.js' import type { CollectionConfig } from 'payload' import { BlocksFeature, type DefaultNodeTypes, lexicalEditor, lexicalHTMLField, type SerializedBlockNode, } from '@payloadcms/richtext-lexical' const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'nameOfYourRichTextField', type: 'richText', editor: lexicalEditor(), }, lexicalHTMLField({ htmlFieldName: 'nameOfYourRichTextField_html', lexicalFieldName: 'nameOfYourRichTextField', }), { name: 'customRichText', type: 'richText', editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, BlocksFeature({ blocks: [ { interfaceName: 'MyTextBlock', slug: 'myTextBlock', fields: [ { name: 'text', type: 'text', }, ], }, ], }), ], }), }, lexicalHTMLField({ htmlFieldName: 'customRichText_html', lexicalFieldName: 'customRichText', // can pass in additional converters or override default ones converters: (({ defaultConverters }) => ({ ...defaultConverters, blocks: { myTextBlock: ({ node, providedCSSString }) => `
${node.fields.text}
`, }, })) as HTMLConvertersFunction< DefaultNodeTypes | SerializedBlockNode >, }), ], } ``` ## Blocks to HTML If your rich text includes Lexical blocks, you need to provide a way to convert them to HTML. For example: ```tsx 'use client' import type { MyInlineBlock, MyTextBlock } from '@/payload-types' import type { DefaultNodeTypes, SerializedBlockNode, SerializedInlineBlockNode, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { convertLexicalToHTML, type HTMLConvertersFunction, } from '@payloadcms/richtext-lexical/html' import React from 'react' type NodeTypes = | DefaultNodeTypes | SerializedBlockNode | SerializedInlineBlockNode const htmlConverters: HTMLConvertersFunction = ({ defaultConverters, }) => ({ ...defaultConverters, blocks: { // Each key should match your block's slug myTextBlock: ({ node, providedCSSString }) => `
${node.fields.text}
`, }, inlineBlocks: { // Each key should match your inline block's slug myInlineBlock: ({ node, providedStyleTag }) => `${node.fields.text}`, }, }) export const MyComponent = ({ data }: { data: SerializedEditorState }) => { const html = convertLexicalToHTML({ converters: htmlConverters, data, }) return
} ``` ## HTML to Richtext If you need to convert raw HTML into a Lexical editor state, use `convertHTMLToLexical` from `@payloadcms/richtext-lexical`, along with the [editorConfigFactory to retrieve the editor config](../rich-text/converters#retrieving-the-editor-config): ```ts import { convertHTMLToLexical, editorConfigFactory, } from '@payloadcms/richtext-lexical' // Make sure you have jsdom and @types/jsdom installed import { JSDOM } from 'jsdom' const html = convertHTMLToLexical({ editorConfig: await editorConfigFactory.default({ config, // Your Payload Config }), html: '

text

', JSDOM, // Pass in the JSDOM import; it's not bundled to keep package size small }) ``` # Converting Markdown Source: https://payloadcms.com/docs/rich-text/converting-markdown ## Richtext to Markdown If you have access to the Payload Config and the [lexical editor config](../rich-text/converters#retrieving-the-editor-config), you can convert the lexical editor state to Markdown with the following: ```ts import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { convertLexicalToMarkdown, editorConfigFactory, } from '@payloadcms/richtext-lexical' // Your richtext data here const data: SerializedEditorState = {} const markdown = convertLexicalToMarkdown({ data, editorConfig: await editorConfigFactory.default({ config, // <= make sure you have access to your Payload Config }), }) ``` ### Example - outputting Markdown from the Collection ```ts import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import type { CollectionConfig, RichTextField } from 'payload' import { convertLexicalToMarkdown, editorConfigFactory, lexicalEditor, } from '@payloadcms/richtext-lexical' const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'nameOfYourRichTextField', type: 'richText', editor: lexicalEditor(), }, { name: 'markdown', type: 'textarea', admin: { hidden: true, }, hooks: { afterRead: [ ({ siblingData, siblingFields }) => { const data: SerializedEditorState = siblingData['nameOfYourRichTextField'] if (!data) { return '' } const markdown = convertLexicalToMarkdown({ data, editorConfig: editorConfigFactory.fromField({ field: siblingFields.find( (field) => 'name' in field && field.name === 'nameOfYourRichTextField', ) as RichTextField, }), }) return markdown }, ], beforeChange: [ ({ siblingData }) => { // Ensure that the markdown field is not saved in the database delete siblingData['markdown'] return null }, ], }, }, ], } ``` ## Markdown to Richtext If you have access to the Payload Config and the [lexical editor config](../rich-text/converters#retrieving-the-editor-config), you can convert Markdown to the lexical editor state with the following: ```ts import { convertMarkdownToLexical, editorConfigFactory, } from '@payloadcms/richtext-lexical' const lexicalJSON = convertMarkdownToLexical({ editorConfig: await editorConfigFactory.default({ config, // <= make sure you have access to your Payload Config }), markdown: '# Hello world\n\nThis is a **test**.', }) ``` ## Converting MDX Payload supports serializing and deserializing MDX content. While Markdown converters are stored on the features, MDX converters are stored on the blocks that you pass to the `BlocksFeature`. ### Defining a Custom Block Here is an example of a `Banner` block. This block: - Renders in the admin UI as a normal Lexical block with specific fields (e.g. type, content). - Converts to an MDX `Banner` component. - Can parse that MDX `Banner` back into a Lexical state. ```ts import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import type { Block, CollectionConfig, RichTextField } from 'payload' import { BlocksFeature, convertLexicalToMarkdown, editorConfigFactory, lexicalEditor, } from '@payloadcms/richtext-lexical' const BannerBlock: Block = { slug: 'Banner', fields: [ { name: 'type', type: 'select', defaultValue: 'info', options: [ { label: 'Info', value: 'info' }, { label: 'Warning', value: 'warning' }, { label: 'Error', value: 'error' }, ], }, { name: 'content', type: 'richText', editor: lexicalEditor(), }, ], jsx: { /** * Convert from Lexical -> MDX: * child content */ export: ({ fields, lexicalToMarkdown }) => { const props: any = {} if (fields.type) { props.type = fields.type } return { children: lexicalToMarkdown({ editorState: fields.content }), props, } }, /** * Convert from MDX -> Lexical: */ import: ({ children, markdownToLexical, props }) => { return { type: props?.type, content: markdownToLexical({ markdown: children }), } }, }, } const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'nameOfYourRichTextField', type: 'richText', editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, BlocksFeature({ blocks: [BannerBlock], }), ], }), }, { name: 'markdown', type: 'textarea', hooks: { afterRead: [ ({ siblingData, siblingFields }) => { const data: SerializedEditorState = siblingData['nameOfYourRichTextField'] if (!data) { return '' } const markdown = convertLexicalToMarkdown({ data, editorConfig: editorConfigFactory.fromField({ field: siblingFields.find( (field) => 'name' in field && field.name === 'nameOfYourRichTextField', ) as RichTextField, }), }) return markdown }, ], beforeChange: [ ({ siblingData }) => { // Ensure that the markdown field is not saved in the database delete siblingData['markdown'] return null }, ], }, }, ], } ``` The conversion is done using the `jsx` property of the block. The `export` function is called when converting from lexical to MDX, and the `import` function is called when converting from MDX to lexical. ### Export The `export` function takes the block field data and the `lexicalToMarkdown` function as arguments. It returns the following object: | Property | Type | Description | | ---------- | ------ | ------------------------------------------------------------------ | | `children` | string | This will be in between the opening and closing tags of the block. | | `props` | object | This will be in the opening tag of the block. | ### Import The `import` function provides data extracted from the MDX. It takes the following arguments: | Argument | Type | Description | | ---------- | ------ | ------------------------------------------------------------------------------------ | | `children` | string | This will be the text between the opening and closing tags of the block. | | `props` | object | These are the props passed to the block, parsed from the opening tag into an object. | The returning object is equal to the block field data. # Converting Plaintext Source: https://payloadcms.com/docs/rich-text/converting-plaintext ## Richtext to Plaintext Here's how you can convert richtext data to plaintext using `@payloadcms/richtext-lexical/plaintext`. ```ts import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import { convertLexicalToPlaintext } from '@payloadcms/richtext-lexical/plaintext' // Your richtext data here const data: SerializedEditorState = {} const plaintext = convertLexicalToPlaintext({ data }) ``` ### Custom Converters The `convertLexicalToPlaintext` functions accepts a `converters` object that allows you to customize how specific nodes are converted to plaintext. ```ts import type { DefaultNodeTypes, SerializedBlockNode, } from '@payloadcms/richtext-lexical' import type { SerializedEditorState } from '@payloadcms/richtext-lexical/lexical' import type { MyTextBlock } from '@/payload-types' import { convertLexicalToPlaintext, type PlaintextConverters, } from '@payloadcms/richtext-lexical/plaintext' // Your richtext data here const data: SerializedEditorState = {} const converters: PlaintextConverters< DefaultNodeTypes | SerializedBlockNode > = { blocks: { textBlock: ({ node }) => { return node.fields.text ?? '' }, }, link: ({ node }) => { return node.fields.url ?? '' }, } const plaintext = convertLexicalToPlaintext({ converters, data, }) ``` Unlike other converters, there are no default converters for plaintext. If a node does not have a converter defined, the following heuristics are used to convert it to plaintext: - If the node has a `text` field, it will be used as the plaintext. - If the node has a `children` field, the children will be recursively converted to plaintext. - If the node has neither, it will be ignored. - Paragraph, text and tab nodes insert newline / tab characters. # Official Features Source: https://payloadcms.com/docs/rich-text/official-features Below are all the Rich Text Features Payload offers. Everything is customizable; you can [create your own features](../rich-text/custom-features), modify ours and share them with the community. ## Features Overview | Feature Name | Included by default | Description | | ------------------------------- | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`BoldFeature`** | Yes | Adds support for bold text formatting. | | **`ItalicFeature`** | Yes | Adds support for italic text formatting. | | **`UnderlineFeature`** | Yes | Adds support for underlined text formatting. | | **`StrikethroughFeature`** | Yes | Adds support for strikethrough text formatting. | | **`SubscriptFeature`** | Yes | Adds support for subscript text formatting. | | **`SuperscriptFeature`** | Yes | Adds support for superscript text formatting. | | **`InlineCodeFeature`** | Yes | Adds support for inline code formatting. | | **`ParagraphFeature`** | Yes | Provides entries in both the slash menu and toolbar dropdown for explicit paragraph creation or conversion. | | **`HeadingFeature`** | Yes | Adds Heading Nodes (by default, H1 - H6, but that can be customized) | | **`AlignFeature`** | Yes | Adds support for text alignment (left, center, right, justify) | | **`IndentFeature`** | Yes | Adds support for text indentation with toolbar buttons | | **`UnorderedListFeature`** | Yes | Adds support for unordered lists (ul) | | **`OrderedListFeature`** | Yes | Adds support for ordered lists (ol) | | **`ChecklistFeature`** | Yes | Adds support for interactive checklists | | **`LinkFeature`** | Yes | Allows you to create internal and external links | | **`RelationshipFeature`** | Yes | Allows you to create block-level (not inline) relationships to other documents | | **`BlockquoteFeature`** | Yes | Allows you to create block-level quotes | | **`UploadFeature`** | Yes | Allows you to create block-level upload nodes - this supports all kinds of uploads, not just images | | **`HorizontalRuleFeature`** | Yes | Adds support for horizontal rules / separators. Basically displays an `
` element | | **`InlineToolbarFeature`** | Yes | Provides a floating toolbar which appears when you select text. This toolbar only contains actions relevant for selected text | | **`FixedToolbarFeature`** | No | Provides a persistent toolbar pinned to the top and always visible. Both inline and fixed toolbars can be enabled at the same time. | | **`BlocksFeature`** | No | Allows you to use Payload's [Blocks Field](../fields/blocks) directly inside your editor. In the feature props, you can specify the allowed blocks - just like in the Blocks field. | | **`TreeViewFeature`** | No | Provides a debug box under the editor, which allows you to see the current editor state live, the dom, as well as time travel. Very useful for debugging | | **`EXPERIMENTAL_TableFeature`** | No | Adds support for tables. This feature may be removed or receive breaking changes in the future - even within a stable lexical release, without needing a major release. | | **`TextStateFeature`** | No | Allows you to store key-value attributes within TextNodes and assign them inline styles. | ## In depth ### BoldFeature - Description: Adds support for bold text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Markdown Support: `**bold**` or `__bold__` - Keyboard Shortcut: Ctrl/Cmd + B ### ItalicFeature - Description: Adds support for italic text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Markdown Support: `*italic*` or `_italic_` - Keyboard Shortcut: Ctrl/Cmd + I ### UnderlineFeature - Description: Adds support for underlined text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Keyboard Shortcut: Ctrl/Cmd + U ### StrikethroughFeature - Description: Adds support for strikethrough text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Markdown Support: `~~strikethrough~~` ### SubscriptFeature - Description: Adds support for subscript text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes ### SuperscriptFeature - Description: Adds support for superscript text formatting, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes ### InlineCodeFeature - Description: Adds support for inline code formatting with distinct styling, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Markdown Support: \`code\` ### ParagraphFeature - Description: Provides entries in both the slash menu and toolbar dropdown for explicit paragraph creation or conversion. - Included by default: Yes ### HeadingFeature - Description: Adds support for heading nodes (H1-H6) with toolbar dropdown and slash menu entries for each enabled heading size. - Included by default: Yes - Markdown Support: `#`, `##`, `###`, ..., at start of line. - Types: ```ts type HeadingFeatureProps = { enabledHeadingSizes?: HeadingTagType[] // ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] } ``` - Usage example: ```ts HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3'], // Default: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] }) ``` ### AlignFeature - Description: Allows text alignment (left, center, right, justify), along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Keyboard Shortcut: Ctrl/Cmd + Shift + L/E/R/J (left/center/right/justify) ### IndentFeature - Description: Adds support for text indentation, along with buttons to apply it in both fixed and inline toolbars. - Included by default: Yes - Keyboard Shortcut: Tab (increase), Shift + Tab (decrease) - Types: ```ts type IndentFeatureProps = { /** * The nodes that should not be indented. "type" * property of the nodes you don't want to be indented. * These can be: "paragraph", "heading", "listitem", * "quote" or other indentable nodes if they exist. */ disabledNodes?: string[] /** * If true, pressing Tab in the middle of a block such * as a paragraph or heading will not insert a tabNode. * Instead, Tab will only be used for block-level indentation. * @default false */ disableTabNode?: boolean } ``` - Usage example: ```ts // Allow block-level indentation only IndentFeature({ disableTabNode: true, }) ``` ### UnorderedListFeature - Description: Adds support for unordered lists (bullet points) with toolbar dropdown and slash menu entries. - Included by default: Yes - Markdown Support: `-`, `*`, or `+` at start of line ### OrderedListFeature - Description: Adds support for ordered lists (numbered lists) with toolbar dropdown and slash menu entries. - Included by default: Yes - Markdown Support: `1.` at start of line ### ChecklistFeature - Description: Adds support for interactive checklists with toolbar dropdown and slash menu entries. - Included by default: Yes - Markdown Support: `- [ ]` (unchecked) or `- [x]` (checked) ### LinkFeature - Description: Allows creation of internal and external links with toolbar buttons and automatic URL conversion. - Included by default: Yes - Markdown Support: `[anchor](url)` - Types: ```ts type LinkFeatureServerProps = { /** * Disables the automatic creation of links * from URLs typed or pasted into the editor, * @default false */ disableAutoLinks?: 'creationOnly' | true /** * A function or array defining additional * fields for the link feature. * These will be displayed in the link editor drawer. */ fields?: | ((args: { config: SanitizedConfig defaultFields: FieldAffectingData[] }) => (Field | FieldAffectingData)[]) | Field[] /** * Sets a maximum population depth for the internal * doc default field of link, regardless of the * remaining depth when the field is reached. */ maxDepth?: number } & ExclusiveLinkCollectionsProps type ExclusiveLinkCollectionsProps = | { disabledCollections?: CollectionSlug[] enabledCollections?: never } | { disabledCollections?: never enabledCollections?: CollectionSlug[] } ``` - Usage example: ```ts LinkFeature({ fields: ({ defaultFields }) => [ ...defaultFields, { name: 'rel', type: 'select', options: ['noopener', 'noreferrer', 'nofollow'], }, ], enabledCollections: ['pages', 'posts'], // Collections for internal links maxDepth: 2, // Population depth for internal links disableAutoLinks: false, // Allow auto-conversion of URLs }) ``` ### RelationshipFeature - Description: Allows creation of block-level relationships to other documents with toolbar button and slash menu entry. - Included by default: Yes - Types: ```ts type RelationshipFeatureProps = { /** * Sets a maximum population depth for this relationship, * regardless of the remaining depth when the respective * field is reached. */ maxDepth?: number } & ExclusiveRelationshipFeatureProps type ExclusiveRelationshipFeatureProps = | { disabledCollections?: CollectionSlug[] enabledCollections?: never } | { disabledCollections?: never enabledCollections?: CollectionSlug[] } ``` - Usage example: ```ts RelationshipFeature({ disabledCollections: ['users'], // Collections to exclude maxDepth: 2, // Population depth for relationships }) ``` ### UploadFeature - Description: Allows creation of upload/media nodes with toolbar button and slash menu entry, supports all file types. - Included by default: Yes - Types: ```ts type UploadFeatureProps = { collections?: { [collection: UploadCollectionSlug]: { fields: Field[] } } /** * Sets a maximum population depth for this upload (not the fields for this upload), regardless of the remaining depth when the respective field is reached. * This behaves exactly like the maxDepth properties of relationship and upload fields. * * {@link ../getting-started/concepts#field-level-max-depth} */ maxDepth?: number } & ExclusiveUploadFeatureProps type ExclusiveUploadFeatureProps = | { /** * The collections that should be disabled. Overrides the `enableRichTextRelationship` property in the collection config. * When this property is set, `enabledCollections` will not be available. **/ disabledCollections?: UploadCollectionSlug[] // Ensures that enabledCollections is not available when disabledCollections is set enabledCollections?: never } | { // Ensures that disabledCollections is not available when enabledCollections is set disabledCollections?: never /** * The collections that should be enabled. Overrides the `enableRichTextRelationship` property in the collection config * When this property is set, `disabledCollections` will not be available. **/ enabledCollections?: UploadCollectionSlug[] } ``` - Usage example: ```ts UploadFeature({ collections: { uploads: { fields: [ { name: 'caption', type: 'text', label: 'Caption', }, { name: 'alt', type: 'text', label: 'Alt Text', }, ], }, }, maxDepth: 1, // Population depth for uploads disabledCollections: ['specialUploads'], // Collections to exclude }) ``` ### BlockquoteFeature - Description: Allows creation of blockquotes with toolbar button and slash menu entry. - Included by default: Yes - Markdown Support: `> quote text` ### HorizontalRuleFeature - Description: Adds support for horizontal rules/separators with toolbar button and slash menu entry. - Included by default: Yes - Markdown Support: `---` ### InlineToolbarFeature - Description: Provides a floating toolbar that appears when text is selected, containing formatting options relevant to selected text. - Included by default: Yes ### FixedToolbarFeature - Description: Provides a persistent toolbar pinned to the top of the editor that's always visible. - Included by default: No - Types: ```ts type FixedToolbarFeatureProps = { /** * @default false * If this is enabled, the toolbar will apply * to the focused editor, not the editor with * the FixedToolbarFeature. */ applyToFocusedEditor?: boolean /** * Custom configurations for toolbar groups * Key is the group key (e.g. 'format', 'indent', 'align') * Value is a partial ToolbarGroup object that will * be merged with the default configuration */ customGroups?: CustomGroups /** * @default false * If there is a parent editor with a fixed toolbar, * this will disable the toolbar for this editor. */ disableIfParentHasFixedToolbar?: boolean } ``` - Usage example: ```ts FixedToolbarFeature({ applyToFocusedEditor: false, // Apply to focused editor customGroups: { format: { // Custom configuration for format group }, }, }) ``` ### BlocksFeature - Description: Allows use of Payload's Blocks Field directly in the editor with toolbar buttons and slash menu entries for each block type. - Included by default: No - Types: ```ts type BlocksFeatureProps = { blocks?: (Block | BlockSlug)[] | Block[] inlineBlocks?: (Block | BlockSlug)[] | Block[] } ``` - Usage example: ```ts BlocksFeature({ blocks: [ { slug: 'callout', fields: [ { name: 'text', type: 'text', required: true, }, ], }, ], inlineBlocks: [ { slug: 'mention', fields: [ { name: 'name', type: 'text', required: true, }, ], }, ], }) ``` #### Code Blocks Payload exports a premade CodeBlock that you can import and use in your project. It supports syntax highlighting, dynamically selecting the language and loading in external type definitions: ```ts import { BlocksFeature, CodeBlock } from '@payloadcms/richtext-lexical' // ... BlocksFeature({ blocks: [ CodeBlock({ defaultLanguage: 'ts', languages: { js: 'JavaScript', plaintext: 'Plain Text', ts: 'TypeScript', }, }), ], }), // ... ``` When using TypeScript, you can also pass in additional type definitions that will be available in the editor. Here's an example of how to make `payload` and `react` available in the editor: ```ts import { BlocksFeature, CodeBlock } from '@payloadcms/richtext-lexical' // ... BlocksFeature({ blocks: [ CodeBlock({ slug: 'PayloadCode', languages: { ts: 'TypeScript', }, typescript: { fetchTypes: [ { // The index.bundled.d.ts contains all the types for Payload in one file, so that Monaco doesn't need to fetch multiple files. // This file may be removed in the future and is not guaranteed to be available in future versions of Payload. url: 'https://unpkg.com/payload@3.59.0-internal.8435f3c/dist/index.bundled.d.ts', filePath: 'file:///node_modules/payload/index.d.ts', }, { url: 'https://unpkg.com/@types/react@19.1.17/index.d.ts', filePath: 'file:///node_modules/@types/react/index.d.ts', }, ], paths: { payload: ['file:///node_modules/payload/index.d.ts'], react: ['file:///node_modules/@types/react/index.d.ts'], }, typeRoots: ['node_modules/@types', 'node_modules/payload'], // Enable type checking. By default, only syntax checking is enabled. enableSemanticValidation: true, }, }), ], }), // ... ``` ### TreeViewFeature - Description: Provides a debug panel below the editor showing the editor's internal state, DOM tree, and time travel debugging. - Included by default: No ### EXPERIMENTAL_TableFeature - Description: Adds support for tables with toolbar button and slash menu entry for creation and editing. - Included by default: No ### TextStateFeature - Description: Allows storing key-value attributes in text nodes with inline styles and toolbar dropdown for style selection. - Included by default: No - Types: ```ts type TextStateFeatureProps = { /** * The keys of the top-level object (stateKeys) represent the attributes that the textNode can have (e.g., color). * The values of the top-level object (stateValues) represent the values that the attribute can have (e.g., red, blue, etc.). * Within the stateValue, you can define inline styles and labels. */ state: { [stateKey: string]: StateValues } } type StateValues = { [stateValue: string]: { css: StyleObject label: string } } type StyleObject = { [K in keyof PropertiesHyphenFallback]?: | Extract | undefined } ``` - Usage example: ```ts // We offer default colors that have good contrast and look good in dark and light mode. import { defaultColors, TextStateFeature } from '@payloadcms/richtext-lexical' TextStateFeature({ // prettier-ignore state: { color: { ...defaultColors, // fancy gradients! galaxy: { label: 'Galaxy', css: { background: 'linear-gradient(to right, #0000ff, #ff0000)', color: 'white' } }, sunset: { label: 'Sunset', css: { background: 'linear-gradient(to top, #ff5f6d, #6a3093)' } }, }, // You can have both colored and underlined text at the same time. // If you don't want that, you should group them within the same key. // (just like I did with defaultColors and my fancy gradients) underline: { 'solid': { label: 'Solid', css: { 'text-decoration': 'underline', 'text-underline-offset': '4px' } }, // You'll probably want to use the CSS light-dark() utility. 'yellow-dashed': { label: 'Yellow Dashed', css: { 'text-decoration': 'underline dashed', 'text-decoration-color': 'light-dark(#EAB308,yellow)', 'text-underline-offset': '4px' } }, }, }, }), ``` This is what the example above will look like: # Custom Features Source: https://payloadcms.com/docs/rich-text/custom-features Before you begin building custom features for Lexical, it is crucial to familiarize yourself with the [Lexical docs](https://lexical.dev/docs/intro), particularly the "Concepts" section. This foundation is necessary for understanding Lexical's core principles, such as nodes, editor state, and commands. Lexical features are designed to be modular, meaning each piece of functionality is encapsulated within just two specific interfaces: one for server-side code and one for client-side code. By convention, these are named `feature.server.ts` for server-side functionality and `feature.client.ts` for client-side functionality. The primary functionality is housed within `feature.server.ts`, which users will import into their projects. The client-side feature, although defined separately, is integrated and rendered server-side through the server feature. That way, we still maintain a clear boundary between server and client code, while also centralizing the code needed for a feature in basically one place. This approach is beneficial for managing all the bits and pieces which make up your feature as a whole, such as toolbar entries, buttons, or new nodes, allowing each feature to be neatly contained and managed independently. **Important:** Do not import directly from core lexical packages - this may break in minor Payload version bumps. Instead, import the re-exported versions from `@payloadcms/richtext-lexical`. For example, change `import { $insertNodeToNearestRoot } from '@lexical/utils'` to `import { $insertNodeToNearestRoot } from '@payloadcms/richtext-lexical/lexical/utils'` ## Do I need a custom feature? Before you start building a custom feature, consider whether you can achieve your desired functionality using the existing `BlocksFeature`. The `BlocksFeature` is a powerful feature that allows you to create custom blocks with a variety of options, including custom React components, markdown converters, and more. If you can achieve your desired functionality using the `BlocksFeature`, it is recommended to use it instead of building a custom feature. Using the BlocksFeature, you can add both inline blocks (= can be inserted into a paragraph, in between text) and block blocks (= take up the whole line) to the editor. If you simply want to bring custom react components into the editor, this is the way to go. ### Example: Code Field Block with language picker This example demonstrates how to create a custom code field block with a language picker using the `BlocksFeature`. First, make sure to explicitly install `@payloadcms/ui` in your project. Field Config: ```ts import { BlocksFeature, lexicalEditor, } from '@payloadcms/richtext-lexical' export const languages = { ts: 'TypeScript', plaintext: 'Plain Text', tsx: 'TSX', js: 'JavaScript', jsx: 'JSX', } // ... { name: 'richText', type: 'richText', editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, BlocksFeature({ blocks: [ { slug: 'Code', fields: [ { type: 'select', name: 'language', options: Object.entries(languages).map(([key, value]) => ({ label: value, value: key, })), defaultValue: 'ts', }, { admin: { components: { Field: './path/to/CodeComponent#Code', }, }, name: 'code', type: 'code', }, ], } ], inlineBlocks: [], }), ], }), }, ``` CodeComponent.tsx: ```tsx 'use client' import type { CodeFieldClient, CodeFieldClientProps } from 'payload' import { CodeField, useFormFields } from '@payloadcms/ui' import React, { useMemo } from 'react' import { languages } from './yourFieldConfig' const languageKeyToMonacoLanguageMap = { plaintext: 'plaintext', ts: 'typescript', tsx: 'typescript', } type Language = keyof typeof languageKeyToMonacoLanguageMap export const Code: React.FC = ({ autoComplete, field, forceRender, path, permissions, readOnly, renderedBlocks, schemaPath, validate, }) => { const languageField = useFormFields(([fields]) => fields['language']) const language: Language = (languageField?.value as Language) || (languageField?.initialValue as Language) || 'ts' const label = languages[language] const props: CodeFieldClient = useMemo( () => ({ ...field, type: 'code', admin: { ...field.admin, editorOptions: undefined, language: languageKeyToMonacoLanguageMap[language] || language, }, label, }), [field, language, label], ) const key = `${field.name}-${language}-${label}` return ( ) } ``` ## Server Feature Custom Blocks are not enough? To start building a custom feature, you should start with the server feature, which is the entry-point. **Example myFeature/feature.server.ts:** ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' export const MyFeature = createServerFeature({ feature: {}, key: 'myFeature', }) ``` `createServerFeature` is a helper function which lets you create new features without boilerplate code. Now, the feature is ready to be used in the editor: ```ts import { MyFeature } from './myFeature/feature.server'; import { lexicalEditor } from '@payloadcms/richtext-lexical'; //... { name: 'richText', type: 'richText', editor: lexicalEditor({ features: [ MyFeature(), ], }), }, ``` By default, this server feature does nothing - you haven't added any functionality yet. Depending on what you want your feature to do, the ServerFeature type exposes various properties you can set to inject custom functionality into the lexical editor. ### i18n Each feature can register their own translations, which are automatically scoped to the feature key: ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' export const MyFeature = createServerFeature({ feature: { i18n: { en: { label: 'My Feature', }, de: { label: 'Mein Feature', }, }, }, key: 'myFeature', }) ``` This allows you to add i18n translations scoped to your feature. This specific example translation will be available under `lexical:myFeature:label` - `myFeature` being your feature key. ### Markdown Transformers#server-feature-markdown-transformers The Server Feature, just like the Client Feature, allows you to add markdown transformers. Markdown transformers on the server are used when [converting the editor from or to markdown](../rich-text/converting-markdown). ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' import type { ElementTransformer } from '@payloadcms/richtext-lexical/lexical/markdown' import { $createMyNode, $isMyNode, MyNode } from './nodes/MyNode' const MyMarkdownTransformer: ElementTransformer = { type: 'element', dependencies: [MyNode], export: (node, exportChildren) => { if (!$isMyNode(node)) { return null } return '+++' }, // match --- regExp: /^+++\s*$/, replace: (parentNode) => { const node = $createMyNode() if (node) { parentNode.replace(node) } }, } export const MyFeature = createServerFeature({ feature: { markdownTransformers: [MyMarkdownTransformer], }, key: 'myFeature', }) ``` In this example, the node will be outputted as `+++` in Markdown, and the markdown `+++` will be converted to a `MyNode` node in the editor. ### Nodes#server-feature-nodes While nodes added to the server feature do not control how the node is rendered in the editor, they control other aspects of the node: - HTML conversion - Node Hooks - Sub fields - Behavior in a headless editor The `createNode` helper function is used to create nodes with proper typing. It is recommended to use this function to create nodes. ```ts import { createServerFeature, createNode } from '@payloadcms/richtext-lexical' import { MyNode } from './nodes/MyNode' export const MyFeature = createServerFeature({ feature: { nodes: [ // Use the createNode helper function to more easily create nodes with proper typing createNode({ converters: { html: { converter: () => { return `
` }, nodeTypes: [MyNode.getType()], }, }, // Here you can add your actual node. On the server, they will be // used to initialize a headless editor which can be used to perform // operations on the editor, like markdown / html conversion. node: MyNode, }), ], }, key: 'myFeature', }) ``` While nodes in the client feature are added by themselves to the nodes array, nodes in the server feature can be added together with the following sibling options: | Option | Description | | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`getSubFields`** | If a node includes sub-fields (e.g. block and link nodes), passing the subFields schema here will make Payload automatically populate & run hooks for them. | | **`getSubFieldsData`** | If a node includes sub-fields, the sub-fields data needs to be returned here, alongside `getSubFields` which returns their schema. | | **`graphQLPopulationPromises`** | Allows you to run population logic when a node's data was requested from GraphQL. While `getSubFields` and `getSubFieldsData` automatically handle populating sub-fields (since they run hooks on them), those are only populated in the Rest API. This is because the Rest API hooks do not have access to the 'depth' property provided by GraphQL. In order for them to be populated correctly in GraphQL, the population logic needs to be provided here. | | **`node`** | The actual lexical node needs to be provided here. This also supports [lexical node replacements](https://lexical.dev/docs/concepts/node-replacement). | | **`validations`** | This allows you to provide node validations, which are run when your document is being validated, alongside other Payload fields. You can use it to throw a validation error for a specific node in case its data is incorrect. | | **`converters`** | Allows you to define how a node can be serialized into different formats. Currently, only supports HTML. Markdown converters are defined in `markdownTransformers` and not here. | | **`hooks`** | Just like Payload fields, you can provide hooks which are run for this specific node. These are called Node Hooks. | ### Feature load order Server features can also accept a function as the `feature` property (useful for sanitizing props, as mentioned below). This function will be called when the feature is loaded during the Payload sanitization process: ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' createServerFeature({ //... feature: async ({ config, isRoot, props, resolvedFeatures, unSanitizedEditorConfig, featureProviderMap, }) => { return { //Actual server feature here... } }, }) ``` "Loading" here means the process of calling this `feature` function. By default, features are called in the order in which they are added to the editor. However, sometimes you might want to load a feature after another feature has been loaded, or require a different feature to be loaded, throwing an error if this is not the case. Within lexical, one example where this is done are our list features. Both `UnorderedListFeature` and `OrderedListFeature` register the same `ListItem` node. Within `UnorderedListFeature` we register it normally, but within `OrderedListFeature` we want to only register the `ListItem` node if the `UnorderedListFeature` is not present - otherwise, we would have two features registering the same node. Here is how we do it: ```ts import { createServerFeature, createNode } from '@payloadcms/richtext-lexical' export const OrderedListFeature = createServerFeature({ feature: ({ featureProviderMap }) => { return { // ... nodes: featureProviderMap.has('unorderedList') ? [] : [ createNode({ // ... }), ], } }, key: 'orderedList', }) ``` `featureProviderMap` will always be available and contain all the features, even yet-to-be-loaded ones, so we can check if a feature is loaded by checking if its `key` present in the map. If you wanted to make sure a feature is loaded before another feature, you can use the `dependenciesPriority` property: ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' export const MyFeature = createServerFeature({ feature: ({ featureProviderMap }) => { return { // ... } }, key: 'myFeature', dependenciesPriority: ['otherFeature'], }) ``` | Option | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`dependenciesSoft`** | Keys of soft-dependencies needed for this feature. These are optional. Payload will attempt to load them before this feature, but doesn't throw an error if that's not possible. | | **`dependencies`** | Keys of dependencies needed for this feature. These dependencies do not have to be loaded first, but they have to exist, otherwise an error will be thrown. | | **`dependenciesPriority`** | Keys of priority dependencies needed for this feature. These dependencies have to be loaded first AND have to exist, otherwise an error will be thrown. They will be available in the `feature` property. | ## Client Feature Most of the functionality which the user actually sees and interacts with, like toolbar items and React components for nodes, resides on the client-side. To set up your client-side feature, follow these three steps: 1. **Create a Separate File**: Start by creating a new file specifically for your client feature, such as `myFeature/feature.client.ts`. It's important to keep client and server features in separate files to maintain a clean boundary between server and client code. 2. **'use client'**: Mark that file with a 'use client' directive at the top of the file 3. **Register the Client Feature**: Register the client feature within your server feature, by passing it to the `ClientFeature` prop. This is needed because the server feature is the sole entry-point of your feature. This also means you are not able to create a client feature without a server feature, as you will not be able to register it otherwise. **Example myFeature/feature.client.ts:** ```ts 'use client' import { createClientFeature } from '@payloadcms/richtext-lexical/client' export const MyClientFeature = createClientFeature({}) ``` Explore the APIs available through ClientFeature to add the specific functionality you need. Remember, do not import directly from `'@payloadcms/richtext-lexical'` when working on the client-side, as it will cause errors with webpack or turbopack. Instead, use `'@payloadcms/richtext-lexical/client'` for all client-side imports. Type-imports are excluded from this rule and can always be imported. ### Adding a client feature to the server feature Inside of your server feature, you can provide an [import path](../custom-components/overview#component-paths) to the client feature like this: ```ts import { createServerFeature } from '@payloadcms/richtext-lexical' export const MyFeature = createServerFeature({ feature: { ClientFeature: './path/to/feature.client#MyClientFeature', }, key: 'myFeature', dependenciesPriority: ['otherFeature'], }) ``` ### Nodes#client-feature-nodes Add nodes to the `nodes` array in **both** your client & server feature. On the server side, nodes are utilized for backend operations like HTML conversion in a headless editor. On the client side, these nodes are integral to how content is displayed and managed in the editor, influencing how they are rendered, behave, and saved in the database. Example: **myFeature/feature.client.ts:** ```ts 'use client' import { createClientFeature } from '@payloadcms/richtext-lexical/client' import { MyNode } from './nodes/MyNode' export const MyClientFeature = createClientFeature({ nodes: [MyNode], }) ``` This also supports [lexical node replacements](https://lexical.dev/docs/concepts/node-replacement). **myFeature/nodes/MyNode.tsx:** Here is a basic DecoratorNode example: ```ts import type { DOMConversionMap, DOMConversionOutput, DOMExportOutput, EditorConfig, LexicalNode, SerializedLexicalNode, } from '@payloadcms/richtext-lexical/lexical' import { $applyNodeReplacement, DecoratorNode } from '@payloadcms/richtext-lexical/lexical' // SerializedLexicalNode is the default lexical node. // By setting your SerializedMyNode type to SerializedLexicalNode, // you are basically saying that this node does not save any additional data. // If you want your node to save data, feel free to extend it export type SerializedMyNode = SerializedLexicalNode // Lazy-import the React component to your node here const MyNodeComponent = React.lazy(() => import('../component/index.js').then((module) => ({ default: module.MyNodeComponent, })), ) /** * This node is a DecoratorNode. DecoratorNodes allow * you to render React components in the editor. * * They need both createDom and decorate functions. * createDom => outside of the html. * decorate => React Component inside of the html. * * If we used DecoratorBlockNode instead, * we would only need a decorate method */ export class MyNode extends DecoratorNode { static clone(node: MyNode): MyNode { return new MyNode(node.__key) } static getType(): string { return 'myNode' } /** * Defines what happens if you copy a div element * from another page and paste it into the lexical editor * * This also determines the behavior of lexical's * internal HTML -> Lexical converter */ static importDOM(): DOMConversionMap | null { return { div: () => ({ conversion: $yourConversionMethod, priority: 0, }), } } /** * The data for this node is stored serialized as JSON. * This is the "load function" of that node: it takes * the saved data and converts it into a node. */ static importJSON(serializedNode: SerializedMyNode): MyNode { return $createMyNode() } /** * Determines how the hr element is rendered in the * lexical editor. This is only the "initial" / "outer" * HTML element. */ createDOM(config: EditorConfig): HTMLElement { const element = document.createElement('div') return element } /** * Allows you to render a React component within * whatever createDOM returns. */ decorate(): React.ReactElement { return } /** * Opposite of importDOM, this function defines what * happens when you copy a div element from the lexical * editor and paste it into another page. * * This also determines the behavior of lexical's * internal Lexical -> HTML converter */ exportDOM(): DOMExportOutput { return { element: document.createElement('div') } } /** * Opposite of importJSON. This determines what * data is saved in the database / in the lexical * editor state. */ exportJSON(): SerializedLexicalNode { return { type: 'myNode', version: 1, } } getTextContent(): string { return '\n' } isInline(): false { return false } updateDOM(): boolean { return false } } // This is used in the importDOM method. Totally optional // if you do not want your node to be created automatically // when copy & pasting certain dom elements into your editor. function $yourConversionMethod(): DOMConversionOutput { return { node: $createMyNode() } } // This is a utility method to create a new MyNode. // Utility methods prefixed with $ make it explicit // that this should only be used within lexical export function $createMyNode(): MyNode { return $applyNodeReplacement(new MyNode()) } // This is just a utility method you can use // to check if a node is a MyNode. This also // ensures correct typing. export function $isMyNode( node: LexicalNode | null | undefined, ): node is MyNode { return node instanceof MyNode } ``` Please do not add any 'use client' directives to your nodes, as the node class can be used on the server. ### Plugins One small part of a feature are plugins. The name stems from the lexical playground plugins and is just a small part of a lexical feature. Plugins are simply React components which are added to the editor, within all the lexical context providers. They can be used to add any functionality to the editor, by utilizing the lexical API. Most commonly, they are used to register [lexical listeners](https://lexical.dev/docs/concepts/listeners), [node transforms](https://lexical.dev/docs/concepts/transforms) or [commands](https://lexical.dev/docs/concepts/commands). For example, you could add a drawer to your plugin and register a command which opens it. That command can then be called from anywhere within lexical, e.g. from within your custom lexical node. To add a plugin, simply add it to the `plugins` array in your client feature: ```ts 'use client' import { createClientFeature } from '@payloadcms/richtext-lexical/client' import { MyPlugin } from './plugin' export const MyClientFeature = createClientFeature({ plugins: [MyPlugin], }) ``` Example plugin.tsx: ```ts 'use client' import type { LexicalCommand } from '@payloadcms/richtext-lexical/lexical' import { createCommand, $getSelection, $isRangeSelection, COMMAND_PRIORITY_EDITOR, } from '@payloadcms/richtext-lexical/lexical' import { useLexicalComposerContext } from '@payloadcms/richtext-lexical/lexical/react/LexicalComposerContext' import { $insertNodeToNearestRoot } from '@payloadcms/richtext-lexical/lexical/utils' import { useEffect } from 'react' import type { PluginComponent } from '@payloadcms/richtext-lexical' // type imports can be imported from @payloadcms/richtext-lexical - even on the client import { $createMyNode } from '../nodes/MyNode' import './index.scss' export const INSERT_MYNODE_COMMAND: LexicalCommand = createCommand( 'INSERT_MYNODE_COMMAND', ) /** * Plugin which registers a lexical command to * insert a new MyNode into the editor */ export const MyNodePlugin: PluginComponent = () => { // The useLexicalComposerContext hook can be used // to access the lexical editor instance const [editor] = useLexicalComposerContext() useEffect(() => { return editor.registerCommand( INSERT_MYNODE_COMMAND, (type) => { const selection = $getSelection() if (!$isRangeSelection(selection)) { return false } const focusNode = selection.focus.getNode() if (focusNode !== null) { const newMyNode = $createMyNode() $insertNodeToNearestRoot(newMyNode) } return true }, COMMAND_PRIORITY_EDITOR, ) }, [editor]) return null } ``` In this example, we register a lexical command, which simply inserts a new MyNode into the editor. This command can be called from anywhere within lexical, e.g. from within a custom node. ### Toolbar groups Toolbar groups are visual containers which hold toolbar items. There are different toolbar group types which determine _how_ a toolbar item is displayed: `dropdown` and `buttons`. All the default toolbar groups are exported from `@payloadcms/richtext-lexical/client`. You can use them to add your own toolbar items to the editor: - Dropdown: `toolbarAddDropdownGroupWithItems` - Dropdown: `toolbarTextDropdownGroupWithItems` - Buttons: `toolbarFormatGroupWithItems` - Buttons: `toolbarFeatureButtonsGroupWithItems` Within dropdown groups, items are positioned vertically when the dropdown is opened and include the icon & label. Within button groups, items are positioned horizontally and only include the icon. If a toolbar group with the same key is declared twice, all its items will be merged into one group. #### Custom buttons toolbar group | Option | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`items`** | All toolbar items part of this toolbar group need to be added here. | | **`key`** | Each toolbar group needs to have a unique key. Groups with the same keys will have their items merged together. | | **`order`** | Determines where the toolbar group will be. | | **`type`** | Controls the toolbar group type. Set to `buttons` to create a buttons toolbar group, which displays toolbar items horizontally using only their icons. | Example: ```ts import type { ToolbarGroup, ToolbarGroupItem, } from '@payloadcms/richtext-lexical' export const toolbarFormatGroupWithItems = ( items: ToolbarGroupItem[], ): ToolbarGroup => { return { type: 'buttons', items, key: 'myButtonsToolbar', order: 10, } } ``` #### Custom dropdown toolbar group | Option | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`items`** | All toolbar items part of this toolbar group need to be added here. | | **`key`** | Each toolbar group needs to have a unique key. Groups with the same keys will have their items merged together. | | **`order`** | Determines where the toolbar group will be. | | **`type`** | Controls the toolbar group type. Set to `dropdown` to create a buttons toolbar group, which displays toolbar items vertically using their icons and labels, if the dropdown is open. | | **`ChildComponent`** | The dropdown toolbar ChildComponent allows you to pass in a React Component which will be displayed within the dropdown button. | Example: ```ts import type { ToolbarGroup, ToolbarGroupItem, } from '@payloadcms/richtext-lexical' import { MyIcon } from './icons/MyIcon' export const toolbarAddDropdownGroupWithItems = ( items: ToolbarGroupItem[], ): ToolbarGroup => { return { type: 'dropdown', ChildComponent: MyIcon, items, key: 'myDropdownToolbar', order: 10, } } ``` ### Toolbar items Custom nodes and features on its own are pointless, if they can't be added to the editor. You will need to hook in one of our interfaces which allow the user to interact with the editor: - Fixed toolbar which stays fixed at the top of the editor - Inline, floating toolbar which appears when selecting text - Slash menu which appears when typing `/` in the editor - Markdown transformers, which are triggered when a certain text pattern is typed in the editor - Or any other interfaces which can be added via your own plugins. Our toolbars are a prime example of this - they are just plugins. To add a toolbar item to either the floating or the inline toolbar, you can add a ToolbarGroup with a ToolbarItem to the `toolbarFixed` or `toolbarInline` props of your client feature: ```ts 'use client' import { createClientFeature, toolbarAddDropdownGroupWithItems, } from '@payloadcms/richtext-lexical/client' import { IconComponent } from './icon' import { $isHorizontalRuleNode } from './nodes/MyNode' import { INSERT_MYNODE_COMMAND } from './plugin' import { $isNodeSelection } from '@payloadcms/richtext-lexical/lexical' export const MyClientFeature = createClientFeature({ toolbarFixed: { groups: [ toolbarAddDropdownGroupWithItems([ { ChildComponent: IconComponent, isActive: ({ selection }) => { if (!$isNodeSelection(selection) || !selection.getNodes().length) { return false } const firstNode = selection.getNodes()[0] return $isHorizontalRuleNode(firstNode) }, key: 'myNode', label: ({ i18n }) => { return i18n.t('lexical:myFeature:label') }, onSelect: ({ editor }) => { editor.dispatchCommand(INSERT_MYNODE_COMMAND, undefined) }, }, ]), ], }, }) ``` You will have to provide a toolbar group first, and then the items for that toolbar group (more on that above). A `ToolbarItem` various props you can use to customize its behavior: | Option | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`ChildComponent`** | A React component which is rendered within your toolbar item's default button component. Usually, you want this to be an icon. | | **`Component`** | A React component which is rendered in place of the toolbar item's default button component, thus completely replacing it. The `ChildComponent` and `onSelect` properties will be ignored. | | **`label`** | The label will be displayed in your toolbar item, if it's within a dropdown group. To make use of i18n, this can be a function. | | **`key`** | Each toolbar item needs to have a unique key. | | **`onSelect`** | A function which is called when the toolbar item is clicked. | | **`isEnabled`** | This is optional and controls if the toolbar item is clickable or not. If `false` is returned here, it will be grayed out and unclickable. | | **`isActive`** | This is optional and controls if the toolbar item is highlighted or not | The API for adding an item to the floating inline toolbar (`toolbarInline`) is identical. If you wanted to add an item to both the fixed and inline toolbar, you can extract it into its own variable (typed as `ToolbarGroup[]`) and add it to both the `toolbarFixed` and `toolbarInline` props. ### Slash Menu groups We're exporting `slashMenuBasicGroupWithItems` from `@payloadcms/richtext-lexical/client` which you can use to add items to the slash menu labelled "Basic". If you want to create your own slash menu group, here is an example: ```ts import type { SlashMenuGroup, SlashMenuItem, } from '@payloadcms/richtext-lexical' export function mwnSlashMenuGroupWithItems( items: SlashMenuItem[], ): SlashMenuGroup { return { items, key: 'myGroup', label: 'My Group', // <= This can be a function to make use of i18n } } ``` By creating a helper function like this, you can easily re-use it and add items to it. All Slash Menu groups with the same keys will have their items merged together. | Option | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------- | | **`items`** | An array of `SlashMenuItem`'s which will be displayed in the slash menu. | | **`label`** | The label will be displayed before your Slash Menu group. In order to make use of i18n, this can be a function. | | **`key`** | Used for class names and, if label is not provided, for display. Slash menus with the same key will have their items merged together. | ### Slash Menu items The API for adding items to the slash menu is similar. There are slash menu groups, and each slash menu groups has items. Here is an example: ```ts 'use client' import { createClientFeature, slashMenuBasicGroupWithItems, } from '@payloadcms/richtext-lexical/client' import { INSERT_MYNODE_COMMAND } from './plugin' import { IconComponent } from './icon' export const MyClientFeature = createClientFeature({ slashMenu: { groups: [ slashMenuBasicGroupWithItems([ { Icon: IconComponent, key: 'myNode', keywords: ['myNode', 'myFeature', 'someOtherKeyword'], label: ({ i18n }) => { return i18n.t('lexical:myFeature:label') }, onSelect: ({ editor }) => { editor.dispatchCommand(INSERT_MYNODE_COMMAND, undefined) }, }, ]), ], }, }) ``` | Option | Description | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`Icon`** | The icon which is rendered in your slash menu item. | | **`label`** | The label will be displayed in your slash menu item. In order to make use of i18n, this can be a function. | | **`key`** | Each slash menu item needs to have a unique key. The key will be matched when typing, displayed if no `label` property is set, and used for classNames. | | **`onSelect`** | A function which is called when the slash menu item is selected. | | **`keywords`** | Keywords are used to match the item for different texts typed after the '/'. E.g. you might want to show a horizontal rule item if you type both /hr, /separator, /horizontal etc. In addition to the keywords, the label and key will be used to find the right slash menu item. | ### Markdown Transformers#client-feature-markdown-transformers The Client Feature, just like the Server Feature, allows you to add markdown transformers. Markdown transformers on the client are used to create new nodes when a certain markdown pattern is typed in the editor. ```ts import { createClientFeature } from '@payloadcms/richtext-lexical/client' import type { ElementTransformer } from '@payloadcms/richtext-lexical/lexical/markdown' import { $createMyNode, $isMyNode, MyNode } from './nodes/MyNode' const MyMarkdownTransformer: ElementTransformer = { type: 'element', dependencies: [MyNode], export: (node, exportChildren) => { if (!$isMyNode(node)) { return null } return '+++' }, // match --- regExp: /^+++\s*$/, replace: (parentNode) => { const node = $createMyNode() if (node) { parentNode.replace(node) } }, } export const MyFeature = createClientFeature({ markdownTransformers: [MyMarkdownTransformer], }) ``` In this example, a new `MyNode` will be inserted into the editor when `+++ ` is typed. ### Providers You can add providers to your client feature, which will be nested below the `EditorConfigProvider`. This can be useful if you want to provide some context to your nodes or other parts of your feature. ```ts 'use client' import { createClientFeature } from '@payloadcms/richtext-lexical/client' import { TableContext } from './context' export const MyClientFeature = createClientFeature({ providers: [TableContext], }) ``` ## Props To accept props in your feature, type them as a generic. Server Feature: ```ts createServerFeature({ //... }) ``` Client Feature: ```ts createClientFeature({ //... }) ``` The unSanitized props are what the user will pass to the feature when they call its provider function and add it to their editor config. You then have an option to sanitize those props. To sanitize those in the server feature, you can pass a function to `feature` instead of an object: ```ts createServerFeature({ //... feature: async ({ config, isRoot, props, resolvedFeatures, unSanitizedEditorConfig, featureProviderMap, }) => { const sanitizedProps = doSomethingWithProps(props) return { sanitizedServerFeatureProps: sanitizedProps, //Actual server feature here... } }, }) ``` Keep in mind that any sanitized props then have to be returned in the `sanitizedServerFeatureProps` property. In the client feature, it works similarly: ```ts createClientFeature( ({ clientFunctions, featureProviderMap, props, resolvedFeatures, unSanitizedEditorConfig, }) => { const sanitizedProps = doSomethingWithProps(props) return { sanitizedClientFeatureProps: sanitizedProps, //Actual client feature here... } }, ) ``` ### Bringing props from the server to the client By default, the client feature will never receive any props from the server feature. In order to pass props from the server to the client, you can need to return those props in the server feature: ```ts type UnSanitizedClientProps = { test: string } createServerFeature({ //... feature: { clientFeatureProps: { test: 'myValue', }, }, }) ``` The reason the client feature does not have the same props available as the server by default is because all client props need to be serializable. You can totally accept things like functions or Maps as props in your server feature, but you will not be able to send those to the client. In the end, those props are sent from the server to the client over the network, so they need to be serializable. ## More information Have a look at the [features we've already built](https://github.com/payloadcms/payload/tree/main/packages/richtext-lexical/src/features) - understanding how they work will help you understand how to create your own. There is no difference between the features included by default and the ones you create yourself - since those features are all isolated from the "core", you have access to the same APIs, whether the feature is part of Payload or not! # Rendering On Demand Source: https://payloadcms.com/docs/rich-text/rendering-on-demand Lexical in Payload is a **React Server Component (RSC)**. Historically that created three headaches: 1. You couldn't render the editor directly from the client. 2. Features like blocks, tables and link drawers require the server to know the shape of nested sub-fields at render time. If you tried to render on demand, the server didn't know those schemas. 3. The rich text field is designed to live inside a `Form`. For simple use cases, setting up a full form just to manage editor state was cumbersome. To simplify rendering richtext on demand, , that renders a Lexical editor while still covering the full feature set. On mount, it calls a server action to render the editor on the server using the new `render-field` server function. That server render gives Lexical everything it needs (including nested field schemas) and returns a ready-to-hydrate editor. `RenderLexical` and the underlying `render-field` server function are experimental and may change in minor releases. ## Inside an existing Form If you have an existing Form and want to render a richtext field within it, you can use the `RenderLexical` component like this: ```tsx 'use client' import type { JSONFieldClientComponent } from 'payload' import { buildEditorState, RenderLexical, } from '@payloadcms/richtext-lexical/client' import { lexicalFullyFeaturedSlug } from '../../slugs.js' export const Component: JSONFieldClientComponent = (args) => { return ( ({ text: 'default value', })} schemaPath={`collection.${lexicalFullyFeaturedSlug}.richText`} /> ) } ``` ## Outside of a Form (you control state) ```tsx 'use client' import type { DefaultTypedEditorState } from '@payloadcms/richtext-lexical' import type { JSONFieldClientComponent } from 'payload' import { buildEditorState, RenderLexical, } from '@payloadcms/richtext-lexical/client' import React, { useState } from 'react' import { lexicalFullyFeaturedSlug } from '../../slugs.js' export const Component: JSONFieldClientComponent = (args) => { // Manually manage the editor state const [value, setValue] = useState(() => buildEditorState({ text: 'state default' }), ) const handleReset = React.useCallback(() => { setValue(buildEditorState({ text: 'state default' })) }, []) return (
({ text: 'default value', })} schemaPath={`collection.${lexicalFullyFeaturedSlug}.richText`} setValue={setValue as any} value={value} />
) } ``` ## Choosing the schemaPath `schemaPath` tells the server which richText field to render. This gives the server the exact nested field schemas (blocks, relationship drawers, upload fields, tables, etc.). Format: - `collection..` - `global..` Example (top level): `collection.posts.richText` Example (nested in a group/tab): `collection.posts.content.richText` **Tip:** If your target editor lives deep in arrays/blocks and you're unsure of the exact path, you can define a **hidden top-level richText** purely as a "render anchor": ```ts { name: 'onDemandAnchor', type: 'richText', admin: { hidden: true } } ``` Then use `schemaPath="collection.posts.onDemandAnchor"` # Lexical Migration Source: https://payloadcms.com/docs/rich-text/migration ## Migrating from Slate While both Slate and Lexical save the editor state in JSON, the structure of the JSON is different. ### Migration via Migration Script (Recommended) Just import the `migrateSlateToLexical` function we provide, pass it the `payload` object and run it. Depending on the amount of collections, this might take a while. IMPORTANT: This will overwrite all slate data. We recommend doing the following first: 1. Take a backup of your entire database. If anything goes wrong and you do not have a backup, you are on your own and will not receive any support. 2. Make every richText field a lexical editor. This script will only convert lexical richText fields with old Slate data. 3. Add the SlateToLexicalFeature (as seen below) first, and test it out by loading up the Admin Panel, to see if the migrator works as expected. You might have to build some custom converters for some fields first in order to convert custom Slate nodes. The SlateToLexicalFeature is where the converters are stored. Only fields with this feature added will be migrated. 4. If this works as expected, add the `disableHooks: true` prop everywhere you're initializing `SlateToLexicalFeature`. Example: `SlateToLexicalFeature({ disableHooks: true })`. Once you did that, you're ready to run the migration script. ```ts import { migrateSlateToLexical } from '@payloadcms/richtext-lexical/migrate' await migrateSlateToLexical({ payload }) ``` ### Migration via SlateToLexicalFeature One way to handle this is to just give your lexical editor the ability to read the slate JSON. Simply add the `SlateToLexicalFeature` to your editor: ```ts import type { CollectionConfig } from 'payload' import { SlateToLexicalFeature } from '@payloadcms/richtext-lexical/migrate' import { lexicalEditor } from '@payloadcms/richtext-lexical' const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'nameOfYourRichTextField', type: 'richText', editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, SlateToLexicalFeature({}), ], }), }, ], } ``` and done! Now, every time this lexical editor is initialized, it converts the slate date to lexical on-the-fly. If the data is already in lexical format, it will just pass it through. This is by far the easiest way to migrate from Slate to Lexical, although it does come with a few caveats: - There is a performance hit when initializing the lexical editor - The editor will still output the Slate data in the output JSON, as the on-the-fly converter only runs for the Admin Panel The easy way to solve this: Edit the richText field and save the document! This overrides the slate data with the lexical data, and the next time the document is loaded, the lexical data will be used. This solves both the performance and the output issue for that specific document. This, however, is a slow and gradual migration process, thus you will have to support both API formats. Especially for a large number of documents, we recommend running the migration script, as explained above. ### Converting custom Slate nodes If you have custom Slate nodes, create a custom converter for them. Here's the Upload converter as an example: ```ts import type { SerializedUploadNode } from '../uploadNode' import type { SlateNodeConverter } from '@payloadcms/richtext-lexical' export const SlateUploadConverter: SlateNodeConverter = { converter({ slateNode }) { return { fields: { ...slateNode.fields, }, format: '', relationTo: slateNode.relationTo, type: 'upload', value: { id: slateNode.value?.id || '', }, version: 1, } as const as SerializedUploadNode }, nodeTypes: ['upload'], } ``` It's pretty simple: You get a Slate node as input, and you return the lexical node. The `nodeTypes` array is used to determine which Slate nodes this converter can handle. When using a migration script, you can add your custom converters to the `converters` property of the `convertSlateToLexical` props, as seen in the example above. When using the `SlateToLexicalFeature`, you can add your custom converters to the `converters` property of the `SlateToLexicalFeature` props: ```ts import type { CollectionConfig } from 'payload' import { lexicalEditor } from '@payloadcms/richtext-lexical' import { SlateToLexicalFeature, defaultSlateConverters, } from '@payloadcms/richtext-lexical' import { YourCustomConverter } from '../converters/YourCustomConverter' const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'nameOfYourRichTextField', type: 'richText', editor: lexicalEditor({ features: ({ defaultFeatures }) => [ ...defaultFeatures, SlateToLexicalFeature({ converters: [...defaultSlateConverters, YourCustomConverter], }), ], }), }, ], } ``` ## Migrating from payload-plugin-lexical Migrating from [payload-plugin-lexical](https://github.com/AlessioGr/payload-plugin-lexical) works similar to migrating from Slate. Instead of a `SlateToLexicalFeature` there is a `LexicalPluginToLexicalFeature` you can use. And instead of `convertSlateToLexical` you can use `convertLexicalPluginToLexical`. ## Migrating lexical data from old version to new version Each lexical node has a `version` property which is saved in the database. Every time we make a breaking change to the node's data, we increment the version. This way, we can detect an old version and automatically convert old data to the new format once you open up the editor. The problem is, this migration only happens when you open the editor, modify the richText field (so that the field's `setValue` function is called) and save the document. Until you do that for all documents, some documents will still have the old data. To solve this, we export an `upgradeLexicalData` function which goes through every single document in your Payload app and re-saves it, if it has a lexical editor. This way, the data is automatically converted to the new format, and that automatic conversion gets applied to every single document in your app. IMPORTANT: Take a backup of your entire database. If anything goes wrong and you do not have a backup, you are on your own and will not receive any support. ```ts import { upgradeLexicalData } from '@payloadcms/richtext-lexical' await upgradeLexicalData({ payload }) ``` # Slate Editor Source: https://payloadcms.com/docs/rich-text/slate The [default Payload editor](../rich-text/overview) is currently based on Lexical. This documentation is about our old Slate-based editor. You can continue using it because it is still supported, or you can see the optional [migration guide](../rich-text/migration) to migrate from Slate to Lexical (recommended). To use the Slate editor, first you need to install it: ``` npm install --save @payloadcms/richtext-slate ``` After installation, you can pass it to your top-level Payload Config: ```ts import { buildConfig } from 'payload' import { slateEditor } from '@payloadcms/richtext-slate' export default buildConfig({ collections: [ // your collections here ], // Pass the Slate editor to the root config editor: slateEditor({}), }) ``` And here's an example for how to install the Slate editor on a field-by-field basis, while customizing its options: ```ts import type { CollectionConfig } from 'payload' import { slateEditor } from '@payloadcms/richtext-slate' export const Pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'content', type: 'richText', // Pass the Slate editor here and configure it accordingly editor: slateEditor({ admin: { elements: [ // customize elements allowed in Slate editor here ], leaves: [ // customize leaves allowed in Slate editor here ], }, }), }, ], } ``` ## Admin Options **`elements`** The `elements` property is used to specify which built-in or custom [SlateJS elements](https://docs.slatejs.org/concepts/02-nodes#element) should be made available to the field within the Admin Panel. The default `elements` available in Payload are: - `h1` - `h2` - `h3` - `h4` - `h5` - `h6` - `blockquote` - `link` - `ol` - `ul` - `li` - `textAlign` - `indent` - [`relationship`](#relationship-element) - [`upload`](#upload-element) - [`textAlign`](#text-align) **`leaves`** The `leaves` property specifies built-in or custom [SlateJS leaves](https://docs.slatejs.org/concepts/08-rendering#leaves) to be enabled within the Admin Panel. The default `leaves` available in Payload are: - `bold` - `code` - `italic` - `strikethrough` - `underline` **`link.fields`** This allows [fields](../fields/overview) to be saved as extra fields on a link inside the Rich Text Editor. When this is present, the fields will render inside a modal that can be opened by clicking the "edit" button on the link element. `link.fields` may either be an array of fields (in which case all fields defined in it will be appended below the default fields) or a function that accepts the default fields as only argument and returns an array defining the entirety of fields to be used (thus providing a mechanism of overriding the default fields). ![RichText link fields](https://payloadcms.com/images/docs/fields/richText/rte-link-fields-modal.jpg) _RichText link with custom fields_ **`upload.collections[collection-name].fields`** This allows [fields](../fields/overview) to be saved as meta data on an upload field inside the Rich Text Editor. When this is present, the fields will render inside a modal that can be opened by clicking the "edit" button on the upload element. ![RichText upload element](https://payloadcms.com/images/docs/fields/richText/rte-upload-element.jpg) _RichText field using the upload element_ ![RichText upload element modal](https://payloadcms.com/images/docs/fields/richText/rte-upload-fields-modal.jpg) _RichText upload element modal displaying fields from the config_ ### Relationship element The built-in `relationship` element is a powerful way to reference other Documents directly within your Rich Text editor. ### Upload element Similar to the `relationship` element, the `upload` element is a user-friendly way to reference [Upload-enabled collections](../upload/overview) with a UI specifically designed for media / image-based uploads. **Tip:** Collections are automatically allowed to be selected within the Rich Text relationship and upload elements by default. If you want to disable a collection from being able to be referenced in Rich Text fields, set the collection admin options of **enableRichTextLink** and **enableRichTextRelationship** to false. Relationship and Upload elements are populated dynamically into your Rich Text field' content. Within the REST and Local APIs, any present RichText `relationship` or `upload` elements will respect the `depth` option that you pass, and will be populated accordingly. In GraphQL, each `richText` field accepts an argument of `depth` for you to utilize. ### TextAlign element Text Alignment is not included by default and can be added to a Rich Text Editor by adding `textAlign` to the list of elements. TextAlign will alter the existing element to include a new `textAlign` field in the resulting JSON. This field can be used in combination with other elements and leaves to position content to the left, center or right. ### Specifying which elements and leaves to allow To specify which default elements or leaves should be allowed to be used for this field, define arrays that contain string names for each element or leaf you wish to enable. To specify a custom element or leaf, pass an object with all corresponding properties as outlined below. View the [example](#example) to reference how this all works. ### Building custom elements and leaves You can design and build your own Slate elements and leaves to extend the editor with your own functionality. To do so, first start by reading the [SlateJS documentation](https://docs.slatejs.org/) and looking at the [Slate examples](https://www.slatejs.org/examples/richtext) to familiarize yourself with the SlateJS editor as a whole. Once you're up to speed with the general concepts involved, you can pass in your own elements and leaves to your field's admin config. **Both custom elements and leaves are defined via the following config:** | Property | Description | | --------------- | ---------------------------------------------------------- | | **`name`** \* | The default name to be used as a `type` for this element. | | **`Button`** \* | A React component to be rendered in the Rich Text toolbar. | | **`plugins`** | An array of plugins to provide to the Rich Text editor. | | **`type`** | A type that overrides the default type used by `name` | Custom `Element`s also require the `Element` property set to a React component to be rendered as the `Element` within the rich text editor itself. Custom `Leaf` objects follow a similar pattern but require you to define the `Leaf` property instead. Specifying custom `Type`s let you extend your custom elements by adding additional fields to your JSON object. ### Example ```ts import type { CollectionConfig } from 'payload' import { slateEditor } from '@payloadcms/richtext-slate' export const ExampleCollection: CollectionConfig = { slug: 'example-collection', fields: [ { name: 'content', // required type: 'richText', // required defaultValue: [ { children: [{ text: 'Here is some default content for this field' }], }, ], required: true, editor: slateEditor({ admin: { elements: [ 'h2', 'h3', 'h4', 'link', 'blockquote', { name: 'cta', Button: CustomCallToActionButton, Element: CustomCallToActionElement, plugins: [ // any plugins that are required by this element go here ], }, ], leaves: [ 'bold', 'italic', { name: 'highlight', Button: CustomHighlightButton, Leaf: CustomHighlightLeaf, plugins: [ // any plugins that are required by this leaf go here ], }, ], link: { // Inject your own fields into the Link element fields: [ { name: 'rel', label: 'Rel Attribute', type: 'select', hasMany: true, options: ['noopener', 'noreferrer', 'nofollow'], }, ], }, upload: { collections: { media: { fields: [ // any fields that you would like to save // on an upload element in the `media` collection ], }, }, }, }, }), }, ], } ``` ### Generating HTML As the Rich Text field saves its content in a JSON format, you'll need to render it as HTML yourself. Here is an example for how to generate JSX / HTML from Rich Text content: ```ts import React, { Fragment } from "react"; import escapeHTML from "escape-html"; import { Text } from "slate"; const serialize = (children) => children.map((node, i) => { if (Text.isText(node)) { let text = ( ); if (node.bold) { text = {text}; } if (node.code) { text = {text}; } if (node.italic) { text = {text}; } // Handle other leaf types here... return {text}; } if (!node) { return null; } switch (node.type) { case "h1": return

{serialize(node.children)}

; // Iterate through all headings here... case "h6": return
{serialize(node.children)}
; case "blockquote": return
{serialize(node.children)}
; case "ul": return
    {serialize(node.children)}
; case "ol": return
    {serialize(node.children)}
; case "li": return
  • {serialize(node.children)}
  • ; case "link": return ( {serialize(node.children)} ); default: return

    {serialize(node.children)}

    ; } }); ``` **Note:** The above example is for how to render to JSX, although for plain HTML the pattern is similar. Just remove the JSX and return HTML strings instead! ### Built-in SlateJS Plugins Payload comes with a few built-in SlateJS plugins which can be extended to make developing your own elements and leaves a bit easier. #### `shouldBreakOutOnEnter` Payload's built-in heading elements all allow a "hard return" to "break out" of the currently active element. For example, if you hit `enter` while typing an `h1`, the `h1` will be "broken out of" and you'll be able to continue writing as the default paragraph element. If you want to utilize this functionality within your own custom elements, you can do so by adding a custom plugin to your `element` like the following "large body" element example: `customLargeBodyElement.js`: ```ts import Button from './Button' import Element from './Element' import withLargeBody from './plugin' export default { name: 'large-body', Button, Element, plugins: [ (incomingEditor) => { const editor = incomingEditor const { shouldBreakOutOnEnter } = editor editor.shouldBreakOutOnEnter = (element) => element.type === 'large-body' ? true : shouldBreakOutOnEnter(element) return editor }, ], } ``` Above, you can see that we are creating a custom SlateJS element with a name of `large-body`. This might render a slightly larger body copy on the frontend of your app(s). We pass it a name, button, and element—but additionally, we pass it a `plugins` array containing a single SlateJS plugin. The plugin itself extends Payload's built-in `shouldBreakOutOnEnter` Slate function to add its own element name to the list of elements that should "break out" when the `enter` key is pressed. ### TypeScript If you are building your own custom Rich Text elements or leaves, you may benefit from importing the following types: ```ts import type { RichTextCustomElement, RichTextCustomLeaf, } from '@payloadcms/richtext-slate' ``` # Live Preview Source: https://payloadcms.com/docs/live-preview/overview With Live Preview you can render your front-end application directly within the [Admin Panel](../admin/overview). As you type, your changes take effect in real-time. No need to save a draft or publish your changes. This works in both [Server-side](./server) as well as [Client-side](./client) environments. Live Preview works by rendering an iframe on the page that loads your front-end application. The Admin Panel communicates with your app through [`window.postMessage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage) events. These events are emitted every time a change is made to the Document. Your app then listens for these events and re-renders itself with the data it receives. To add Live Preview, use the `admin.livePreview` property in your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { // ... // highlight-start livePreview: { url: 'http://localhost:3000', collections: ['pages'], }, // highlight-end }, }) ``` **Reminder:** Alternatively, you can define the `admin.livePreview` property on individual [Collection Admin Configs](../configuration/collections#admin-options) and [Global Admin Configs](../configuration/globals#admin-options). Settings defined here will be merged into the top-level as overrides. ## Options Setting up Live Preview is easy. This can be done either globally through the [Root Admin Config](../admin/overview), or on individual [Collection Admin Configs](../configuration/collections#admin-options) and [Global Admin Configs](../configuration/globals#admin-options). Once configured, a new "Live Preview" button will appear at the top of enabled Documents. Toggling this button opens the preview window and loads your front-end application. The following options are available: | Path | Description | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | **`url`** | String, or function that returns a string, pointing to your front-end application. This value is used as the iframe `src`. [More details](#url). | | **`breakpoints`** | Array of breakpoints to be used as “device sizes” in the preview window. Each item appears as an option in the toolbar. [More details](#breakpoints). | | **`collections`** | Array of collection slugs to enable Live Preview on. | | **`globals`** | Array of global slugs to enable Live Preview on. | ### URL The `url` property resolves to a string that points to your front-end application. This value is used as the `src` attribute of the iframe rendering your front-end. Once loaded, the Admin Panel will communicate directly with your app through `window.postMessage` events. To set the URL, use the `admin.livePreview.url` property in your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { // ... livePreview: { url: 'http://localhost:3000', // highlight-line collections: ['pages'], }, }, }) ``` #### Dynamic URLs You can also pass a function in order to dynamically format URLs. This is useful for multi-tenant applications, localization, or any other scenario where the URL needs to be generated based on the Document being edited. This is also useful for conditionally rendering Live Preview, similar to access control. See [Conditional Rendering](./conditional-rendering) for more details. To set dynamic URLs, set the `admin.livePreview.url` property in your [Payload Config](../configuration/overview) to a function: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { // ... livePreview: { // highlight-start url: ({ data, collectionConfig, locale }) => `${data.tenant.url}${ collectionConfig.slug === 'posts' ? `/posts/${data.slug}` : `${data.slug !== 'home' ? `/${data.slug}` : ''}` }${locale ? `?locale=${locale?.code}` : ''}`, // Localization query param collections: ['pages'], }, // highlight-end }, }) ``` The following arguments are provided to the `url` function: | Path | Description | | ---------------------- | --------------------------------------------------------------------------------------------------------------------- | | **`data`** | The data of the Document being edited. This includes changes that have not yet been saved. | | **`locale`** | The locale currently being edited (if applicable). [More details](../configuration/localization). | | **`collectionConfig`** | The Collection Admin Config of the Document being edited. [More details](../configuration/collections#admin-options). | | **`globalConfig`** | The Global Admin Config of the Document being edited. [More details](../configuration/globals#admin-options). | | **`req`** | The Payload Request object. | You can return either an absolute URL or relative URL from this function. If you don't know the URL of your frontend at build-time, you can return a relative URL, and in that case, Payload will automatically construct an absolute URL by injecting the protocol, domain, and port from your browser window. Returning a relative URL is helpful for platforms like Vercel where you may have preview deployment URLs that are unknown at build time. If your application requires a fully qualified URL, or you are attempting to preview with a frontend on a different domain, you can use the `req` property to build this URL: ```ts url: ({ data, req }) => `${req.protocol}//${req.host}/${data.slug}` ``` #### Conditional Rendering You can conditionally render Live Preview by returning `undefined` or `null` from the `url` function. This is similar to access control, where you may want to restrict who can use Live Preview based on certain criteria, such as the current user or document data. For example, you could check the user's role and only enable Live Preview if they have the appropriate permissions: ```ts url: ({ req }) => (req.user?.role === 'admin' ? '/hello-world' : null) ``` ### Breakpoints The breakpoints property is an array of objects which are used as “device sizes” in the preview window. Each item will render as an option in the toolbar. When selected, the preview window will resize to the exact dimensions specified in that breakpoint. To set breakpoints, use the `admin.livePreview.breakpoints` property in your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... admin: { // ... livePreview: { url: 'http://localhost:3000', // highlight-start breakpoints: [ { label: 'Mobile', name: 'mobile', width: 375, height: 667, }, ], // highlight-end }, }, }) ``` The following options are available for each breakpoint: | Path | Description | | --------------- | --------------------------------------------------------------------------- | | **`label`** \* | The label to display in the drop-down. This is what the user will see. | | **`name`** \* | The name of the breakpoint. | | **`width`** \* | The width of the breakpoint. This is used to set the width of the iframe. | | **`height`** \* | The height of the breakpoint. This is used to set the height of the iframe. | _\* An asterisk denotes that a property is required._ The "Responsive" option is always available in the drop-down and requires no additional configuration. This is the default breakpoint that will be used on initial load. This option styles the iframe with a width and height of `100%` so that it fills the screen at its maximum size and automatically resizes as the window changes size. You may also explicitly resize the Live Preview by using the corresponding inputs in the toolbar. This will temporarily override the breakpoint selection to "Custom" until a predefined breakpoint is selected once again. If you prefer to freely resize the Live Preview without the use of breakpoints, you can open it in a new window by clicking the button in the toolbar. This will close the iframe and open a new window which can be resized as you wish. Closing it will automatically re-open the iframe. ## Example For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview). # Implementing Live Preview in your frontend Source: https://payloadcms.com/docs/live-preview/frontend There are two ways to use Live Preview in your own application depending on whether your front-end framework supports Server Components: - [Server-side Live Preview (suggested)](./server) - [Client-side Live Preview](./client) We suggest using server-side Live Preview if your framework supports Server Components, it is both simpler to setup and more performant to run than the client-side alternative. # Server-side Live Preview Source: https://payloadcms.com/docs/live-preview/server Server-side Live Preview is only for front-end frameworks that support the concept of Server Components, i.e. [React Server Components](https://react.dev/reference/rsc/server-components). If your front-end application is built with a client-side framework like the [Next.js Pages Router](https://nextjs.org/docs/pages), [React Router](https://reactrouter.com), [Vue 3](https://vuejs.org), etc., see [client-side Live Preview](./client). Server-side Live Preview works by making a roundtrip to the server every time your document is saved, i.e. draft save, autosave, or publish. While using Live Preview, the Admin Panel emits a new `window.postMessage` event which your front-end application can use to invoke this process. In Next.js, this means simply calling `router.refresh()` which will hydrate the HTML using new data straight from the [Local API](../local-api/overview). It is recommended that you enable [Autosave](../versions/autosave) alongside Live Preview to make the experience feel more responsive. If your front-end application is built with [React](#react), you can use the `RefreshRouteOnChange` function that Payload provides. In the future, all other major frameworks like Vue and Svelte will be officially supported. If you are using any of these frameworks today, you can still integrate with Live Preview yourself using the underlying tooling that Payload provides. See [building your own router refresh component](#building-your-own-router-refresh-component) for more information. ## React If your front-end application is built with server-side [React](https://react.dev) like [Next.js App Router](https://nextjs.org/docs/app), you can use the `RefreshRouteOnSave` component that Payload provides. First, install the `@payloadcms/live-preview-react` package: ```bash npm install @payloadcms/live-preview-react ``` Then, render the `RefreshRouteOnSave` component anywhere in your `page.tsx`. Here's an example: `page.tsx`: ```tsx import { RefreshRouteOnSave } from './RefreshRouteOnSave.tsx' import { getPayload } from 'payload' import config from '../payload.config' export default async function Page() { const payload = await getPayload({ config }) const page = await payload.findByID({ collection: 'pages', id: '123', draft: true, trash: true, // add this if trash is enabled in your collection and want to preview trashed documents }) return (

    {page.title}

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

    {data.title}

    } ``` **Reminder:** If you are using [React Server Components](https://react.dev/reference/rsc/server-components), we strongly suggest setting up [server-side Live Preview](./server) instead. ### Vue If your front-end application is built with [Vue 3](https://vuejs.org) or [Nuxt 3](https://nuxt.js), you can use the `useLivePreview` composable that Payload provides. First, install the `@payloadcms/live-preview-vue` package: ```bash npm install @payloadcms/live-preview-vue ``` Then, use the `useLivePreview` hook in your Vue component: ```ts ``` ## Building your own hook No matter what front-end framework you are using, you can build your own hook using the same underlying tooling that Payload provides. First, install the base `@payloadcms/live-preview` package: ```bash npm install @payloadcms/live-preview ``` This package provides the following functions: | Path | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------ | | **`subscribe`** | Subscribes to the Admin Panel's `window.postMessage` events and calls the provided callback function. | | **`unsubscribe`** | Unsubscribes from the Admin Panel's `window.postMessage` events. | | **`ready`** | Sends a `window.postMessage` event to the Admin Panel to indicate that the front-end is ready to receive messages. | | **`isLivePreviewEvent`** | Checks if a `MessageEvent` originates from the Admin Panel and is a Live Preview event, i.e. debounced form state. | The `subscribe` function takes the following args: | Path | Description | | ------------------ | ------------------------------------------------------------------------------------------- | | **`callback`** \* | A callback function that is called with `data` every time a change is made to the document. | | **`serverURL`** \* | The URL of your Payload server. | | **`initialData`** | The initial data of the document. The live data will be merged in as changes are made. | | **`depth`** | The depth of the relationships to fetch. Defaults to `0`. | With these functions, you can build your own hook using your front-end framework of choice: ```tsx import { subscribe, unsubscribe } from '@payloadcms/live-preview' // To build your own hook, subscribe to Live Preview events using the `subscribe` function // It handles everything from: // 1. Listening to `window.postMessage` events // 2. Merging initial data with active form state // 3. Populating relationships and uploads // 4. Calling the `onChange` callback with the result // Your hook should also: // 1. Tell the Admin Panel when it is ready to receive messages // 2. Handle the results of the `onChange` callback to update the UI // 3. Unsubscribe from the `window.postMessage` events when it unmounts ``` Here is an example of what the same `useLivePreview` React hook from above looks like under the hood: ```tsx import { subscribe, unsubscribe, ready } from '@payloadcms/live-preview' import { useCallback, useEffect, useState, useRef } from 'react' export const useLivePreview = (props: { depth?: number initialData: T serverURL: string }): { data: T isLoading: boolean } => { const { depth = 0, initialData, serverURL } = props const [data, setData] = useState(initialData) const [isLoading, setIsLoading] = useState(true) const hasSentReadyMessage = useRef(false) const onChange = useCallback((mergedData) => { // When a change is made, the `onChange` callback will be called with the merged data // Set this merged data into state so that React will re-render the UI setData(mergedData) setIsLoading(false) }, []) useEffect(() => { // Listen for `window.postMessage` events from the Admin Panel // When a change is made, the `onChange` callback will be called with the merged data const subscription = subscribe({ callback: onChange, depth, initialData, serverURL, }) // Once subscribed, send a `ready` message back up to the Admin Panel // This will indicate that the front-end is ready to receive messages if (!hasSentReadyMessage.current) { hasSentReadyMessage.current = true ready({ serverURL, }) } // When the component unmounts, unsubscribe from the `window.postMessage` events return () => { unsubscribe(subscription) } }, [serverURL, onChange, depth, initialData]) return { data, isLoading, } } ``` When building your own hook, ensure that the args and return values are consistent with the ones listed at the top of this document. This will ensure that all hooks follow the same API. ## Example For a working demonstration of this, check out the official [Live Preview Example](https://github.com/payloadcms/payload/tree/main/examples/live-preview). There you will find an example of a fully integrated Next.js App Router front-end that runs on the same server as Payload. ## Troubleshooting #### Relationships and/or uploads are not populating If you are using relationships or uploads in your front-end application, and your front-end application runs on a different domain than your Payload server, you may need to configure [CORS](../configuration/overview#cors) to allow requests to be made between the two domains. This includes sites that are running on a different port or subdomain. Similarly, if you are protecting resources behind user authentication, you may also need to configure [CSRF](../authentication/cookies#csrf-prevention) to allow cookies to be sent between the two domains. For example: ```ts // payload.config.ts { // ... // If your site is running on a different domain than your Payload server, // This will allow requests to be made between the two domains cors: [ 'http://localhost:3001' // Your front-end application ], // If you are protecting resources behind user authentication, // This will allow cookies to be sent between the two domains csrf: [ 'http://localhost:3001' // Your front-end application ], } ``` #### Relationships and/or uploads disappear after editing a document It is possible that either you are setting an improper [`depth`](../queries/depth) in your initial request and/or your `useLivePreview` hook, or they're mismatched. Ensure that the `depth` parameter is set to the correct value, and that it matches exactly in both places. For example: ```tsx // Your initial request const { docs } = await payload.find({ collection: 'pages', depth: 1, // Ensure this is set to the proper depth for your application where: { slug: { equals: 'home', }, }, }) ``` ```tsx // Your hook const { data } = useLivePreview({ initialData: initialPage, serverURL: PAYLOAD_SERVER_URL, depth: 1, // Ensure this matches the depth of your initial request }) ``` #### Iframe refuses to connect If your front-end application has set a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) that blocks the Admin Panel from loading your front-end application, the iframe will not be able to load your site. To resolve this, you can whitelist the Admin Panel's domain in your CSP by setting the `frame-ancestors` directive: ```plaintext frame-ancestors: "self" localhost:* https://your-site.com; ``` # Versions Source: https://payloadcms.com/docs/versions/overview Payload's powerful Versions functionality allows you to keep a running history of changes over time and extensible to fit any content publishing workflow. When enabled, Payload will automatically scaffold a new Collection in your database to store versions of your document(s) over time, and the Admin UI will be extended with additional views that allow you to browse document versions, view diffs in order to see exactly what has changed in your documents (and when they changed), and restore documents back to prior versions easily. ![Versions](/images/docs/versions-v3.jpg) _Comparing an old version to a newer version of a document_ **With Versions, you can:** - Maintain an audit log / history of every change ever made to a document, including monitoring for what user made which change - Restore documents and globals to prior states in case you need to roll back changes - Build a true [Draft Preview](../versions/drafts) mode for your data - Manage who can see Drafts, and who can only see Published documents via [Access Control](../access-control/overview) - Enable [Autosave](../versions/autosave) on collections and globals to never lose your work again - Build a powerful publishing schedule mechanism to create documents and have them become publicly readable automatically at a future date Versions are extremely performant and totally opt-in. They don't change the shape of your data at all. All versions are stored in a separate Collection and can be turned on and off easily at your risk. ## Options Versions support a few different levels of functionality that each come with their own impacts to document workflow. ### Versions enabled, drafts disabled If you enable versions but keep draft mode disabled, Payload will simply create a new version of a document each time you update a document. This is great for use cases where you need to retain a history of all document updates over time, but always want to treat the newest document version as the version that is "published". For example, a use case for "versions enabled, drafts disabled" could be on a collection of users, where you might want to keep a version history (or audit log) of all changes ever made to users - but any changes to users should _always_ be treated as "published" and you have no need to maintain a "draft" version of a user. ### Versions and drafts enabled If you have versions _and_ drafts enabled, you are able to control which documents are published, and which are considered draft. That lets you write [Access Control](../access-control/overview) to control who can see published documents, and who can see draft documents. It also lets you save versions (drafts) that are _newer_ than your most recently published document, which is helpful if you want to draft changes and maybe even preview them before you publish the changes. Read more about Drafts [here](../versions/drafts). ### Versions, drafts, and autosave enabled When you have versions, drafts, _and_ `autosave` enabled, the Admin UI will automatically save changes that you make to a new `draft` version as you edit a document, which makes sure that you never lose your changes ever again. Autosave will not affect your published post at all—instead, it'll just save your changes and let you publish them whenever you or your editors are ready to do so. Read more about Autosave [here](../versions/autosave). ## Collection config Configuring Versions is done by adding the `versions` key to your Collection configs. Set it to `true` to enable default Versions settings, or customize versions options by setting the property equal to an object containing the following available options: | Option | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `maxPerDoc` | Use this setting to control how many versions to keep on a document by document basis. Must be an integer. Defaults to 100, use 0 to save all versions. | | `drafts` | Enable [Drafts](../versions/drafts) mode for this collection. To enable, set to `true` or pass an object with `draft` [options](../versions/drafts#options). | ## Global config Global versions work similarly to Collection versions but have a slightly different set of config properties supported. | Option | Description | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `max` | Use this setting to control how many versions to keep on a global by global basis. Must be an integer. | | `drafts` | Enable [Drafts](../versions/drafts) mode for this global. To enable, set to `true` or pass an object with `draft` [options](../versions/drafts#options) | ### Database impact By enabling `versions`, a new database collection will be made to store versions for your collection or global. The collection will be named based off the `slug` of the collection or global and will follow this pattern (where `slug` is replaced with the `slug` of your collection or global): ``` _slug_versions ``` Each document in this new `versions` collection will store a set of meta properties about the version as well as a _full_ copy of the document. For example, a version's data might look like this for a Collection document: ```json { "_id": "61cf752c19cdf1b1af7b61f1", // a unique ID of this version "parent": "61ce1354091d5b3ffc20ea6e", // the ID of the parent document "autosave": false, // used to denote if this version was created via autosave "version": { // your document's data goes here // all fields are set to not required and this property can be partially complete }, "createdAt": "2021-12-31T21:25:00.992+00:00", "updatedAt": "2021-12-31T21:25:00.992+00:00" } ``` Global versions are stored the same as the collection version shown above, except they do not feature the `parent` property, as each Global receives its own `versions` collection. That means we know that all versions in that collection correspond to that specific global. ## Version operations Versions expose new operations for both collections and globals. They allow you to find and query versions, find a single version by ID, and publish (or restore) a version by ID. Both Collections and Globals support the same new operations. They are used primarily by the admin UI, but if you are writing custom logic in your app and would like to utilize them, they're available for you to use as well via REST, GraphQL, and Local APIs. **Collection REST endpoints:** | Method | Path | Description | | ------ | ------------------------------------ | --------------------------------- | | `GET` | `/api/{collectionSlug}/versions` | Find and query paginated versions | | `GET` | `/api/{collectionSlug}/versions/:id` | Find a specific version by ID | | `POST` | `/api/{collectionSlug}/versions/:id` | Restore a version by ID | **Collection GraphQL queries:** | Query Name | Operation | | ---------------------------------------- | ----------------- | | **`version{collection.label.singular}`** | `findVersionByID` | | **`versions{collection.label.plural}`** | `findVersions` | **And mutation:** | Query Name | Operation | | ----------------------------------------------- | ---------------- | | **`restoreVersion{collection.label.singular}`** | `restoreVersion` | **Collection Local API methods:** ### Find ```js // Result will be a paginated set of Versions. // See /docs/queries/pagination for more. const result = await payload.findVersions({ collection: 'posts', // required depth: 2, page: 1, limit: 10, where: {}, // pass a `where` query here sort: '-createdAt', locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Find by ID ```js // Result will be a Post document. const result = await payload.findVersionByID({ collection: 'posts', // required id: '507f1f77bcf86cd799439013', // required depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Restore ```js // Result will be the restored global document. const result = await payload.restoreVersion({ collection: 'posts', // required id: '507f1f77bcf86cd799439013', // required depth: 2, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` **Global REST endpoints:** | Method | Path | Description | | ------ | ---------------------------------------- | --------------------------------- | | `GET` | `/api/globals/{globalSlug}/versions` | Find and query paginated versions | | `GET` | `/api/globals/{globalSlug}/versions/:id` | Find a specific version by ID | | `POST` | `/api/globals/{globalSlug}/versions/:id` | Restore a version by ID | **Global GraphQL queries:** | Query Name | Operation | | ---------------------------- | ----------------- | | **`version{global.label}`** | `findVersionByID` | | **`versions{global.label}`** | `findVersions` | **Global GraphQL mutation:** | Query Name | Operation | | ---------------------------------- | ---------------- | | **`restoreVersion{global.label}`** | `restoreVersion` | **Global Local API methods:** ### Find ```js // Result will be a paginated set of Versions. // See /docs/queries/pagination for more. const result = await payload.findGlobalVersions({ slug: 'header', // required depth: 2, page: 1, limit: 10, where: {}, // pass a `where` query here sort: '-createdAt', locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Find by ID ```js // Result will be a Post document. const result = await payload.findGlobalVersionByID({ slug: 'header', // required id: '507f1f77bcf86cd799439013', // required depth: 2, locale: 'en', fallbackLocale: false, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ### Restore ```js // Result will be the restored global document. const result = await payload.restoreGlobalVersion({ slug: 'header', // required id: '507f1f77bcf86cd799439013', // required depth: 2, user: dummyUser, overrideAccess: false, showHiddenFields: true, }) ``` ## Access Control Versions expose a new [Access Control](../access-control/overview) function on both [Collections](../configuration/collections) and [Globals](../configuration/globals) that allow for you to control who can see versions of documents, and who can't. | Function | Allows/Denies Access | | ------------------ | ---------------------------------------------------------------------------------------------------------------------- | | **`readVersions`** | Used to control who can read versions, and who can't. Will automatically restrict the Admin UI version viewing access. | For full details on how to use Access Control with Versions, see the [Access Control](../access-control/overview) documentation. # Drafts Source: https://payloadcms.com/docs/versions/drafts Payload's Draft functionality builds on top of the Versions functionality to allow you to make changes to your collection documents and globals, but publish only when you're ready. This functionality allows you to build powerful Preview environments for your data, where you can make sure your changes look good before publishing documents. Drafts rely on Versions being enabled in order to function. By enabling Versions with Drafts, your collections and globals can maintain _newer_, and _unpublished_ versions of your documents. It's perfect for cases where you might want to work on a document, update it and save your progress, but not necessarily make it publicly published right away. Drafts are extremely helpful when building preview implementations. ![Drafts Enabled](/images/docs/autosave-drafts.jpg) _If Drafts are enabled, the typical Save button is replaced with new actions which allow you to either save a draft, or publish your changes._ ## Options Collections and Globals both support the same options for configuring drafts. You can either set `versions.drafts` to `true`, or pass an object to configure draft properties. | Draft Option | Description | | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `autosave` | Enable `autosave` to automatically save progress while documents are edited. To enable, set to `true` or pass an object with [options](../versions/autosave). | | `schedulePublish` | Allow for editors to schedule publish / unpublish events in the future. [More](#scheduled-publish) | | `validate` | Set `validate` to `true` to validate draft documents when saved. Default is `false`. | ## Database changes By enabling drafts on a collection or a global, Payload will **automatically inject a new field into your schema** called `_status`. The `_status` field is used internally by Payload to store if a document is set to `draft` or `published`. **Admin UI status indication** Within the Admin UI, if drafts are enabled, a document can be shown with one of three "statuses": 1. **Draft** - if a document has never been published, and only draft versions of the document are present 1. **Published** - if a document is published and there are no newer drafts available 1. **Changed** - if a document has been published, but there are newer drafts available and not yet published ## Draft API If drafts are enabled on your collection or global, important and powerful changes are made to your REST, GraphQL, and Local APIs that allow you to specify if you are interacting with drafts or with live documents. #### Updating or creating drafts If you enable drafts on a collection or global, the `create` and `update` operations for REST, GraphQL, and Local APIs expose a new option called `draft` which allows you to specify if you are creating or updating a **draft**, or if you're just sending your changes straight to the published document. For example: ```ts // REST API POST /api/your-collection?draft=true // Local API await payload.create({ collection: 'your-collection', data: { // your data here }, draft: true, // This is required to create a draft }) // GraphQL mutation { createYourCollection(data: { ... }, draft: true) { // ... } } ``` **Required fields** If `draft` is enabled while creating or updating a document, all fields are considered as not required, so that you can save drafts that are incomplete. Setting `_status: "draft"` will not bypass the required fields. You need to set `draft: true` as shown in the previous examples. #### Reading drafts vs. published documents In addition to the `draft` argument within `create` and `update` operations, a `draft` argument is also exposed for `find` and `findByID` operations. If `draft` is set to `true` while reading a document, **Payload will automatically replace returned document(s) with their newest drafts** if any newer drafts are available. **For example, let's take the following scenario:** 1. You create a new collection document and publish it right away 1. You then make some updates, and save the updates as a draft 1. You then make some further updates, and save more updates as another draft Here, you will have a published document that resides in your main collection, and then you'll have two _newer_ drafts that reside in the `_[collectionSlug]_versions` database collection. If you simply fetch your created document using a `find` or `findByID` operation, your published document will be returned and the drafts will be ignored. But, if you specify `draft` as `true`, Payload will automatically replace your published document's content with content coming from the most recently saved `version`. In this case, as we have created _two_ versions in the above scenario, Payload will send back data from the newest (second) draft and your document will appear as the most recently drafted version instead of the published version. **Important:** the `draft` argument on its own will not restrict documents with `_status: 'draft'` from being returned from the API. You need to use Access Control to prevent documents with `_status: 'draft'` from being returned to unauthenticated users. Read below for more information on how this works. ## Controlling who can see Collection drafts If you're using the **drafts** feature, it's important for you to consider who can view your drafts, and who can view only published documents. Luckily, Payload makes this extremely simple and puts the power completely in your hands. #### Restricting draft access You can use the `read` [Access Control](../access-control/collections#read) method to restrict who is able to view drafts of your documents by simply returning a [query constraint](../queries/overview) which restricts the documents that any given user is able to retrieve. Here is an example that utilizes the `_status` field to require a user to be logged in to retrieve drafts: ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', access: { read: ({ req }) => { // If there is a user logged in, // let them retrieve all documents if (req.user) return true // If there is no user, // restrict the documents that are returned // to only those where `_status` is equal to `published` return { _status: { equals: 'published', }, } }, }, versions: { drafts: true, }, //.. the rest of the Pages config here } ``` **Note regarding adding versions to an existing collection** If you already have a collection with documents, and you _opt in_ to draft functionality after you have already created existing documents, all of your old documents _will not have a `_status` field_ until you resave them. For this reason, if you are _adding_ versions into an existing collection, you might want to write your Access Control function to allow for users to read both documents where **`_status` is equal to `"published"`** as well as where **`_status` does not exist**. Here is an example for how to write an [Access Control](../access-control/overview) function that grants access to both documents where `_status` is equal to "published" and where `_status` does not exist: ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', access: { read: ({ req }) => { // If there is a user logged in, // let them retrieve all documents if (req.user) return true // If there is no user, // restrict the documents that are returned // to only those where `_status` is equal to `published` // or where `_status` does not exist return { or: [ { _status: { equals: 'published', }, }, { _status: { exists: false, }, }, ], } }, }, versions: { drafts: true, }, //.. the rest of the Pages config here } ``` ## Scheduled publish Payload provides for an ability to schedule publishing / unpublishing events in the future, which can be helpful if you need to set certain documents to "go live" at a given date in the future, or, vice versa, revert to a draft state after a certain time has passed. You can enable this functionality on both collections and globals via the `versions.drafts.schedulePublish: true` property. **Important:** if you are going to enable scheduled publish / unpublish, you need to make sure your Payload app is set up to process [Jobs](../jobs-queue/overview). This feature works by creating a Job in the background, which will be picked up after the job becomes available. If you do not have any mechanism in place to run jobs, your scheduled publish / unpublish jobs will never be executed. ## Unpublishing drafts If a document is published, the Payload Admin UI will be updated to show an "unpublish" button at the top of the sidebar, which will "unpublish" the currently published document. Consider this as a way to "revert" a document back to a draft state. On the API side, this is done by simply setting `_status: 'draft'` on any document. ## Reverting to published If a document is published, and you have made further changes which are saved as a draft, Payload will show a "revert to published" button at the top of the sidebar which will allow you to reject your draft changes and "revert" back to the published state of the document. Your drafts will still be saved, but a new version will be created that will reflect the last published state of the document. # Autosave Source: https://payloadcms.com/docs/versions/autosave Extending on Payload's [Draft](../versions/drafts) functionality, you can configure your collections and globals to autosave changes as drafts, and publish only you're ready. The Admin UI will automatically adapt to autosaving progress at an interval that you define, and will store all autosaved changes as a new Draft version. Never lose your work - and publish changes to the live document only when you're ready. Autosave relies on Versions and Drafts being enabled in order to function. ![Autosave Enabled](/images/docs/autosave-v3.jpg) _If Autosave is enabled, drafts will be created automatically as the document is modified and the Admin UI adds an indicator describing when the document was last saved to the top right of the sidebar._ ## Options Collections and Globals both support the same options for configuring autosave. You can either set `versions.drafts.autosave` to `true`, or pass an object to configure autosave properties. | Drafts Autosave Options | Description | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `interval` | Define an `interval` in milliseconds to automatically save progress while documents are edited. Document updates are "debounced" at this interval. Defaults to `800`. | | `showSaveDraftButton` | Set this to `true` to show the "Save as draft" button even while autosave is enabled. Defaults to `false`. | **Example config with versions, drafts, and autosave enabled:** ```ts import type { CollectionConfig } from 'payload' export const Pages: CollectionConfig = { slug: 'pages', access: { read: ({ req }) => { // If there is a user logged in, // let them retrieve all documents if (req.user) return true // If there is no user, // restrict the documents that are returned // to only those where `_status` is equal to `published` return { _status: { equals: 'published', }, } }, }, versions: { drafts: { autosave: true, // Alternatively, you can specify an object to customize autosave: // autosave: { // Define how often the document should be autosaved (in milliseconds) // interval: 1500, // // Show the "Save as draft" button even while autosave is enabled // showSaveDraftButton: true, // }, }, }, //.. the rest of the Pages config here } ``` ## Autosave API When `autosave` is enabled, all `update` operations within Payload expose a new argument called `autosave`. When set to `true`, Payload will treat the incoming draft update as an `autosave`. This is primarily used by the Admin UI, but there may be some cases where you are building an app for your users and wish to implement `autosave` in your own app. To do so, use the `autosave` argument in your `update` operations. ### How autosaves are stored If we created a new version for each autosave, you'd quickly find a ton of autosaves that clutter up your `_versions` collection within the database. That would be messy quick because `autosave` is typically set to save a document at ~800ms intervals. Instead of creating a new version each time a document is autosaved, Payload smartly only creates **one** autosave version, and then updates that specific version with each autosave performed. This makes sure that your versions remain nice and tidy. # Uploads Source: https://payloadcms.com/docs/upload/overview Payload provides everything you need to enable file upload, storage, and management directly on your server—including extremely powerful file [access control](#access-control). **Here are some common use cases of Uploads:** - Creating a "Media Library" that contains images for use throughout your site or app - Building a Gated Content library where users need to sign up to gain access to downloadable assets like ebook PDFs, whitepapers, etc. - Storing publicly available, downloadable assets like software, ZIP files, MP4s, etc. **By simply enabling Upload functionality on a Collection, Payload will automatically transform your Collection into a robust file management / storage solution. The following modifications will be made:** 1. `filename`, `mimeType`, and `filesize` fields will be automatically added to your Collection. Optionally, if you pass `imageSizes` to your Collection's Upload config, a [`sizes`](#image-sizes) array will also be added containing auto-resized image sizes and filenames. 1. The Admin Panel will modify its built-in `List` component to show a thumbnail for each upload within the List View 1. The Admin Panel will modify its `Edit` view(s) to add a new set of corresponding Upload UI which will allow for file upload 1. The `create`, `update`, and `delete` Collection operations will be modified to support file upload, re-upload, and deletion ## Enabling Uploads Every Payload Collection can opt-in to supporting Uploads by specifying the `upload` property on the Collection's config to either `true` or to an object containing `upload` options. **Tip:** A common pattern is to create a **"media"** collection and enable **upload** on that collection. ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { staticDir: 'media', imageSizes: [ { name: 'thumbnail', width: 400, height: 300, position: 'centre', }, { name: 'card', width: 768, height: 1024, position: 'centre', }, { name: 'tablet', width: 1024, // By specifying `undefined` or leaving a height undefined, // the image will be sized to a certain width, // but it will retain its original aspect ratio // and calculate a height automatically. height: undefined, position: 'centre', }, ], adminThumbnail: 'thumbnail', mimeTypes: ['image/*'], }, fields: [ { name: 'alt', type: 'text', }, ], } ``` ### Collection Upload Options _An asterisk denotes that an option is required._ | Option | Description | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`adminThumbnail`** | Set the way that the [Admin Panel](../admin/overview) will display thumbnails for this Collection. [More](#admin-thumbnails) | | **`bulkUpload`** | Allow users to upload in bulk from the list view, default is true | | **`cacheTags`** | Set to `false` to disable the cache tag set in the UI for the admin thumbnail component. Useful for when CDNs don't allow certain cache queries. | | **`constructorOptions`** | An object passed to the the Sharp image library that accepts any Constructor options and applies them to the upload file. [More](https://sharp.pixelplumbing.com/api-constructor/) | | **`crop`** | Set to `false` to disable the cropping tool in the [Admin Panel](../admin/overview). Crop is enabled by default. [More](#crop-and-focal-point-selector) | | **`disableLocalStorage`** | Completely disable uploading files to disk locally. [More](#disabling-local-upload-storage) | | **`displayPreview`** | Enable displaying preview of the uploaded file in Upload fields related to this Collection. Can be locally overridden by `displayPreview` option in Upload field. [More](../fields/upload#config-options). | | **`externalFileHeaderFilter`** | Accepts existing headers and returns the headers after filtering or modifying. If using this option, you should handle the removal of any sensitive cookies (like payload-prefixed cookies) to prevent leaking session information to external services. By default, Payload automatically filters out payload-prefixed cookies when this option is not defined. | | **`filesRequiredOnCreate`** | Mandate file data on creation, default is true. | | **`filenameCompoundIndex`** | Field slugs to use for a compound index instead of the default filename index. | | **`focalPoint`** | Set to `false` to disable the focal point selection tool in the [Admin Panel](../admin/overview). The focal point selector is only available when `imageSizes` or `resizeOptions` are defined. [More](#crop-and-focal-point-selector) | | **`formatOptions`** | An object with `format` and `options` that are used with the Sharp image library to format the upload file. [More](https://sharp.pixelplumbing.com/api-output#toformat) | | **`handlers`** | Array of Request handlers to execute when fetching a file, if a handler returns a Response it will be sent to the client. Otherwise Payload will retrieve and send back the file. | | **`imageSizes`** | If specified, image uploads will be automatically resized in accordance to these image sizes. [More](#image-sizes) | | **`mimeTypes`** | Restrict mimeTypes in the file picker. Array of valid mimetypes or mimetype wildcards [More](#mimetypes) | | **`pasteURL`** | Controls whether files can be uploaded from remote URLs by pasting them into the Upload field. **Enabled by default.** Accepts `false` to disable or an object with an `allowList` of valid remote URLs. [More](#uploading-files-from-remote-urls) | | **`resizeOptions`** | An object passed to the the Sharp image library to resize the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize) | | **`skipSafeFetch`** | Set to an `allowList` to skip the safe fetch check when fetching external files. Set to `true` to skip the safe fetch for all documents in this collection. Defaults to `false`. | | **`allowRestrictedFileTypes`** | Set to `true` to allow restricted file types. If your Collection has defined [mimeTypes](#mimetypes), restricted file verification will be skipped. Defaults to `false`. [More](#restricted-file-types) | | **`staticDir`** | The folder directory to use to store media in. Can be either an absolute path or relative to the directory that contains your config. Defaults to your collection slug | | **`trimOptions`** | An object passed to the the Sharp image library to trim the uploaded file. [More](https://sharp.pixelplumbing.com/api-resize#trim) | | **`withMetadata`** | If specified, appends metadata to the output image file. Accepts a boolean or a function that receives `metadata` and `req`, returning a boolean. | | **`hideFileInputOnCreate`** | Set to `true` to prevent the admin UI from showing file inputs during document creation, useful for programmatic file generation. | | **`hideRemoveFile`** | Set to `true` to prevent the admin UI having a way to remove an existing file while editing. | | **`modifyResponseHeaders`** | Accepts an object with existing `headers` and allows you to manipulate the response headers for media files. [More](#modifying-response-headers) | ### Payload-wide Upload Options Upload options are specifiable on a Collection by Collection basis, you can also control app wide options by passing your base Payload Config an `upload` property containing an object supportive of all `Busboy` configuration options. | Option | Description | | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`abortOnLimit`** | A boolean that, if `true`, returns HTTP 413 if a file exceeds the file size limit. If `false`, the file is truncated. Defaults to `false`. | | **`createParentPath`** | Set to `true` to automatically create a directory path when moving files from a temporary directory or buffer. Defaults to `false`. | | **`debug`** | A boolean that turns upload process logging on if `true`, or off if `false`. Useful for troubleshooting. Defaults to `false`. | | **`limitHandler`** | A function which is invoked if the file is greater than configured limits. | | **`parseNested`** | Set to `true` to turn `req.body` and `req.files` into nested structures. By default `req.body` and `req.files` are flat objects. Defaults to `false`. | | **`preserveExtension`** | Preserves file extensions with the `safeFileNames` option. Limits file names to 3 characters if `true` or a custom length if a `number`, trimming from the start of the extension. | | **`responseOnLimit`** | A `string` that is sent in the Response to a client if the file size limit is exceeded when used with `abortOnLimit`. | | **`safeFileNames`** | Set to `true` to strip non-alphanumeric characters except dashes and underscores. Can also be set to a regex to determine what to strip. Defaults to `false`. | | **`tempFileDir`** | A `string` path to store temporary files used when the `useTempFiles` option is set to `true`. Defaults to `'./tmp'`. | | **`uploadTimeout`** | A `number` that defines how long to wait for data before aborting, specified in milliseconds. Set to `0` to disable timeout checks. Defaults to `60000`. | | **`uriDecodeFileNames`** | Set to `true` to apply uri decoding to file names. Defaults to `false`. | | **`useTempFiles`** | Set to `true` to store files to a temporary directory instead of in RAM, reducing memory usage for large files or many files. | [Click here](https://github.com/mscdex/busboy#api) for more documentation about what you can control with `Busboy`. A common example of what you might want to customize within Payload-wide Upload options would be to increase the allowed `fileSize` of uploads sent to Payload: ```ts import { buildConfig } from 'payload' export default buildConfig({ collections: [ { slug: 'media', fields: [ { name: 'alt', type: 'text', }, ], upload: true, }, ], upload: { limits: { fileSize: 5000000, // 5MB, written in bytes }, }, }) ``` ### Custom filename via hooks You can customize the filename before it's uploaded to the server by using a `beforeOperation` hook. ```ts beforeOperation: [ ({ req, operation }) => { if ((operation === 'create' || operation === 'update') && req.file) { req.file.name = 'test.jpg' } }, ], ``` The `req.file` object will have additional information about the file, such as mimeType and extension, and you also have full access to the file data itself. The filename from here will also be threaded to image sizes if they're enabled. ## Image Sizes If you specify an array of `imageSizes` to your `upload` config, Payload will automatically crop and resize your uploads to fit each of the sizes specified by your config. The [Admin Panel](../admin/overview) will also automatically display all available files, including width, height, and file size, for each of your uploaded files. Behind the scenes, Payload relies on [`sharp`](https://sharp.pixelplumbing.com/api-resize#resize) to perform its image resizing. You can specify additional options for `sharp` to use while resizing your images. Note that for image resizing to work, `sharp` must be specified in your [Payload Config](../configuration/overview). This is configured by default if you created your Payload project with `create-payload-app`. See `sharp` in [Config Options](../configuration/overview#config-options). #### Admin List View Options Each image size also supports `admin` options to control whether it appears in the [Admin Panel](../admin/overview) list view. ```ts { name: 'thumbnail', width: 400, height: 300, admin: { disableGroupBy: true, // hide from list view groupBy options disableListColumn: true, // hide from list view columns disableListFilter: true, // hide from list view filters }, } ``` | Option | Description | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | | **`disableGroupBy`** | If set to `true`, this image size will not be available as a selectable groupBy option in the collection list view. Defaults to `false`. | | **`disableListColumn`** | If set to `true`, this image size will not be available as a selectable column in the collection list view. Defaults to `false`. | | **`disableListFilter`** | If set to `true`, this image size will not be available as a filter option in the collection list view. Defaults to `false`. | This is useful for hiding large or rarely used image sizes from the list view UI while still keeping them available in the API. #### Accessing the resized images in hooks All auto-resized images are exposed to be re-used in hooks and similar via an object that is bound to `req.payloadUploadSizes`. The object will have keys for each size generated, and each key will be set equal to a buffer containing the file data. #### Handling Image Enlargement When an uploaded image is smaller than the defined image size, we have 3 options: `withoutEnlargement: undefined | false | true` 1. `undefined` [default]: uploading images with smaller width AND height than the image size will return null 2. `false`: always enlarge images to the image size 3. `true`: if the image is smaller than the image size, return the original image **Note:** By default, the image size will return NULL when the uploaded image is smaller than the defined image size. Use the `withoutEnlargement` prop to change this. #### Custom file name per size Each image size supports a `generateImageName` function that can be used to generate a custom file name for the resized image. This function receives the original file name, the resize name, the extension, height and width as arguments. ```ts { name: 'thumbnail', width: 400, height: 300, generateImageName: ({ height, sizeName, extension, width }) => { return `custom-${sizeName}-${height}-${width}.${extension}` }, } ``` ## Crop and Focal Point Selector This feature is only available for image file types. Setting `crop: false` and `focalPoint: false` in your Upload config will be disable the respective selector in the [Admin Panel](../admin/overview). Image cropping occurs before any resizing, the resized images will therefore be generated from the cropped image (**not** the original image). If no resizing options are specified (`imageSizes` or `resizeOptions`), the focal point selector will not be displayed. ## Disabling Local Upload Storage If you are using a plugin to send your files off to a third-party file storage host or CDN, like Amazon S3 or similar, you may not want to store your files locally at all. You can prevent Payload from writing files to disk by specifying `disableLocalStorage: true` on your collection's upload config. **Note:** This is a fairly advanced feature. If you do disable local file storage, by default, your admin panel's thumbnails will be broken as you will not have stored a file. It will be totally up to you to use either a plugin or your own hooks to store your files in a permanent manner, as well as provide your own admin thumbnail using **upload.adminThumbnail**. ## Admin Thumbnails You can specify how Payload retrieves admin thumbnails for your upload-enabled Collections with one of the following: 1. `adminThumbnail` as a **string**, equal to one of your provided image size names. ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { // highlight-start adminThumbnail: 'small', // highlight-end imageSizes: [ { name: 'small', fit: 'cover', height: 300, width: 900, }, { name: 'large', fit: 'cover', height: 600, width: 1800, }, ], }, } ``` 2. `adminThumbnail` as a **function** that takes the document's data and sends back a full URL to load the thumbnail. ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { // highlight-start adminThumbnail: ({ doc }) => `https://google.com/custom-path-to-file/${doc.filename}`, // highlight-end }, } ``` ## Restricted File Types Possibly problematic file types are automatically restricted from being uploaded to your application. If your Collection has defined [mimeTypes](#mimetypes) or has set `allowRestrictedFileTypes` to `true`, restricted file verification will be skipped. Restricted file types and extensions: | File Extensions | MIME Type | | ------------------------------------ | ----------------------------------------------- | | `exe`, `dll` | `application/x-msdownload` | | `exe`, `com`, `app`, `action` | `application/x-executable` | | `bat`, `cmd` | `application/x-msdos-program` | | `exe`, `com` | `application/x-ms-dos-executable` | | `dmg` | `application/x-apple-diskimage` | | `deb` | `application/x-debian-package` | | `rpm` | `application/x-redhat-package-manager` | | `exe`, `dll` | `application/vnd.microsoft.portable-executable` | | `msi` | `application/x-msi` | | `jar`, `ear`, `war` | `application/java-archive` | | `desktop` | `application/x-desktop` | | `cpl` | `application/x-cpl` | | `lnk` | `application/x-ms-shortcut` | | `pkg` | `application/x-apple-installer` | | `htm`, `html`, `shtml`, `xhtml` | `text/html` | | `php`, `phtml` | `application/x-httpd-php` | | `js`, `jse` | `text/javascript` | | `jsp` | `application/x-jsp` | | `py` | `text/x-python` | | `rb` | `text/x-ruby` | | `pl` | `text/x-perl` | | `ps1`, `psc1`, `psd1`, `psh`, `psm1` | `application/x-powershell` | | `vbe`, `vbs` | `application/x-vbscript` | | `ws`, `wsc`, `wsf`, `wsh` | `application/x-ms-wsh` | | `scr` | `application/x-msdownload` | | `asp`, `aspx` | `application/x-asp` | | `hta` | `application/x-hta` | | `reg` | `application/x-registry` | | `url` | `application/x-url` | | `workflow` | `application/x-workflow` | | `command` | `application/x-command` | ## MimeTypes Specifying the `mimeTypes` property can restrict what files are allowed from the user's file picker. This accepts an array of strings, which can be any valid mimetype or mimetype wildcards Some example values are: `image/*`, `audio/*`, `video/*`, `image/png`, `application/pdf` **Example mimeTypes usage:** ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { mimeTypes: ['image/*', 'application/pdf'], // highlight-line }, } ``` ## Uploading Files **Important:** Uploading files is currently only possible through the REST and Local APIs due to how GraphQL works. It's difficult and fairly nonsensical to support uploading files through GraphQL. To upload a file, use your collection's [`create`](../rest-api/overview#collections) endpoint. Send it all the data that your Collection requires, as well as a `file` key containing the file that you'd like to upload. Send your request as a `multipart/form-data` request, using [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) if possible. **Note:** To include any additional fields (like `title`, `alt`, etc.), append a `_payload` field containing a JSON-stringified object of the required values. These values must match the schema of your upload-enabled collection. ```ts const fileInput = document.querySelector('#your-file-input') const formData = new FormData() formData.append('file', fileInput.files[0]) // Replace with the fields defined in your upload-enabled collection. // The example below includes an optional field like 'title'. formData.append( '_payload', JSON.stringify({ title: 'Example Title', description: 'An optional description for the file', }), ) fetch('api/:upload-slug', { method: 'POST', body: formData, /** * Do not manually add the Content-Type Header * the browser will handle this. * * headers: { * 'Content-Type': 'multipart/form-data' * } */ }) ``` ## Uploading Files stored locally If you want to upload a file stored on your machine directly using the `payload.create` method, for example, during a seed script, you can use the `filePath` property to specify the local path of the file. ```ts const localFilePath = path.resolve(__dirname, filename) await payload.create({ collection: 'media', data: { alt, }, filePath: localFilePath, }) ``` The `data` property should still include all the required fields of your `media` collection. **Important:** Remember that all custom hooks attached to the `media` collection will still trigger. Ensure that files match the specified mimeTypes or sizes defined in the collection's `formatOptions` or custom `hooks`. ## Uploading Files from Remote URLs The `pasteURL` option allows users to fetch files from remote URLs by pasting them into an Upload field. This option is **enabled by default** and can be configured to either **allow unrestricted client-side fetching** or **restrict server-side fetching** to specific trusted domains. By default, Payload uses **client-side fetching**, where the browser downloads the file directly from the provided URL. However, **client-side fetching will fail if the URL’s server has CORS restrictions**, making it suitable only for internal URLs or public URLs without CORS blocks. To fetch files from **restricted URLs** that would otherwise be blocked by CORS, use **server-side fetching** by configuring the `pasteURL` option with an `allowList` of trusted domains. This method ensures that Payload downloads the file on the server and streams it to the browser. However, for security reasons, only URLs that match the specified `allowList` will be allowed. #### Configuration Example Here’s how to configure the pasteURL option to control remote URL fetching: ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { pasteURL: { allowList: [ { hostname: 'payloadcms.com', // required pathname: '', port: '', protocol: 'https', search: '', }, { hostname: 'example.com', pathname: '/images/*', }, ], }, }, } ``` You can also adjust server-side fetching at the upload level as well, this does not effect the `CORS` policy like the `pasteURL` option does, but it allows you to skip the safe fetch check for specific URLs. ``` import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { skipSafeFetch: [ { hostname: 'example.com', pathname: '/images/*', }, ], }, } ``` ##### Accepted Values for `pasteURL` | Option | Description | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`undefined`** | Default behavior. Enables client-side fetching for internal or public URLs. | | **`false`** | Disables the ability to paste URLs into Upload fields. | | **`allowList`** | Enables server-side fetching for specific trusted URLs. Requires an array of objects defining trusted domains. See the table below for details on `AllowItem`. | ##### `AllowItem` Properties _An asterisk denotes that an option is required._ | Option | Description | Example | | ----------------- | ---------------------------------------------------------------------------------------------------- | ------------- | | **`hostname`** \* | The hostname of the allowed URL. This is required to ensure the URL is coming from a trusted source. | `example.com` | | **`pathname`** | The path portion of the URL. Supports wildcards to match multiple paths. | `/images/*` | | **`port`** | The port number of the URL. If not specified, the default port for the protocol will be used. | `3000` | | **`protocol`** | The protocol to match. Must be either `http` or `https`. Defaults to `https`. | `https` | | **`search`** | The query string of the URL. If specified, the URL must match this exact query string. | `?version=1` | ## Access Control All files that are uploaded to each Collection automatically support the `read` [Access Control](../access-control/overview) function from the Collection itself. You can use this to control who should be allowed to see your uploads, and who should not. ## Modifying response headers You can modify the response headers for files by specifying the `modifyResponseHeaders` option in your upload config. This option accepts an object with existing headers and allows you to manipulate the response headers for media files. ### Modifying existing headers With this method you can directly interface with the `Headers` object and modify the existing headers to append or remove headers. ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { modifyResponseHeaders: ({ headers }) => { headers.set('X-Frame-Options', 'DENY') // You can directly set headers without returning }, }, } ``` ### Return new headers You can also return a new `Headers` object with the modified headers. This is useful if you want to set new headers or remove existing ones. ```ts import type { CollectionConfig } from 'payload' export const Media: CollectionConfig = { slug: 'media', upload: { modifyResponseHeaders: ({ headers }) => { const newHeaders = new Headers(headers) // Copy existing headers newHeaders.set('X-Frame-Options', 'DENY') // Set new header return newHeaders }, }, } ``` # Storage Adapters Source: https://payloadcms.com/docs/upload/storage-adapters Payload offers additional storage adapters to handle file uploads. These adapters allow you to store files in different locations, such as Amazon S3, Vercel Blob Storage, Google Cloud Storage, and more. | Service | Package | | -------------------- | ----------------------------------------------------------------------------------------------------------------- | | Vercel Blob | [`@payloadcms/storage-vercel-blob`](https://github.com/payloadcms/payload/tree/main/packages/storage-vercel-blob) | | AWS S3 | [`@payloadcms/storage-s3`](https://github.com/payloadcms/payload/tree/main/packages/storage-s3) | | Azure | [`@payloadcms/storage-azure`](https://github.com/payloadcms/payload/tree/main/packages/storage-azure) | | Google Cloud Storage | [`@payloadcms/storage-gcs`](https://github.com/payloadcms/payload/tree/main/packages/storage-gcs) | | Uploadthing | [`@payloadcms/storage-uploadthing`](https://github.com/payloadcms/payload/tree/main/packages/storage-uploadthing) | | R2 | [`@payloadcms/storage-r2`](https://github.com/payloadcms/payload/tree/main/packages/storage-r2) | ## Vercel Blob Storage [`@payloadcms/storage-vercel-blob`](https://www.npmjs.com/package/@payloadcms/storage-vercel-blob) ### Installation#vercel-blob-installation ```sh pnpm add @payloadcms/storage-vercel-blob ``` ### Usage#vercel-blob-usage - Configure the `collections` object to specify which collections should use the Vercel Blob adapter. The slug _must_ match one of your existing collection slugs. - Ensure you have `BLOB_READ_WRITE_TOKEN` set in your Vercel environment variables. This is usually set by Vercel automatically after adding blob storage to your project. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. - When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. ```ts import { vercelBlobStorage } from '@payloadcms/storage-vercel-blob' import { Media } from './collections/Media' import { MediaWithPrefix } from './collections/MediaWithPrefix' export default buildConfig({ collections: [Media, MediaWithPrefix], plugins: [ vercelBlobStorage({ enabled: true, // Optional, defaults to true // Specify which collections should use Vercel Blob collections: { media: true, 'media-with-prefix': { prefix: 'my-prefix', }, }, // Token provided by Vercel once Blob storage is added to your Vercel project token: process.env.BLOB_READ_WRITE_TOKEN, }), ], }) ``` ### Configuration Options#vercel-blob-configuration | Option | Description | Default | | -------------------- | -------------------------------------------------------------------- | ----------------------------- | | `enabled` | Whether or not to enable the plugin | `true` | | `collections` | Collections to apply the Vercel Blob adapter to | | | `addRandomSuffix` | Add a random suffix to the uploaded file name in Vercel Blob storage | `false` | | `cacheControlMaxAge` | Cache-Control max-age in seconds | `365 * 24 * 60 * 60` (1 Year) | | `token` | Vercel Blob storage read/write token | `''` | | `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | | ## S3 Storage [`@payloadcms/storage-s3`](https://www.npmjs.com/package/@payloadcms/storage-s3) ### Installation#s3-installation ```sh pnpm add @payloadcms/storage-s3 ``` ### Usage#s3-usage - Configure the `collections` object to specify which collections should use the S3 Storage adapter. The slug _must_ match one of your existing collection slugs. - The `config` object can be any [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object (from [`@aws-sdk/client-s3`](https://github.com/aws/aws-sdk-js-v3)). _This is highly dependent on your AWS setup_. Check the AWS documentation for more information. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. - When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website. - Configure `signedDownloads` (either globally of per-collection in `collections`) to use [presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html) for files downloading. This can improve performance for large files (like videos) while still respecting your access control. Additionally, with `signedDownloads.shouldUseSignedURL` you can specify a condition whether Payload should use a presigned URL, if you want to use this feature only for specific files. ```ts import { s3Storage } from '@payloadcms/storage-s3' import { Media } from './collections/Media' import { MediaWithPrefix } from './collections/MediaWithPrefix' export default buildConfig({ collections: [Media, MediaWithPrefix], plugins: [ s3Storage({ collections: { media: true, 'media-with-prefix': { prefix, }, 'media-with-presigned-downloads': { // Filter only mp4 files signedDownloads: { shouldUseSignedURL: ({ collection, filename, req }) => { return filename.endsWith('.mp4') }, }, }, }, 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, // ... Other S3 configuration }, }), ], }) ``` ### Configuration Options#s3-configuration See the the [AWS SDK Package](https://github.com/aws/aws-sdk-js-v3) and [`S3ClientConfig`](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/s3) object for guidance on AWS S3 configuration. ## Azure Blob Storage [`@payloadcms/storage-azure`](https://www.npmjs.com/package/@payloadcms/storage-azure) ### Installation#azure-installation ```sh pnpm add @payloadcms/storage-azure ``` ### Usage#azure-usage - Configure the `collections` object to specify which collections should use the Azure Blob adapter. The slug _must_ match one of your existing collection slugs. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. - When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method to your website. ```ts import { azureStorage } from '@payloadcms/storage-azure' import { Media } from './collections/Media' import { MediaWithPrefix } from './collections/MediaWithPrefix' export default buildConfig({ collections: [Media, MediaWithPrefix], plugins: [ azureStorage({ collections: { media: true, 'media-with-prefix': { prefix, }, }, allowContainerCreate: process.env.AZURE_STORAGE_ALLOW_CONTAINER_CREATE === 'true', baseURL: process.env.AZURE_STORAGE_ACCOUNT_BASEURL, connectionString: process.env.AZURE_STORAGE_CONNECTION_STRING, containerName: process.env.AZURE_STORAGE_CONTAINER_NAME, }), ], }) ``` ### Configuration Options#azure-configuration | Option | Description | Default | | ---------------------- | ------------------------------------------------------------------------ | ------- | | `enabled` | Whether or not to enable the plugin | `true` | | `collections` | Collections to apply the Azure Blob adapter to | | | `allowContainerCreate` | Whether or not to allow the container to be created if it does not exist | `false` | | `baseURL` | Base URL for the Azure Blob storage account | | | `connectionString` | Azure Blob storage connection string | | | `containerName` | Azure Blob storage container name | | | `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | | ## Google Cloud Storage [`@payloadcms/storage-gcs`](https://www.npmjs.com/package/@payloadcms/storage-gcs) ### Installation#gcs-installation ```sh pnpm add @payloadcms/storage-gcs ``` ### Usage#gcs-usage - Configure the `collections` object to specify which collections should use the Google Cloud Storage adapter. The slug _must_ match one of your existing collection slugs. - When enabled, this package will automatically set `disableLocalStorage` to `true` for each collection. - When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. You must allow CORS PUT method for the bucket to your website. ```ts import { gcsStorage } from '@payloadcms/storage-gcs' import { Media } from './collections/Media' import { MediaWithPrefix } from './collections/MediaWithPrefix' export default buildConfig({ collections: [Media, MediaWithPrefix], plugins: [ gcsStorage({ collections: { media: true, 'media-with-prefix': { prefix, }, }, bucket: process.env.GCS_BUCKET, options: { apiEndpoint: process.env.GCS_ENDPOINT, projectId: process.env.GCS_PROJECT_ID, }, }), ], }) ``` ### Configuration Options#gcs-configuration | Option | Description | Default | | --------------- | --------------------------------------------------------------------------------------------------- | --------- | | `enabled` | Whether or not to enable the plugin | `true` | | `collections` | Collections to apply the storage to | | | `bucket` | The name of the bucket to use | | | `options` | Google Cloud Storage client configuration. See [Docs](https://github.com/googleapis/nodejs-storage) | | | `acl` | Access control list for files that are uploaded | `Private` | | `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | | ## Uploadthing Storage [`@payloadcms/storage-uploadthing`](https://www.npmjs.com/package/@payloadcms/storage-uploadthing) ### Installation#uploadthing-installation ```sh pnpm add @payloadcms/storage-uploadthing ``` ### Usage#uploadthing-usage - Configure the `collections` object to specify which collections should use uploadthing. The slug _must_ match one of your existing collection slugs and be an `upload` type. - Get a token from Uploadthing and set it as `token` in the `options` object. - `acl` is optional and defaults to `public-read`. - When deploying to Vercel, server uploads are limited with 4.5MB. Set `clientUploads` to `true` to do uploads directly on the client. ```ts export default buildConfig({ collections: [Media], plugins: [ uploadthingStorage({ collections: { media: true, }, options: { token: process.env.UPLOADTHING_TOKEN, acl: 'public-read', }, }), ], }) ``` ### Configuration Options#uploadthing-configuration | Option | Description | Default | | ---------------- | ------------------------------------------------------------- | ------------- | | `token` | Token from Uploadthing. Required. | | | `acl` | Access control list for files that are uploaded | `public-read` | | `logLevel` | Log level for Uploadthing | `info` | | `fetch` | Custom fetch function | `fetch` | | `defaultKeyType` | Default key type for file operations | `fileKey` | | `clientUploads` | Do uploads directly on the client to bypass limits on Vercel. | | ## R2 Storage **Note**: The R2 Storage Adapter is in **beta** as some aspects of it may change on any minor releases. [`@payloadcms/storage-r2`](https://www.npmjs.com/package/@payloadcms/storage-r2) Use this adapter to store uploads in a Cloudflare R2 bucket via the Cloudflare Workers environment. If you're trying to connect to R2 using the S3 API then you should use the [S3](#s3-storage) adapter instead. ### Installation#r2-installation ```sh pnpm add @payloadcms/storage-r2 ``` ### Usage#r2-usage - Configure the `collections` object to specify which collections should use r2. The slug _must_ match one of your existing collection slugs and be an `upload` type. - Pass in the R2 bucket binding to the `bucket` option, this should be done in the environment where Payload is running (e.g. Cloudflare Worker). - You can conditionally datamine whether or not to enable the plugin with the `enabled` option. ```ts export default buildConfig({ collections: [Media], plugins: [ r2Storage({ collections: { media: true, }, bucket: cloudflare.env.R2, }), ], }) ``` ## Custom Storage Adapters If you need to create a custom storage adapter, you can use the [`@payloadcms/plugin-cloud-storage`](https://www.npmjs.com/package/@payloadcms/plugin-cloud-storage) package. This package is used internally by the storage adapters mentioned above. ### Installation#custom-installation `pnpm add @payloadcms/plugin-cloud-storage` ### Usage#custom-usage Reference any of the existing storage adapters for guidance on how this should be structured. Create an adapter following the `GeneratedAdapter` interface. Then, pass the adapter to the `cloudStorage` plugin. ```ts export interface GeneratedAdapter { /** * Additional fields to be injected into the base * collection and image sizes */ fields?: Field[] /** * Generates the public URL for a file */ generateURL?: GenerateURL handleDelete: HandleDelete handleUpload: HandleUpload name: string onInit?: () => void staticHandler: StaticHandler } ``` ```ts import { buildConfig } from 'payload' import { cloudStoragePlugin } from '@payloadcms/plugin-cloud-storage' export default buildConfig({ plugins: [ cloudStorage({ collections: { 'my-collection-slug': { adapter: theAdapterToUse, // see docs for the adapter you want to use }, }, }), ], // The rest of your config goes here }) ``` ## Plugin options This plugin is configurable to work across many different Payload collections. A `*` denotes that the property is required. | Option | Type | Description | | ---------------- | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | `collections` \* | `Record` | Object with keys set to the slug of collections you want to enable the plugin for, and values set to collection-specific options. | | `enabled` | `boolean` | To conditionally enable/disable plugin. Default: `true`. | ## Collection-specific options | Option | Type | Description | | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `adapter` \* | [Adapter](https://github.com/payloadcms/payload/blob/main/packages/plugin-cloud-storage/src/types.ts#L49) | Pass in the adapter that you'd like to use for this collection. You can also set this field to `null` for local development if you'd like to bypass cloud storage in certain scenarios and use local storage. | | `disableLocalStorage` | `boolean` | Choose to disable local storage on this collection. Defaults to `true`. | | `disablePayloadAccessControl` | `true` | Set to `true` to disable Payload's Access Control. [More](#payload-access-control) | | `prefix` | `string` | Set to `media/images` to upload files inside `media/images` folder in the bucket. | | `generateFileURL` | [GenerateFileURL](https://github.com/payloadcms/payload/blob/main/packages/plugin-cloud-storage/src/types.ts#L67) | Override the generated file URL with one that you create. | ## Payload Access Control Payload ships with [Access Control](../access-control/overview) that runs _even on statically served files_. The same `read` Access Control property on your `upload`-enabled collections is used, and it allows you to restrict who can request your uploaded files. To preserve this feature, by default, this plugin _keeps all file URLs exactly the same_. Your file URLs won't be updated to point directly to your cloud storage source, as in that case, Payload's Access control will be completely bypassed and you would need public readability on your cloud-hosted files. Instead, all uploads will still be reached from the default `/collectionSlug/staticURL/filename` path. This plugin will "pass through" all files that are hosted on your third-party cloud service—with the added benefit of keeping your existing Access Control in place. If this does not apply to you (your upload collection has `read: () => true` or similar) you can disable this functionality by setting `disablePayloadAccessControl` to `true`. When this setting is in place, this plugin will update your file URLs to point directly to your cloud host. ## Conditionally Enabling/Disabling The proper way to conditionally enable/disable this plugin is to use the `enabled` property. ```ts cloudStoragePlugin({ enabled: process.env.MY_CONDITION === 'true', collections: { 'my-collection-slug': { adapter: theAdapterToUse, // see docs for the adapter you want to use }, }, }), ``` # Folders Source: https://payloadcms.com/docs/folders/overview Folders allow you to group documents across collections, and are a great way to organize your content. Folders are built on top of relationship fields, when you enable folders on a collection, Payload adds a hidden relationship field `folders`, that relates to a folder — or no folder. Folders also have the `folder` field, allowing folders to be nested within other folders. The configuration for folders is done in two places, the collection config and the Payload config. The collection config is where you enable folders, and the Payload config is where you configure the global folder settings. **Note:** The Folders feature is currently in beta and may be subject to change in minor versions updates prior to being stable. ## Folder Configuration On the payload config, you can configure the following settings under the `folders` property: ```ts // Type definition type RootFoldersConfiguration = { /** * If true, the browse by folder view will be enabled * * @default true */ browseByFolder?: boolean /** * An array of functions to be ran when the folder collection is initialized * This allows plugins to modify the collection configuration */ collectionOverrides?: (({ collection, }: { collection: CollectionConfig }) => CollectionConfig | Promise)[] /** * Ability to view hidden fields and collections related to folders * * @default false */ debug?: boolean /** * The Folder field name * * @default "folder" */ fieldName?: string /** * Slug for the folder collection * * @default "payload-folders" */ slug?: string } ``` ```ts // Example usage import { buildConfig } from 'payload' const config = buildConfig({ // ... folders: { // highlight-start debug: true, // optional collectionOverrides: [ async ({ collection }) => { return collection }, ], // optional fieldName: 'folder', // optional slug: 'payload-folders', // optional // highlight-end }, }) ``` ## Collection Configuration To enable folders on a collection, you need to set the `admin.folders` property to `true` on the collection config. This will add a hidden relationship field to the collection that relates to a folder — or no folder. ```ts // Type definition type CollectionFoldersConfiguration = | boolean | { /** * If true, the collection will be included in the browse by folder view * * @default true */ browseByFolder?: boolean } ``` ```ts // Example usage import { buildConfig } from 'payload' const config = buildConfig({ collections: [ { slug: 'pages', // highlight-start folders: true, // defaults to false // highlight-end }, ], }) ``` # Email Functionality Source: https://payloadcms.com/docs/email/overview ## Introduction Payload has a few email adapters that can be imported to enable email functionality. The [@payloadcms/email-nodemailer](https://www.npmjs.com/package/@payloadcms/email-nodemailer) package will be the package most will want to install. This package provides an easy way to use [Nodemailer](https://nodemailer.com) for email and won't get in your way for those already familiar. The email adapter should be passed into the `email` property of the Payload Config. This will allow Payload to send [auth-related emails](../authentication/email) for things like password resets, new user verification, and any other email sending needs you may have. ## Configuration ### Default Configuration When email is not needed or desired, Payload will log a warning on startup notifying that email is not configured. A warning message will also be logged on any attempt to send an email. ### Email Adapter An email adapter will require at least the following fields: | Option | Description | | --------------------------- | -------------------------------------------------------------------------------- | | **`defaultFromName`** \* | The name part of the From field that will be seen on the delivered email | | **`defaultFromAddress`** \* | The email address part of the From field that will be used when delivering email | ### Official Email Adapters | Name | Package | Description | | ---------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Nodemailer | [@payloadcms/email-nodemailer](https://www.npmjs.com/package/@payloadcms/email-nodemailer) | Use any [Nodemailer transport](https://nodemailer.com/transports), including SMTP, Resend, SendGrid, and more. This was provided by default in Payload 2.x. This is the easiest migration path. | | Resend | [@payloadcms/email-resend](https://www.npmjs.com/package/@payloadcms/email-resend) | Resend email via their REST API. This is preferred for serverless platforms such as Vercel because it is much more lightweight than the nodemailer adapter. | ## Nodemailer Configuration | Option | Description | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **`transport`** | The Nodemailer transport object for when you want to do it yourself, not needed when transportOptions is set | | **`transportOptions`** | An object that configures the transporter that Payload will create. For all the available options see the [Nodemailer documentation](https://nodemailer.com) or see the examples below | ## Use SMTP Simple Mail Transfer Protocol (SMTP) options can be passed in using the `transportOptions` object on the `email` options. See the [Nodemailer SMTP documentation](https://nodemailer.com/smtp/) for more information, including details on when `secure` should and should not be set to `true`. **Example email options using SMTP:** ```ts import { buildConfig } from 'payload' import { nodemailerAdapter } from '@payloadcms/email-nodemailer' export default buildConfig({ email: nodemailerAdapter({ defaultFromAddress: 'info@payloadcms.com', defaultFromName: 'Payload', // Nodemailer transportOptions transportOptions: { host: process.env.SMTP_HOST, port: 587, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, }, }), }) ``` **Example email options using nodemailer.createTransport:** ```ts import { buildConfig } from 'payload' import { nodemailerAdapter } from '@payloadcms/email-nodemailer' import nodemailer from 'nodemailer' export default buildConfig({ email: nodemailerAdapter({ defaultFromAddress: 'info@payloadcms.com', defaultFromName: 'Payload', // Any Nodemailer transport can be used transport: nodemailer.createTransport({ host: process.env.SMTP_HOST, port: 587, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS, }, }), }), }) ``` **Custom Transport:** You also have the ability to bring your own nodemailer transport. This is an example of using the SendGrid nodemailer transport. ```ts import { buildConfig } from 'payload' import { nodemailerAdapter } from '@payloadcms/email-nodemailer' import nodemailerSendgrid from 'nodemailer-sendgrid' export default buildConfig({ email: nodemailerAdapter({ defaultFromAddress: 'info@payloadcms.com', defaultFromName: 'Payload', transportOptions: nodemailerSendgrid({ apiKey: process.env.SENDGRID_API_KEY, }), }), }) ``` During development, if you pass nothing to `nodemailerAdapter`, it will use the [ethereal.email](https://ethereal.email) service. This will log the ethereal.email details to console on startup. ```ts import { nodemailerAdapter } from '@payloadcms/email-nodemailer' export default buildConfig({ email: nodemailerAdapter(), }) ``` ## Resend Configuration The Resend adapter requires an API key to be passed in the options. This can be found in the Resend dashboard. This is the preferred package if you are deploying on Vercel because this is much more lightweight than the Nodemailer adapter. | Option | Description | | ------ | ----------------------------------- | | apiKey | The API key for the Resend service. | ```ts import { buildConfig } from 'payload' import { resendAdapter } from '@payloadcms/email-resend' export default buildConfig({ email: resendAdapter({ defaultFromAddress: 'dev@payloadcms.com', defaultFromName: 'Payload CMS', apiKey: process.env.RESEND_API_KEY || '', }), }) ``` ## Sending Mail With a working transport you can call it anywhere you have access to Payload by calling `payload.sendEmail(message)`. The `message` will contain the `to`, `subject` and `html` or `text` for the email being sent. Other options are also available and can be seen in the sendEmail args. Support for these will depend on the adapter being used. ```ts // Example of sending an email const email = await payload.sendEmail({ to: 'test@example.com', subject: 'This is a test email', text: 'This is my message body', }) ``` ## Using multiple mail providers Payload supports the use of a single transporter of email, but there is nothing stopping you from having more. Consider a use case where sending bulk email is handled differently than transactional email and could be done using a [hook](../hooks/overview). # Jobs Queue Source: https://payloadcms.com/docs/jobs-queue/overview Payload's Jobs Queue gives you a simple, yet powerful way to offload large or future tasks to separate compute resources which is a very powerful feature of many application frameworks. ### Example use cases **Non-blocking workloads** You might need to perform some complex, slow-running logic in a Payload [Hook](../hooks/overview) but you don't want that hook to "block" or slow down the response returned from the Payload API. Instead of running this logic directly in a hook, which would block your API response from returning until the expensive work is completed, you can queue a new Job and let it run at a later date. Examples: - Create vector embeddings from your documents, and keep them in sync as your documents change - Send data to a third-party API on document change - Trigger emails based on customer actions **Scheduled actions** If you need to schedule an action to be run or processed at a certain date in the future, you can queue a job with the `waitUntil` property set. This will make it so the job is not "picked up" until that `waitUntil` date has passed. Examples: - Process scheduled posts, where the scheduled date is at a time set in the future - Unpublish posts at a given time - Send a reminder email to a customer after X days of signing up for a trial **Periodic sync or similar scheduled action** Some applications may need to perform a regularly scheduled operation of some type. Jobs are perfect for this because you can execute their logic using `cron`, scheduled nightly, every twelve hours, or some similar time period. Examples: - You'd like to send emails to all customers on a regular, scheduled basis - Periodically trigger a rebuild of your frontend at night - Sync resources to or from a third-party API during non-peak times **Offloading complex operations** You may run into the need to perform computationally expensive functions which might slow down your main Payload API server(s). The Jobs Queue allows you to offload these tasks to a separate compute resource rather than slowing down the server(s) that run your Payload APIs. With Payload Task definitions, you can even keep large dependencies out of your main Next.js bundle by dynamically importing them only when they are used. This keeps your Next.js + Payload compilation fast and ensures large dependencies do not get bundled into your Payload production build. Examples: - You need to create (and then keep in sync) vector embeddings of your documents as they change, but you use an open source model to generate embeddings - You have a PDF generator that needs to dynamically build and send PDF versions of documents to customers - You need to use a headless browser to perform some type of logic - You need to perform a series of actions, each of which depends on a prior action and should be run in as "durable" of a fashion as possible ### How it works There are a few concepts that you should become familiarized with before using Payload's Jobs Queue. We recommend learning what each of these does in order to fully understand how to leverage the power of Payload's Jobs Queue. 1. [Tasks](../jobs-queue/tasks) 1. [Workflows](../jobs-queue/workflows) 1. [Jobs](../jobs-queue/jobs) 1. [Queues](../jobs-queue/queues) All of these pieces work together in order to allow you to offload long-running, expensive, or future scheduled work from your main APIs. Here's a quick overview: - A Task is a specific function that performs business logic - Workflows are groupings of specific tasks which should be run in-order, and can be retried from a specific point of failure - A Job is an instance of a single task or workflow which will be executed - A Queue is a way to segment your jobs into different "groups" - for example, some to run nightly, and others to run every 10 minutes ### Visualizing Jobs in the Admin UI By default, the internal `payload-jobs` collection is hidden from the Payload Admin Panel. To make this collection visible for debugging or inspection purposes, you can override its configuration using `jobsCollectionOverrides`. ```ts import { buildConfig } from 'payload' export default buildConfig({ // ... other config jobs: { // ... other job settings jobsCollectionOverrides: ({ defaultJobsCollection }) => { if (!defaultJobsCollection.admin) { defaultJobsCollection.admin = {} } defaultJobsCollection.admin.hidden = false return defaultJobsCollection }, }, }) ``` # Tasks Source: https://payloadcms.com/docs/jobs-queue/tasks A **"Task"** is a function definition that performs business logic and whose input and output are both strongly typed. You can register Tasks on the Payload config, and then create [Jobs](../jobs-queue/jobs) or [Workflows](../jobs-queue/workflows) that use them. Think of Tasks like tidy, isolated "functions that do one specific thing". Payload Tasks can be configured to automatically retried if they fail, which makes them valuable for "durable" workflows like AI applications where LLMs can return non-deterministic results, and might need to be retried. Tasks can either be defined within the `jobs.tasks` array in your Payload config, or they can be defined inline within a workflow. ### Defining tasks in the config Simply add a task to the `jobs.tasks` array in your Payload config. A task consists of the following fields: | Option | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `slug` | Define a slug-based name for this job. This slug needs to be unique among both tasks and workflows. | | `handler` | The function that should be responsible for running the job. You can either pass a string-based path to the job function file, or the job function itself. If you are using large dependencies within your job, you might prefer to pass the string path because that will avoid bundling large dependencies in your Next.js app. Passing a string path is an advanced feature that may require a sophisticated build pipeline in order to work. | | `inputSchema` | Define the input field schema - Payload will generate a type for this schema. | | `interfaceName` | You can use interfaceName to change the name of the interface that is generated for this task. By default, this is "Task" + the capitalized task slug. | | `outputSchema` | Define the output field schema - Payload will generate a type for this schema. | | `label` | Define a human-friendly label for this task. | | `onFail` | Function to be executed if the task fails. | | `onSuccess` | Function to be executed if the task succeeds. | | `retries` | Specify the number of times that this step should be retried if it fails. If this is undefined, the task will either inherit the retries from the workflow or have no retries. If this is 0, the task will not be retried. By default, this is undefined. | The logic for the Task is defined in the `handler` - which can be defined as a function, or a path to a function. The `handler` will run once a worker picks up a Job that includes this task. It should return an object with an `output` key, which should contain the output of the task as you've defined. Example: ```ts export default buildConfig({ // ... jobs: { tasks: [ { // Configure this task to automatically retry // up to two times retries: 2, // This is a unique identifier for the task slug: 'createPost', // These are the arguments that your Task will accept inputSchema: [ { name: 'title', type: 'text', required: true, }, ], // These are the properties that the function should output outputSchema: [ { name: 'postID', type: 'text', required: true, }, ], // This is the function that is run when the task is invoked handler: async ({ input, job, req }) => { const newPost = await req.payload.create({ collection: 'post', req, data: { title: input.title, }, }) return { output: { postID: newPost.id, }, } }, } as TaskConfig<'createPost'>, ], }, }) ``` In addition to defining handlers as functions directly provided to your Payload config, you can also pass an _absolute path_ to where the handler is defined. If your task has large dependencies, and you are planning on executing your jobs in a separate process that has access to the filesystem, this could be a handy way to make sure that your Payload + Next.js app remains quick to compile and has minimal dependencies. Keep in mind that this is an advanced feature that may require a sophisticated build pipeline, especially when using it in production or within Next.js, e.g. by calling opening the `/api/payload-jobs/run` endpoint. You will have to transpile the handler files separately and ensure they are available in the same location when the job is run. If you're using an endpoint to execute your jobs, it's recommended to define your handlers as functions directly in your Payload Config, or use import paths handlers outside of Next.js. In general, this is an advanced use case. Here's how this would look: `payload.config.ts:` ```ts import { fileURLToPath } from 'node:url' import path from 'path' const filename = fileURLToPath(import.meta.url) const dirname = path.dirname(filename) export default buildConfig({ jobs: { tasks: [ { // ... // The #createPostHandler is a named export within the `createPost.ts` file handler: path.resolve(dirname, 'src/tasks/createPost.ts') + '#createPostHandler', }, ], }, }) ``` Then, the `createPost` file itself: `src/tasks/createPost.ts:` ```ts import type { TaskHandler } from 'payload' export const createPostHandler: TaskHandler<'createPost'> = async ({ input, job, req, }) => { const newPost = await req.payload.create({ collection: 'post', req, data: { title: input.title, }, }) return { output: { postID: newPost.id, }, } } ``` ### Configuring task restoration By default, if a task has passed previously and a workflow is re-run, the task will not be re-run. Instead, the output from the previous task run will be returned. This is to prevent unnecessary re-runs of tasks that have already passed. You can configure this behavior through the `retries.shouldRestore` property. This property accepts a boolean or a function. If `shouldRestore` is set to true, the task will only be re-run if it previously failed. This is the default behavior. If `shouldRestore` this is set to false, the task will be re-run even if it previously succeeded, ignoring the maximum number of retries. If `shouldRestore` is a function, the return value of the function will determine whether the task should be re-run. This can be used for more complex restore logic, e.g you may want to re-run a task up to X amount of times and then restore it for consecutive runs, or only re-run a task if the input has changed. Example: ```ts export default buildConfig({ // ... jobs: { tasks: [ { slug: 'myTask', retries: { shouldRestore: false, }, // ... } as TaskConfig<'myTask'>, ], }, }) ``` Example - determine whether a task should be restored based on the input data: ```ts export default buildConfig({ // ... jobs: { tasks: [ { slug: 'myTask', inputSchema: [ { name: 'someDate', type: 'date', required: true, }, ], retries: { shouldRestore: ({ input }) => { if (new Date(input.someDate) > new Date()) { return false } return true }, }, // ... } as TaskConfig<'myTask'>, ], }, }) ``` ## Nested tasks You can run sub-tasks within an existing task, by using the `tasks` or `inlineTask` arguments passed to the task `handler` function: ```ts export default buildConfig({ // ... jobs: { // It is recommended to set `addParentToTaskLog` to `true` when using nested tasks, so that the parent task is included in the task log // This allows for better observability and debugging of the task execution addParentToTaskLog: true, tasks: [ { slug: 'parentTask', inputSchema: [ { name: 'text', type: 'text', }, ], handler: async ({ input, req, tasks, inlineTask }) => { await inlineTask('Sub Task 1', { task: () => { // Do something return { output: {}, } }, }) await tasks.CreateSimple('Sub Task 2', { input: { message: 'hello' }, }) return { output: {}, } }, } as TaskConfig<'parentTask'>, ], }, }) ``` # Workflows Source: https://payloadcms.com/docs/jobs-queue/workflows A **"Workflow"** is an optional way to *combine multiple tasks together* in a way that can be gracefully retried from the point of failure. They're most helpful when you have multiple tasks in a row, and you want to configure each task to be able to be retried if they fail. If a task within a workflow fails, the Workflow will automatically "pick back up" on the task where it failed and **not re-execute any prior tasks that have already been executed**. #### Defining a workflow The most important aspect of a Workflow is the `handler`, where you can declare when and how the tasks should run by simply calling the `runTask` function. If any task within the workflow, fails, the entire `handler` function will re-run. However, importantly, tasks that have successfully been completed will simply re-return the cached and saved output without running again. The Workflow will pick back up where it failed and only task from the failure point onward will be re-executed. To define a JS-based workflow, simply add a workflow to the `jobs.workflows` array in your Payload config. A workflow consists of the following fields: | Option | Description | | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `slug` | Define a slug-based name for this workflow. This slug needs to be unique among both tasks and workflows. | | `handler` | The function that should be responsible for running the workflow. You can either pass a string-based path to the workflow function file, or workflow job function itself. If you are using large dependencies within your workflow, you might prefer to pass the string path because that will avoid bundling large dependencies in your Next.js app. Passing a string path is an advanced feature that may require a sophisticated build pipeline in order to work. | | `inputSchema` | Define the input field schema - Payload will generate a type for this schema. | | `interfaceName` | You can use interfaceName to change the name of the interface that is generated for this workflow. By default, this is "Workflow" + the capitalized workflow slug. | | `label` | Define a human-friendly label for this workflow. | | `queue` | Optionally, define the queue name that this workflow should be tied to. Defaults to "default". | | `retries` | You can define `retries` on the workflow level, which will enforce that the workflow can only fail up to that number of retries. If a task does not have retries specified, it will inherit the retry count as specified on the workflow. You can specify `0` as `workflow` retries, which will disregard all `task` retry specifications and fail the entire workflow on any task failure. You can leave `workflow` retries as undefined, in which case, the workflow will respect what each task dictates as their own retry count. By default this is undefined, meaning workflows retries are defined by their tasks | Example: ```ts export default buildConfig({ // ... jobs: { tasks: [ // ... ] workflows: [ { slug: 'createPostAndUpdate', // The arguments that the workflow will accept inputSchema: [ { name: 'title', type: 'text', required: true, }, ], // The handler that defines the "control flow" of the workflow // Notice how it uses the `tasks` argument to execute your predefined tasks. // These are strongly typed! handler: async ({ job, tasks }) => { // This workflow first runs a task called `createPost`. // You need to define a unique ID for this task invocation // that will always be the same if this workflow fails // and is re-executed in the future. Here, we hard-code it to '1' const output = await tasks.createPost('1', { input: { title: job.input.title, }, }) // Once the prior task completes, it will run a task // called `updatePost` await tasks.updatePost('2', { input: { post: job.taskStatus.createPost['1'].output.postID, // or output.postID title: job.input.title + '2', }, }) }, } as WorkflowConfig<'updatePost'> ] } }) ``` #### Running tasks inline In the above example, our workflow was executing tasks that we already had defined in our Payload config. But, you can also run tasks without predefining them. To do this, you can use the `inlineTask` function. The drawbacks of this approach are that tasks cannot be re-used across workflows as easily, and the **task data stored in the job** will not be typed. In the following example, the inline task data will be stored on the job under `job.taskStatus.inline['2']` but completely untyped, as types for dynamic tasks like these cannot be generated beforehand. Example: ```ts export default buildConfig({ // ... jobs: { tasks: [ // ... ] workflows: [ { slug: 'createPostAndUpdate', inputSchema: [ { name: 'title', type: 'text', required: true, }, ], handler: async ({ job, tasks, inlineTask }) => { // Here, we run a predefined task. // The `createPost` handler arguments and return type // are both strongly typed const output = await tasks.createPost('1', { input: { title: job.input.title, }, }) // Here, this task is not defined in the Payload config // and is "inline". Its output will be stored on the Job in the database // however its arguments will be untyped. const { newPost } = await inlineTask('2', { task: async ({ req }) => { const newPost = await req.payload.update({ collection: 'post', id: '2', req, retries: 3, data: { title: 'updated!', }, }) return { output: { newPost }, } }, }) }, } as WorkflowConfig<'updatePost'> ] } }) ``` # Jobs Source: https://payloadcms.com/docs/jobs-queue/jobs Now that we have covered Tasks and Workflows, we can tie them together with a concept called a Job. Whereas you define Workflows and Tasks, which control your business logic, a **Job** is an individual instance of either a Task or a Workflow which contains many tasks. For example, let's say we have a Workflow or Task that describes the logic to sync information from Payload to a third-party system. This is how you'd declare how to sync that info, but it wouldn't do anything on its own. In order to run that task or workflow, you'd create a Job that references the corresponding Task or Workflow. Jobs are stored in the Payload database in the `payload-jobs` collection, and you can decide to keep a running list of all jobs, or configure Payload to delete the job when it has been successfully executed. #### Queuing a new job In order to queue a job, you can use the `payload.jobs.queue` function. Here's how you'd queue a new Job, which will run a `createPostAndUpdate` workflow: ```ts const createdJob = await payload.jobs.queue({ // Pass the name of the workflow workflow: 'createPostAndUpdate', // The input type will be automatically typed // according to the input you've defined for this workflow input: { title: 'my title', }, }) ``` In addition to being able to queue new Jobs based on Workflows, you can also queue a job for a single Task: ```ts const createdJob = await payload.jobs.queue({ task: 'createPost', input: { title: 'my title', }, }) ``` #### Cancelling Jobs Payload allows you to cancel jobs that are either queued or currently running. When cancelling a running job, the current task will finish executing, but no subsequent tasks will run. This happens because the job checks its cancellation status between tasks. ##### Cancel a Single Job To cancel a specific job, use the `payload.jobs.cancelByID` method with the job's ID: ```ts await payload.jobs.cancelByID({ id: createdJob.id, }) ``` ##### Cancel Multiple Jobs To cancel multiple jobs at once, use the `payload.jobs.cancel` method with a `Where` query: ```ts await payload.jobs.cancel({ where: { workflowSlug: { equals: 'createPost', }, }, }) ``` # Queues Source: https://payloadcms.com/docs/jobs-queue/queues Queues are the final aspect of Payload's Jobs Queue and deal with how to _run your jobs_. Up to this point, all we've covered is how to queue up jobs to run, but so far, we aren't actually running any jobs. A **Queue** is a grouping of jobs that should be executed in order of when they were added. When you go to run jobs, Payload will query for any jobs that are added to the queue and then run them. By default, all queued jobs are added to the `default` queue. **But, imagine if you wanted to have some jobs that run nightly, and other jobs which should run every five minutes.** By specifying the `queue` name when you queue a new job using `payload.jobs.queue()`, you can queue certain jobs with `queue: 'nightly'`, and other jobs can be left as the default queue. Then, you could configure two different runner strategies: 1. A `cron` that runs nightly, querying for jobs added to the `nightly` queue 2. Another that runs any jobs that were added to the `default` queue every ~5 minutes or so ## Executing jobs As mentioned above, you can queue jobs, but the jobs won't run unless a worker picks up your jobs and runs them. This can be done in four ways: ### Cron jobs The `jobs.autoRun` property allows you to configure cron jobs that automatically run queued jobs at specified intervals. Note that this does not _queue_ new jobs - only _runs_ jobs that are already in the specified queue. **Example**: ```ts export default buildConfig({ // Other configurations... jobs: { tasks: [ // your tasks here ], // autoRun can optionally be a function that receives `payload` as an argument autoRun: [ { cron: '0 * * * *', // every hour at minute 0 limit: 100, // limit jobs to process each run queue: 'hourly', // name of the queue }, // add as many cron jobs as you want ], shouldAutoRun: async (payload) => { // Tell Payload if it should run jobs or not. This function is optional and will return true by default. // This function will be invoked each time Payload goes to pick up and run jobs. // If this function ever returns false, the cron schedule will be stopped. return true }, }, }) ``` autoRun is intended for use with a dedicated server that is always running, and should not be used on serverless platforms like Vercel. ### Endpoint You can execute jobs by making a fetch request to the `/api/payload-jobs/run` endpoint: ```ts // Here, we're saying we want to run only 100 jobs for this invocation // and we want to pull jobs from the `nightly` queue: await fetch('/api/payload-jobs/run?limit=100&queue=nightly', { method: 'GET', headers: { Authorization: `Bearer ${token}`, }, }) ``` This endpoint is automatically mounted for you and is helpful in conjunction with serverless platforms like Vercel, where you might want to use Vercel Cron to invoke a serverless function that executes your jobs. **Query Parameters:** - `limit`: The maximum number of jobs to run in this invocation (default: 10). - `queue`: The name of the queue to run jobs from. If not specified, jobs will be run from the `default` queue. - `allQueues`: If set to `true`, all jobs from all queues will be run. This will ignore the `queue` parameter. **Vercel Cron Example** If you're deploying on Vercel, you can add a `vercel.json` file in the root of your project that configures Vercel Cron to invoke the `run` endpoint on a cron schedule. Here's an example of what this file will look like: ```json { "crons": [ { "path": "/api/payload-jobs/run", "schedule": "*/5 * * * *" } ] } ``` The configuration above schedules the endpoint `/api/payload-jobs/run` to be invoked every 5 minutes. The last step will be to secure your `run` endpoint so that only the proper users can invoke the runner. To do this, you can set an environment variable on your Vercel project called `CRON_SECRET`, which should be a random string—ideally 16 characters or longer. Then, you can modify the `access` function for running jobs by ensuring that only Vercel can invoke your runner. ```ts export default buildConfig({ // Other configurations... jobs: { access: { run: ({ req }: { req: PayloadRequest }): boolean => { // Allow logged in users to execute this endpoint (default) if (req.user) return true // If there is no logged in user, then check // for the Vercel Cron secret to be present as an // Authorization header: const authHeader = req.headers.get('authorization') return authHeader === `Bearer ${process.env.CRON_SECRET}` }, }, // Other job configurations... }, }) ``` This works because Vercel automatically makes the `CRON_SECRET` environment variable available to the endpoint as the `Authorization` header when triggered by the Vercel Cron, ensuring that the jobs can be run securely. After the project is deployed to Vercel, the Vercel Cron job will automatically trigger the `/api/payload-jobs/run` endpoint in the specified schedule, running the queued jobs in the background. ### Local API If you want to process jobs programmatically from your server-side code, you can use the Local API: **Run all jobs:** ```ts // Run all jobs from the `default` queue - default limit is 10 const results = await payload.jobs.run() // You can customize the queue name and limit by passing them as arguments: await payload.jobs.run({ queue: 'nightly', limit: 100 }) // Run all jobs from all queues: await payload.jobs.run({ allQueues: true }) // You can provide a where clause to filter the jobs that should be run: await payload.jobs.run({ where: { 'input.message': { equals: 'secret' } }, }) ``` **Run a single job:** ```ts const results = await payload.jobs.runByID({ id: myJobID, }) ``` ### Bin script Finally, you can process jobs via the bin script that comes with Payload out of the box. By default, this script will run jobs from the `default` queue, with a limit of 10 jobs per invocation: ```sh pnpm payload jobs:run ``` You can override the default queue and limit by passing the `--queue` and `--limit` flags: ```sh pnpm payload jobs:run --queue myQueue --limit 15 ``` If you want to run all jobs from all queues, you can pass the `--all-queues` flag: ```sh pnpm payload jobs:run --all-queues ``` In addition, the bin script allows you to pass a `--cron` flag to the `jobs:run` command to run the jobs on a scheduled, cron basis: ```sh pnpm payload jobs:run --cron "*/5 * * * *" ``` You can also pass `--handle-schedules` flag to the `jobs:run` command to make it schedule jobs according to configured schedules: ```sh pnpm payload jobs:run --cron "*/5 * * * *" --queue myQueue --handle-schedules # This will both schedule jobs according to the configuration and run them ``` ## Processing Order By default, jobs are processed first in, first out (FIFO). This means that the first job added to the queue will be the first one processed. However, you can also configure the order in which jobs are processed. ### Jobs Configuration You can configure the order in which jobs are processed in the jobs configuration by passing the `processingOrder` property. This mimics the Payload [sort](../queries/sort) property that's used for functionality such as `payload.find()`. ```ts export default buildConfig({ // Other configurations... jobs: { tasks: [ // your tasks here ], processingOrder: '-createdAt', // Process jobs in reverse order of creation = LIFO }, }) ``` You can also set this on a queue-by-queue basis: ```ts export default buildConfig({ // Other configurations... jobs: { tasks: [ // your tasks here ], processingOrder: { default: 'createdAt', // FIFO queues: { nightly: '-createdAt', // LIFO myQueue: '-createdAt', // LIFO }, }, }, }) ``` If you need even more control over the processing order, you can pass a function that returns the processing order - this function will be called every time a queue starts processing jobs. ```ts export default buildConfig({ // Other configurations... jobs: { tasks: [ // your tasks here ], processingOrder: ({ queue }) => { if (queue === 'myQueue') { return '-createdAt' // LIFO } return 'createdAt' // FIFO }, }, }) ``` ### Local API You can configure the order in which jobs are processed in the `payload.jobs.queue` method by passing the `processingOrder` property. ```ts const createdJob = await payload.jobs.queue({ workflow: 'createPostAndUpdate', input: { title: 'my title', }, processingOrder: '-createdAt', // Process jobs in reverse order of creation = LIFO }) ``` # Job Schedules Source: https://payloadcms.com/docs/jobs-queue/schedules Payload's `schedule` property lets you enqueue Jobs regularly according to a cron schedule - daily, weekly, hourly, or any custom interval. This is ideal for tasks or workflows that must repeat automatically and without manual intervention. Scheduling Jobs differs significantly from running them: - **Queueing**: Scheduling only creates (enqueues) the Job according to your cron expression. It does not immediately execute any business logic. - **Running**: Execution happens separately through your Jobs runner - such as autorun, or manual invocation using `payload.jobs.run()` or the `payload-jobs/run` endpoint. Use the `schedule` property specifically when you have recurring tasks or workflows. To enqueue a single Job to run once in the future, use the `waitUntil` property instead. ## Example use cases **Regular emails or notifications** Send nightly digests, weekly newsletters, or hourly updates. **Batch processing during off-hours** Process analytics data or rebuild static sites during low-traffic times. **Periodic data synchronization** Regularly push or pull updates to or from external APIs. ## Handling schedules Something needs to actually trigger the scheduling of jobs (execute the scheduling lifecycle seen below). By default, the `jobs.autorun` configuration, as well as the `/api/payload-jobs/run` will also handle scheduling for the queue specified in the `autorun` configuration. You can disable this behavior by setting `disableScheduling: true` in your `autorun` configuration, or by passing `disableScheduling=true` to the `/api/payload-jobs/run` endpoint. This is useful if you want to handle scheduling manually, for example, by using a cron job or a serverless function that calls the `/api/payload-jobs/handle-schedules` endpoint or the `payload.jobs.handleSchedules()` local API method. ### Bin Scripts Payload provides a set of bin scripts that can be used to handle schedules. If you're already using the `jobs:run` bin script, you can set it to also handle schedules by passing the `--handle-schedules` flag: ```sh pnpm payload jobs:run --cron "*/5 * * * *" --queue myQueue --handle-schedules # This will both schedule jobs according to the configuration and run them ``` If you only want to handle schedules, you can use the dedicated `jobs:handle-schedules` bin script: ```sh pnpm payload jobs:handle-schedules --cron "*/5 * * * *" --queue myQueue # or --all-queues ``` ## Defining schedules on Tasks or Workflows Schedules are defined using the `schedule` property: ```ts export type ScheduleConfig = { cron: string // required, supports seconds precision queue: string // required, the queue to push Jobs onto hooks?: { // Optional hooks to customize scheduling behavior beforeSchedule?: BeforeScheduleFn afterSchedule?: AfterScheduleFn } } ``` ### Example schedule The following example demonstrates scheduling a Job to enqueue every day at midnight: ```ts import type { TaskConfig } from 'payload' export const SendDigestEmail: TaskConfig<'SendDigestEmail'> = { slug: 'SendDigestEmail', schedule: [ { cron: '0 0 * * *', // Every day at midnight queue: 'nightly', }, ], handler: async () => { await sendDigestToAllUsers() }, } ``` This configuration only queues the Job - it does not execute it immediately. To actually run the queued Job, you configure autorun in your Payload config (note that autorun should **not** be used on serverless platforms): ```ts export default buildConfig({ jobs: { autoRun: [ { cron: '* * * * *', // Runs every minute queue: 'nightly', }, ], tasks: [SendDigestEmail], }, }) ``` That way, Payload's scheduler will automatically enqueue the job into the `nightly` queue every day at midnight. The autorun configuration will check the `nightly` queue every minute and execute any Jobs that are due to run. ## Scheduling lifecycle Here's how the scheduling process operates in detail: 1. **Cron evaluation**: Payload (or your external trigger in `manual` mode) identifies which schedules are due to run. To do that, it will read the `payload-jobs-stats` global which contains information about the last time each scheduled task or workflow was run. 2. **BeforeSchedule hook**: - The default beforeSchedule hook checks how many active or runnable jobs of the same type that have been queued by the scheduling system currently exist. If such a job exists, it will skip scheduling a new one. - You can provide your own `beforeSchedule` hook to customize this behavior. For example, you might want to allow multiple overlapping Jobs or dynamically set the Job input data. 3. **Enqueue Job**: Payload queues up a new job. This job will have `waitUntil` set to the next scheduled time based on the cron expression. 4. **AfterSchedule hook**: - The default afterSchedule hook updates the `payload-jobs-stats` global metadata with the last scheduled time for the Job. - You can provide your own afterSchedule hook to it for custom logging, metrics, or other post-scheduling actions. ## Customizing concurrency and input (Advanced) You may want more control over concurrency or dynamically set Job inputs at scheduling time. For instance, allowing multiple overlapping Jobs to be scheduled, even if a previously scheduled job has not completed yet, or preparing dynamic data to pass to your Job handler: ```ts import { countRunnableOrActiveJobsForQueue } from 'payload' schedule: [ { cron: '* * * * *', // every minute queue: 'reports', hooks: { beforeSchedule: async ({ queueable, req }) => { const runnableOrActiveJobsForQueue = await countRunnableOrActiveJobsForQueue({ queue: queueable.scheduleConfig.queue, req, taskSlug: queueable.taskConfig?.slug, workflowSlug: queueable.workflowConfig?.slug, onlyScheduled: true, }) // Allow up to 3 simultaneous scheduled jobs and set dynamic input return { shouldSchedule: runnableOrActiveJobsForQueue < 3, input: { text: 'Hi there' }, } }, }, }, ] ``` This allows fine-grained control over how many Jobs can run simultaneously and provides dynamically computed input values each time a Job is scheduled. ## Scheduling in serverless environments On serverless platforms, scheduling must be triggered externally since Payload does not automatically run cron schedules in ephemeral environments. You have two main ways to trigger scheduling manually: - **Invoke via Payload's API:** `payload.jobs.handleSchedules()` - **Use the REST API endpoint:** `/api/payload-jobs/handle-schedules` - **Use the run endpoint, which also handles scheduling by default:** `GET /api/payload-jobs/run` For example, on Vercel, you can set up a Vercel Cron to regularly trigger scheduling: - **Vercel Cron Job:** Configure Vercel Cron to periodically call `GET /api/payload-jobs/handle-schedules`. If you would like to auto-run your scheduled jobs as well, you can use the `GET /api/payload-jobs/run` endpoint. Once Jobs are queued, their execution depends entirely on your configured runner setup (e.g., autorun, or manual invocation). # Query Presets Source: https://payloadcms.com/docs/query-presets/overview Query Presets allow you to save and share filters, columns, and sort orders for your [Collections](../configuration/collections). This is useful for reusing common or complex filtering patterns and/or sharing them across your team. Each Query Preset is saved as a new record in the database under the `payload-query-presets` collection. This allows for an endless number of preset configurations, where the users of your app define the presets that are most useful to them, rather than being hard coded into the Payload Config. Within the [Admin Panel](../admin/overview), Query Presets are applied to the List View. When enabled, new controls are displayed for users to manage presets. Once saved, these presets can be loaded up at any time and optionally shared with others. To enable Query Presets on a Collection, use the `enableQueryPresets` property in your [Collection Config](../configuration/collections): ```ts import type { CollectionConfig } from 'payload' export const MyCollection: CollectionConfig = { // ... // highlight-start enableQueryPresets: true, // highlight-end } ``` ## Config Options While not required, you may want to customize the behavior of Query Presets to suit your needs, such as add custom labels or access control rules. Settings for Query Presets are managed on the `queryPresets` property at the root of your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... // highlight-start queryPresets: { // ... }, // highlight-end }) ``` The following options are available for Query Presets: | Option | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `access` | Used to define custom collection-level access control that applies to all presets. [More details](#access-control). | | `filterConstraints` | Used to define which constraints are available to users when managing presets. [More details](#constraint-access-control). | | `constraints` | Used to define custom document-level access control that apply to individual presets. [More details](#document-access-control). | | `labels` | Custom labels to use for the Query Presets collection. | ## Access Control Query Presets are subject to the same [Access Control](../access-control/overview) as the rest of Payload. This means you can use the same patterns you are already familiar with to control who can read, update, and delete presets. Access Control for Query Presets can be customized in two ways: 1. [Collection Access Control](#collection-access-control): Applies to all presets. These rules are not controllable by the user and are statically defined in the config. 2. [Document Access Control](#document-access-control): Applies to each individual preset. These rules are controllable by the user and are dynamically defined on each record in the database. ### Collection Access Control Collection-level access control applies to _all_ presets within the Query Presets collection. Users cannot control these rules, they are written statically in your config. To add Collection Access Control, use the `queryPresets.access` property in your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... queryPresets: { // ... // highlight-start access: { read: ({ req: { user } }) => user ? user?.roles?.some((role) => role === 'admin') : false, update: ({ req: { user } }) => user ? user?.roles?.some((role) => role === 'admin') : false, }, // highlight-end }, }) ``` This example restricts all Query Presets to users with the role of `admin`. **Note:** Custom access control will override the defaults on this collection, including the requirement for a user to be authenticated. Be sure to include any necessary checks in your custom rules unless you intend on making these publicly accessible. ### Document Access Control You can also define access control rules that apply to each specific preset. Users have the ability to define and modify these rules on the fly as they manage presets. These are saved dynamically in the database on each record. When a user manages a preset, document-level access control options will be available to them in the Admin Panel for each operation. By default, Payload provides a set of sensible defaults for all Query Presets, but you can customize these rules to suit your needs: - **Only Me**: Only the user who created the preset can read, update, and delete it. - **Everyone**: All users can read, update, and delete the preset. - **Specific Users**: Only select users can read, update, and delete the preset. #### Custom Access Control You can augment the default access control rules with your own custom rules. This can be useful for creating more complex access control patterns that the defaults don't provide, such as for RBAC. Adding custom access control rules requires: 1. A label to display in the dropdown 2. A set of fields to conditionally render when that option is selected 3. A function that returns the access control rules for that option To do this, use the `queryPresets.constraints` property in your [Payload Config](../configuration/overview). ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... queryPresets: { // ... // highlight-start constraints: { read: [ { label: 'Specific Roles', value: 'specificRoles', fields: [ { name: 'roles', type: 'select', hasMany: true, options: [ { label: 'Admin', value: 'admin' }, { label: 'User', value: 'user' }, ], }, ], access: ({ req: { user } }) => ({ 'access.read.roles': { in: [user?.roles], }, }), }, ], }, // highlight-end }, }) ``` In this example, we've added a new option called `Specific Roles` that allows users to select from a list of roles. When this option is selected, the user will be prompted to select one or more roles from a list of options. The access control rule for this option is that the user operating on the preset must have one of the selected roles. **Note:** Payload places your custom fields into the `access[operation]` field group, so your rules will need to reflect this. The following options are available for each constraint: | Option | Description | | -------- | ------------------------------------------------------------------------ | | `label` | The label to display in the dropdown for this constraint. | | `value` | The value to store in the database when this constraint is selected. | | `fields` | An array of fields to render when this constraint is selected. | | `access` | A function that determines the access control rules for this constraint. | ### Constraint Access Control Used to dynamically filter which constraints are available based on the current user, document data, or other criteria. Some examples of this might include: - Ensuring that only "admins" are allowed to make a preset available to "everyone" - Preventing the "onlyMe" option from being selected based on a hypothetical "disablePrivatePresets" checkbox When a user lacks the permission to set a constraint, the option will either be hidden from them, or disabled if it is already saved to that preset. To do this, you can use the `filterConstraints` property in your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... queryPresets: { // ... // highlight-start filterConstraints: ({ req, options }) => !req.user?.roles?.includes('admin') ? options.filter( (option) => (typeof option === 'string' ? option : option.value) !== 'everyone', ) : options, // highlight-end }, }) ``` The `filterConstraints` function receives the same arguments as [`filterOptions`](../fields/select#filteroptions) in the [Select field](../fields/select). # Trash Source: https://payloadcms.com/docs/trash/overview Trash (also known as soft delete) allows documents to be marked as deleted without being permanently removed. When enabled on a collection, deleted documents will receive a `deletedAt` timestamp, making it possible to restore them later, view them in a dedicated Trash view, or permanently delete them. Soft delete is a safer way to manage content lifecycle, giving editors a chance to review and recover documents that may have been deleted by mistake. **Note:** The Trash feature is currently in beta and may be subject to change in minor version updates. ## Collection Configuration To enable soft deleting for a collection, set the `trash` property to `true`: ```ts import type { CollectionConfig } from 'payload' export const Posts: CollectionConfig = { slug: 'posts', trash: true, fields: [ { name: 'title', type: 'text', }, // other fields... ], } ``` When enabled, Payload automatically injects a deletedAt field into the collection's schema. This timestamp is set when a document is soft-deleted, and cleared when the document is restored. ## Admin Panel behavior Once `trash` is enabled, the Admin Panel provides a dedicated Trash view for each collection: - A new route is added at `/collections/:collectionSlug/trash` - The `Trash` view shows all documents that have a `deletedAt` timestamp From the Trash view, you can: - Use bulk actions to manage trashed documents: - **Restore** to clear the `deletedAt` timestamp and return documents to their original state - **Delete** to permanently remove selected documents - **Empty Trash** to select and permanently delete all trashed documents at once - Enter each document's **edit view**, just like in the main list view. While in the edit view of a trashed document: - All fields are in a **read-only** state - Standard document actions (e.g., Save, Publish, Restore Version) are hidden and disabled. - The available actions are **Restore** and **Permanently Delete**. - Access to the **API**, **Versions**, and **Preview** views is preserved. When deleting a document from the main collection List View, Payload will soft-delete the document by default. A checkbox in the delete confirmation modal allows users to skip the trash and permanently delete instead. ## API Support Soft deletes are fully supported across all Payload APIs: **Local**, **REST**, and **GraphQL**. The following operations respect and support the `trash` functionality: - `find` - `findByID` - `update` - `updateByID` - `delete` - `deleteByID` - `findVersions` - `findVersionByID` ### Understanding `trash` Behavior Passing `trash: true` to these operations will **include soft-deleted documents** in the query results. To return _only_ soft-deleted documents, you must combine `trash: true` with a `where` clause that checks if `deletedAt` exists. ### Examples #### Local API Return all documents including trashed: ```ts const result = await payload.find({ collection: 'posts', trash: true, }) ``` Return only trashed documents: ```ts const result = await payload.find({ collection: 'posts', trash: true, where: { deletedAt: { exists: true, }, }, }) ``` Return only non-trashed documents: ```ts const result = await payload.find({ collection: 'posts', trash: false, }) ``` #### REST Return **all** documents including trashed: ```http GET /api/posts?trash=true ``` Return **only trashed** documents: ```http GET /api/posts?trash=true&where[deletedAt][exists]=true ``` Return only non-trashed documents: ```http GET /api/posts?trash=false ``` #### GraphQL Return all documents including trashed: ```ts query { Posts(trash: true) { docs { id deletedAt } } } ``` Return only trashed documents: ```ts query { Posts( trash: true where: { deletedAt: { exists: true } } ) { docs { id deletedAt } } } ``` Return only non-trashed documents: ```ts query { Posts(trash: false) { docs { id deletedAt } } } ``` ## Access Control All trash-related actions (delete, permanent delete) respect the `delete` access control defined in your collection config. This means: - If a user is denied delete access, they cannot soft delete or permanently delete documents ## Versions and Trash When a document is soft-deleted: - It can no longer have a version **restored** until it is first restored from trash - Attempting to restore a version while the document is in trash will result in an error - This ensures consistency between the current document state and its version history However, versions are still fully **visible and accessible** from the **edit view** of a trashed document. You can view the full version history, but must restore the document itself before restoring any individual version. # Troubleshooting Source: https://payloadcms.com/docs/troubleshooting/troubleshooting ## Dependency mismatches All `payload` and `@payloadcms/*` packages must be on exactly the same version and installed only once. When two copies—or two different versions—of any of these packages (or of `react` / `react-dom`) appear in your dependency graph, you can see puzzling runtime errors. The most frequent is a broken React context: ```bash TypeError: Cannot destructure property 'config' of... ``` This happens because one package imports a hook (most commonly `useConfig`) from _version A_ while the context provider comes from _version B_. The fix is always the same: make sure every Payload-related and React package resolves to the same module. ### Confirm whether duplicates exist The first thing to do is to confirm whether duplicative dependencies do in fact exist. There are two ways to do this: 1. Using pnpm's built-in inspection tool ```bash pnpm why @payloadcms/ui ``` This prints the dependency tree and shows which versions are being installed. If you see more than one distinct version—or the same version listed under different paths—you have duplication. 2. Manual check (works with any package manager) ```bash find node_modules -name package.json \ -exec grep -H '"name": "@payloadcms/ui"' {} \; ``` Most of these hits are likely symlinks created by pnpm. Edit the matching package.json files (temporarily add a comment or change a description) to confirm whether they point to the same physical folder or to multiple copies. Perform the same two checks for react and react-dom; a second copy of React can cause identical symptoms. #### If no duplicates are found `@payloadcms/ui` intentionally contains two bundles of itself, so you may see dual paths even when everything is correct. Inside the Payload Admin UI you must import only: - `@payloadcms/ui` - `@payloadcms/ui/rsc` - `@payloadcms/ui/shared` Any other deep import such as `@payloadcms/ui/elements/Button` should **only** be used in your own frontend, outside of the Payload Admin Panel. Those deep entries are published un-bundled to help you tree-shake and ship a smaller client bundle if you only need a few components from `@payloadcms/ui`. ### Fixing depedendency issues These steps assume `pnpm`, which the Payload team recommends and uses internally. The principles apply to other package managers like npm and yarn as well. Do note that yarn 1.x is not supported by Payload. 1. Pin every critical package to an exact version In package.json remove `^` or `~` from all versions of: - `payload` - `@payloadcms/*` - `react` - `react-dom` Prefixes allow your package manager to float to a newer minor/patch release, causing mismatches. 2. Delete node_modules Old packages often linger even after you change versions or removed them from your package.json. Deleting node_modules ensures a clean slate. 3. Re-install dependencies ```bash pnpm install ``` #### If the error persists 1. Clean the global store (pnpm only) ```bash pnpm store prune ``` 2. Delete the lockfile Depending on your package manager, this could be `pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock`. Make sure you delete the lockfile **and** the node_modules folder at the same time, then run `pnpm install`. This forces a fresh, consistent resolution for all packages. It will also update all packages with dynamic versions to the latest version. While it's best practice to manage dependencies in such a way where the lockfile can easily be re-generated (often this is the easiest way to resolve dependency issues), this may break your project if you have not tested the latest versions of your dependencies. If you are using a version control system, make sure to commit your lockfile after this step. 3. Deduplicate anything that slipped through ```bash pnpm dedupe ``` **Still stuck?** - Switch to `pnpm` if you are on npm. Its symlinked store helps reducing accidental duplication. - Inspect the lockfile directly for peer-dependency violations. - Check project-level .npmrc / .pnpmfile.cjs overrides. - Run [Syncpack](https://www.npmjs.com/package/syncpack) to enforce identical versions of every `@payloadcms/*`, `react`, and `react-dom` reference. Absolute last resort: add Webpack aliases so that all imports of a given package resolve to the same path (e.g. `resolve.alias['react'] = path.resolve('./node_modules/react')`). Keep this only until you can fix the underlying version skew. ### Monorepos Another error you might see is the following or similarly related to hooks, in particular when `next` versions are mismatched: ```bash useUploadHandlers must be used within UploadHandlersProvider ``` This is a common pitfall when using a monorepo setup with multiple packages. In this case, ensure that all packages in the monorepo use the same version of `payload`, `@payloadcms/*`, `next`, `react`, and `react-dom`. You can use pnpm with workspaces to manage dependencies across packages in a monorepo effectively. Unfortunately this error becomes harder to debug inside a monorepo due to how package managers hoist dependencies as well as resolve them. If you've pinned the versions and the error persists we recommend removing `.next/`, `node_modules/` and if possible deleting the lockfile and re-generating it to ensure that all packages in the monorepo are using the same version of the dependencies mentioned above. ## "Unauthorized, you must be logged in to make this request" when attempting to log in This means that your auth cookie is not being set or accepted correctly upon logging in. To resolve check the following settings in your Payload Config: - CORS - If you are using the '\*', try to explicitly only allow certain domains instead including the one you have specified. - CSRF - Do you have this set? if so, make sure your domain is whitelisted within the csrf domains. If not, probably not the issue, but probably can't hurt to whitelist it anyway. - Cookie settings. If these are completely undefined, then that's fine. but if you have cookie domain set, or anything similar, make sure you don't have the domain misconfigured This error likely means that the auth cookie that Payload sets after logging in successfully is being rejected because of misconfiguration. To further investigate the issue: - Go to the login screen. Open your inspector. Go to the Network tab. - Log in and then find the login request that should appear in your network panel. Click the login request. - The login request should have a Set-Cookie header on the response, and the cookie should be getting set successfully. If it is not, most browsers generally have a little yellow ⚠️ symbol that you can hover over to see why the cookie was rejected. ## Using --experimental-https If you are using the `--experimental-https` flag when starting your Payload server, you may run into issues with your WebSocket connection for HMR (Hot Module Reloading) in development mode. To resolve this, you can set the `USE_HTTPS` environment variable to `true` in your `.env` file: ``` USE_HTTPS=true ``` This will ensure that the WebSocket connection uses the correct protocol (`wss://` instead of `ws://`) when HTTPS is enabled. Alternatively if more of your URL is dynamic, you can set the full URL for the WebSocket connection using the `PAYLOAD_HMR_URL_OVERRIDE` environment variable: ``` PAYLOAD_HMR_URL_OVERRIDE=wss://localhost:3000/_next/webpack-hmr ``` # TypeScript - Overview Source: https://payloadcms.com/docs/typescript/overview Payload supports TypeScript natively, and not only that, the entirety of the CMS is built with TypeScript. To get started developing with Payload and TypeScript, you can use one of Payload's built-in boilerplates in one line via `create-payload-app`: ``` npx create-payload-app@latest ``` Pick a TypeScript project type to get started easily. ## Setting up from Scratch It's also possible to set up a TypeScript project from scratch. We plan to write up a guide for exactly how—so keep an eye out for that, too. ## Using Payload's Exported Types Payload exports a number of types that you may find useful while writing your own custom functionality like [Plugins](../plugins/overview), [Hooks](../hooks/overview), [Access Control](../access-control/overview) functions, [Custom Views](../custom-components/custom-views), [GraphQL queries / mutations](../graphql/overview) or anything else. ## Config Types - [Base config](../configuration/overview#typescript) - [Collections](../configuration/collections#typescript) - [Globals](../configuration/globals#typescript) - [Fields](../fields/overview#typescript) ## Hook Types - [Collection hooks](../hooks/collections#typescript) - [Global hooks](../hooks/globals#typescript) - [Field hooks](../hooks/fields#typescript) # Generating TypeScript Interfaces Source: https://payloadcms.com/docs/typescript/generating-types While building your own custom functionality into Payload, like [Plugins](../plugins/overview), [Hooks](../hooks/overview), [Access Control](../access-control/overview) functions, [Custom Views](../custom-components/custom-views), [GraphQL queries / mutations](../graphql/overview), or anything else, you may benefit from generating your own TypeScript types dynamically from your Payload Config itself. ## Types generation script Run the following command in a Payload project to generate types based on your Payload Config: ``` payload generate:types ``` You can run this command whenever you need to regenerate your types, and then you can use these types in your Payload code directly. ## Disable declare statement By default, `generate:types` will add a `declare` statement to your types file, which automatically enables type inference within Payload. If you are using your `payload-types.ts` file in other repos, though, it might be better to disable this `declare` statement, so that you don't get any TS errors in projects that use your Payload types, but do not have Payload installed. ```ts // payload.config.ts { // ... typescript: { declare: false, // defaults to true if not set }, } ``` If you do disable the `declare` pattern, you'll need to manually add a `declare` statement to your code in order for Payload types to be recognized. Here's an example showing how to declare your types in your `payload.config.ts` file: ```ts import { Config } from './payload-types' declare module 'payload' { export interface GeneratedTypes extends Config {} } ``` ## Custom output file path You can specify where you want your types to be generated by adding a property to your Payload Config: ```ts // payload.config.ts { // ... typescript: { // defaults to: path.resolve(__dirname, './payload-types.ts') outputFile: path.resolve(__dirname, './generated-types.ts'), }, } ``` The above example places your types next to your Payload Config itself as the file `generated-types.ts`. ## Custom generated types Payload generates your types based on a JSON schema. You can extend that JSON schema, and thus the generated types, by passing a function to `typescript.schema`: ```ts // payload.config.ts { // ... typescript: { schema: [ ({ jsonSchema }) => { // Modify the JSON schema here jsonSchema.definitions.Test = { type: 'object', properties: { title: { type: 'string' }, content: { type: 'string' }, }, required: ['title', 'content'], } return jsonSchema }, ] } } // This will generate the following type in your payload-types.ts: export interface Test { title: string content: string [k: string]: unknown } ``` This function takes the existing JSON schema as an argument and returns the modified JSON schema. It can be useful for plugins that wish to generate their own types. ## Example Usage For example, let's look at the following simple Payload Config: ```ts import type { Config } from 'payload' const config: Config = { serverURL: process.env.NEXT_PUBLIC_SERVER_URL, admin: { user: 'users', }, collections: [ { slug: 'users', fields: [ { name: 'name', type: 'text', required: true, }, ], }, { slug: 'posts', admin: { useAsTitle: 'title', }, fields: [ { name: 'title', type: 'text', }, { name: 'author', type: 'relationship', relationTo: 'users', }, ], }, ], } ``` By generating types, we'll end up with a file containing the following two TypeScript interfaces: ```ts export interface User { id: string name: string email?: string resetPasswordToken?: string resetPasswordExpiration?: string loginAttempts?: number lockUntil?: string } export interface Post { id: string title?: string author?: string | User } ``` ## Custom Field Interfaces For `array`, `block`, `group` and named `tab` fields, you can generate top level reusable interfaces. The following group field config: ```ts { type: 'group', name: 'meta', interfaceName: 'SharedMeta', <-- here!! fields: [ { name: 'title', type: 'text', }, { name: 'description', type: 'text', }, ], } ``` will generate: ```ts // a top level reusable interface!! export interface SharedMeta { title?: string description?: string } // example usage inside collection interface export interface Collection1 { // ...other fields meta?: SharedMeta } ``` **Naming Collisions** Since these types are hoisted to the top level, you need to be aware that naming collisions can occur. For example, if you have a collection with the name of `Meta` and you also create a interface with the name `Meta` they will collide. It is recommended to scope your interfaces by appending the field type to the end, i.e. `MetaGroup` or similar. ## Using your types Now that your types have been generated, Payload's Local API will now be typed. It is common for users to want to use this in their frontend code, we recommend generating them with Payload and then copying the file over to your frontend codebase. This is the simplest way to get your types into your frontend codebase. ### Adding an npm script **Important** Payload needs to be able to find your config to generate your types. Payload will automatically try and locate your config, but might not always be able to find it. For example, if you are working in a `/src` directory or similar, you need to tell Payload where to find your config manually by using an environment variable. If this applies to you, you can create an npm script to make generating your types easier. To add an npm script to generate your types and show Payload where to find your config, open your `package.json` and update the `scripts` property to the following: ``` { "scripts": { "generate:types": "PAYLOAD_CONFIG_PATH=src/payload.config.ts payload generate:types", }, } ``` Now you can run `pnpm generate:types` to easily generate your types. # Plugins Source: https://payloadcms.com/docs/plugins/overview Payload Plugins take full advantage of the modularity of the [Payload Config](../configuration/overview), allowing developers to easily inject custom—sometimes complex—functionality into Payload apps from a very small touch-point. This is especially useful for sharing your work across multiple projects or with the greater Payload community. There are many [Official Plugins](#official-plugins) available that solve for some of the most common uses cases, such as the [Form Builder Plugin](./form-builder) or [SEO Plugin](./seo). There are also [Community Plugins](#community-plugins) available, maintained entirely by contributing members. To extend Payload's functionality in some other way, you can easily [build your own plugin](./build-your-own). To configure Plugins, use the `plugins` property in your [Payload Config](../configuration/overview): ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... // highlight-start plugins: [ // Add Plugins here ], // highlight-end }) ``` Writing Plugins is no more complex than writing regular JavaScript. If you know the basic concept of [callback functions](https://developer.mozilla.org/en-US/docs/Glossary/Callback_function) or how [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) works, and are up to speed with Payload concepts, then writing a plugin will be a breeze. Because we rely on a simple config-based structure, Payload Plugins simply take in an existing config and returns a _modified_ config with new fields, hooks, collections, admin views, or anything else you can think of. **Example use cases:** - Automatically sync data from a specific collection to HubSpot or a similar CRM when data is added or changes - Add password-protection functionality to certain documents - Add a full e-commerce backend to any Payload app - Add custom reporting views to Payload's Admin Panel - Encrypt specific collections' data - Add a full form builder implementation - Integrate all `upload`-enabled collections with a third-party file host like S3 or Cloudinary - Add custom endpoints or GraphQL queries / mutations with any type of custom functionality that you can think of ## Official Plugins Payload maintains a set of Official Plugins that solve for some of the common use cases. These plugins are maintained by the Payload team and its contributors and are guaranteed to be stable and up-to-date. - [Form Builder](./form-builder) - [Nested Docs](./nested-docs) - [Redirects](./redirects) - [Search](./search) - [Sentry](./sentry) - [SEO](./seo) - [Stripe](./stripe) - [Import/Export](./import-export) - [Ecommerce](../ecommerce/overview) You can also [build your own plugin](./build-your-own) to easily extend Payload's functionality in some other way. Once your plugin is ready, consider [sharing it with the community](#community-plugins). Plugins are changing every day, so be sure to check back often to see what new plugins may have been added. If you have a specific plugin you would like to see, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions). For a complete list of Official Plugins, visit the [Packages Directory](https://github.com/payloadcms/payload/tree/main/packages) of the [Payload Monorepo](https://github.com/payloadcms/payload). ## Community Plugins Community Plugins are those that are maintained entirely by outside contributors. They are a great way to share your work across the ecosystem for others to use. You can discover Community Plugins by browsing the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin). Some plugins have become so widely used that they are adopted as an [Official Plugin](#official-plugins), such as the [Lexical Plugin](https://github.com/AlessioGr/payload-plugin-lexical). If you have a plugin that you think should be an Official Plugin, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions). For maintainers building plugins for others to use, please add the `payload-plugin` topic on [GitHub](https://github.com/topics/payload-plugin) to help others find it. ## Example The base [Payload Config](../configuration/overview) allows for a `plugins` property which takes an `array` of [Plugin Configs](./build-your-own). ```ts import { buildConfig } from 'payload' import { addLastModified } from './addLastModified.ts' const config = buildConfig({ // ... // highlight-start plugins: [addLastModified], // highlight-end }) ``` Payload Plugins are executed _after_ the incoming config is validated, but before it is sanitized and has had default options merged in. After all plugins are executed, the full config with all plugins will be sanitized. Here is an example what the `addLastModified` plugin from above might look like. It adds a `lastModifiedBy` field to all Payload collections. For full details, see [how to build your own plugin](./build-your-own). ```ts import { Config, Plugin } from 'payload' export const addLastModified: Plugin = (incomingConfig: Config): Config => { // Find all incoming auth-enabled collections // so we can create a lastModifiedBy relationship field // to all auth collections const authEnabledCollections = incomingConfig.collections.filter( (collection) => Boolean(collection.auth), ) // Spread the existing config const config: Config = { ...incomingConfig, collections: incomingConfig.collections.map((collection) => { // Spread each item that we are modifying, // and add our new field - complete with // hooks and proper admin UI config return { ...collection, fields: [ ...collection.fields, { name: 'lastModifiedBy', type: 'relationship', relationTo: authEnabledCollections.map(({ slug }) => slug), hooks: { beforeChange: [ ({ req }) => ({ value: req?.user?.id, relationTo: req?.user?.collection, }), ], }, admin: { position: 'sidebar', readOnly: true, }, }, ], } }), } return config } ``` **Reminder:** See [how to build your own plugin](./build-your-own) for a more in-depth explication on how create your own Payload Plugin. # Building Your Own Plugin Source: https://payloadcms.com/docs/plugins/build-your-own Building your own [Payload Plugin](./overview) is easy, and if you're already familiar with Payload then you'll have everything you need to get started. You can either start from scratch or use the [Plugin Template](#plugin-template) to get up and running quickly. To use the template, run `npx create-payload-app@latest --template plugin` directly in your terminal. Our plugin template includes everything you need to build a full life-cycle plugin: - Example files and functions for extending the Payload Config - A local dev environment to develop the plugin - Test suite with integrated GitHub workflow By abstracting your code into a plugin, you'll be able to reuse your feature across multiple projects and make it available for other developers to use. ## Plugins Recap Here is a brief recap of how to integrate plugins with Payload, to learn more head back to the [plugin overview page](../plugins/overview). ### How to install a plugin To install any plugin, simply add it to your Payload Config in the plugins array. ``` import samplePlugin from 'sample-plugin'; const config = buildConfig({ plugins: [ // Add plugins here samplePlugin({ enabled: true, }), ], }); export default config; ``` ### Initialization The initialization process goes in the following order: 1. Incoming config is validated 2. Plugins execute 3. Default options are integrated 4. Sanitization cleans and validates data 5. Final config gets initialized ## Plugin Template In the [Payload Plugin Template](https://github.com/payloadcms/payload/tree/main/templates/plugin), you will see a common file structure that is used across plugins: 1. `/` root folder - general configuration 2. `/src` folder - everything related to the plugin 3. `/dev` folder - sanitized test project for development ### The root folder In the root folder, you will see various files related to the configuration of the plugin. We set up our environment in a similar manner in Payload core and across other projects. The only two files you need to modify are: - **README**.md - This contains instructions on how to use the template. When you are ready, update this to contain instructions on how to use your Plugin. - **package**.json - Contains necessary scripts and dependencies. Overwrite the metadata in this file to describe your Plugin. ### The dev folder The purpose of the **dev** folder is to provide a sanitized local Payload project. so you can run and test your plugin while you are actively developing it. Do **not** store any of the plugin functionality in this folder - it is purely an environment to _assist_ you with developing the plugin. If you're starting from scratch, you can easily setup a dev environment like this: ``` mkdir dev cd dev npx create-payload-app@latest ``` If you're using the plugin template, the dev folder is built out for you and the `samplePlugin` has already been installed in `dev/payload.config.ts`. ``` plugins: [ // when you rename the plugin or add options, make sure to update it here samplePlugin({ enabled: false, }) ] ``` You can add to the `dev/payload.config.ts` and build out the dev project as needed to test your plugin. When you're ready to start development, navigate into this folder with `cd dev` And then start the project with `pnpm dev` and pull up `http://localhost:3000` in your browser. ## Testing Another benefit of the dev folder is that you have the perfect environment established for testing. A good test suite is essential to ensure quality and stability in your plugin. Payload typically uses [Jest](https://jestjs.io/); a popular testing framework, widely used for testing JavaScript and particularly for applications built with React. Jest organizes tests into test suites and cases. We recommend creating tests based on the expected behavior of your plugin from start to finish. Read more about tests in the [Jest documentation.](https://jestjs.io/) The plugin template provides a stubbed out test suite at `dev/plugin.spec.ts` which is ready to go - just add in your own test conditions and you're all set! ``` let payload: Payload describe('Plugin tests', () => { // Example test to check for seeded data it('seeds data accordingly', async () => { const newCollectionQuery = await payload.find({ collection: 'newCollection', sort: 'createdAt', }) newCollection = newCollectionQuery.docs expect(newCollectionQuery.totalDocs).toEqual(1) }) }) ``` ## Seeding data For development and testing, you will likely need some data to work with. You can streamline this process by seeding and dropping your database - instead of manually entering data. In the plugin template, you can navigate to `dev/src/server.ts` and see an example seed function. ``` if (process.env.PAYLOAD_SEED === 'true') { await seed(payload) } ``` A sample seed function has been created for you at `dev/src/seed`, update this file with additional data as needed. ``` export const seed = async (payload: Payload): Promise => { payload.logger.info('Seeding data...') await payload.create({ collection: 'new-collection', data: { title: 'Seeded title', }, }) // Add additional seed data here } ``` ## Building a Plugin Now that we have our environment setup and dev project ready to go - it's time to build the plugin! ``` import type { Config } from 'payload' export const samplePlugin = (pluginOptions: PluginTypes) => (incomingConfig: Config): Config => { // create copy of incoming config let config = { ...incomingConfig } /** * This is where you could modify the * config based on the plugin options */ // If you wanted to add a new collection: config.collections = [ ...(config.collections || []), newCollection, ] // If you wanted to add a new global: config.globals = [ ...(config.globals || []), newGlobal, ] /** * If you wanted to add a new field to a collection: * * 1. Loop over collections * 2. Find the collection you want to add the field to * 3. Add the field to the collection */ // If you wanted to add to the onInit: config.onInit = async payload => { if (incomingConfig.onInit) await incomingConfig.onInit(payload) // Add additional onInit code here } // Finally, return the modified config return config } ``` To reiterate, the essence of a [Payload Plugin](./overview) is simply to extend the [Payload Config](../configuration/overview) - and that is exactly what we are doing in this file. ### Spread syntax [Spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) (or the spread operator) is a feature in JavaScript that uses the dot notation **(...)** to spread elements from arrays, strings, or objects into various contexts. We are going to use spread syntax to allow us to add data to existing arrays without losing the existing data. It is crucial to spread the existing data correctly, else this can cause adverse behavior and conflicts with Payload Config and other plugins. Let's say you want to build a plugin that adds a new collection: ``` config.collections = [ ...(config.collections || []), newCollection, // Add additional collections here ] ``` First, you need to spread the `config.collections` to ensure that we don't lose the existing collections. Then you can add any additional collections, just as you would in a regular Payload Config. This same logic is applied to other array and object like properties such as admin, globals and hooks: ``` config.globals = [ ...(config.globals || []), // Add additional globals here ] config.hooks = { ...(config.hooks || {}), // Add additional hooks here } ``` ### Extending functions Function properties cannot use spread syntax. The way to extend them is to execute the existing function if it exists and then run your additional functionality. Here is an example extending the `onInit` property: ``` config.onInit = async payload => { if (incomingConfig.onInit) await incomingConfig.onInit(payload) // Add additional onInit code by using the onInitExtension function onInitExtension(pluginOptions, payload) } ``` ## Types If your plugin has options, you should define and provide types for these options in a separate file which gets exported from the main `index.ts`. ``` export interface PluginTypes { /** * Enable or disable plugin * @default false */ enabled?: boolean } ``` If possible, include [JSDoc comments](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#types-1) to describe the options and their types. This allows a developer to see details about the options in their editor. ## Best practices In addition to the setup covered above, here are other best practices to follow: ### Providing an enable / disable option For a better user experience, provide a way to disable the plugin without uninstalling it. ### Include tests in your GitHub CI workflow If you've configured tests for your package, integrate them into your workflow to run the tests each time you commit to the plugin repository. Learn more about [how to configure tests into your GitHub CI workflow.](https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-nodejs) ### Publish your finished plugin to npm The best way to share and allow others to use your plugin once it is complete is to publish an npm package. This process is straightforward and well documented, find out more about [creating and publishing a npm package here](https://docs.npmjs.com/creating-and-publishing-scoped-public-packages/). ### Add payload-plugin topic tag Apply the tag **payload-plugin** to your GitHub repository. This will boost the visibility of your plugin and ensure it gets listed with [existing Payload plugins](https://github.com/topics/payload-plugin). ### Use Semantic Versioning (SemVer) With the [Semantic Versioning](https://semver.org/) (SemVer) system you release version numbers that reflect the nature of changes (major, minor, patch). Ensure all major versions reference their Payload compatibility. # Form Builder Plugin Source: https://payloadcms.com/docs/plugins/form-builder ![https://www.npmjs.com/package/@payloadcms/plugin-form-builder](https://img.shields.io/npm/v/@payloadcms/plugin-form-builder) This plugin allows you to build and manage custom forms directly within the [Admin Panel](../admin/overview). Instead of hard-coding a new form into your website or application every time you need one, admins can simply define the schema for each form they need on-the-fly, and your front-end can map over this schema, render its own UI components, and match your brand's design system. All form submissions are stored directly in your database and are managed directly from the Admin Panel. When forms are submitted, you can display a custom on-screen confirmation message to the user or redirect them to a dedicated confirmation page. You can even send dynamic, personalized emails derived from the form's data. For example, you may want to send a confirmation email to the user who submitted the form, and also send a notification email to your team. Forms can be as simple or complex as you need, from a basic contact form, to a multi-step lead generation engine, or even a donation form that processes payment. You may not need to reach for third-party services like HubSpot or Mailchimp for this, but instead use your own first-party tooling, built directly into your own application. This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-form-builder). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20form-builder&template=bug_report.md&title=plugin-form-builder%3A) with as much detail as possible. ## Core Features - Build completely dynamic forms directly from the Admin Panel for a variety of use cases - Render forms on your front-end using your own UI components and match your brand's design system - Send dynamic, personalized emails upon form submission to multiple recipients, derived from the form's data - Display a custom confirmation message or automatically redirect upon form submission - Build dynamic prices based on form input to use for payment processing (optional) ## Installation Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com): ```bash pnpm add @payloadcms/plugin-form-builder ``` ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin with [options](#options): ```ts import { buildConfig } from 'payload' import { formBuilderPlugin } from '@payloadcms/plugin-form-builder' const config = buildConfig({ collections: [ { slug: 'pages', fields: [], }, ], plugins: [ formBuilderPlugin({ // see below for a list of available options }), ], }) export default config ``` ## Options ### `fields` (option) The `fields` property is an object of field types to allow your admin editors to build forms with. To override default settings, pass either a boolean value or a partial [Payload Block](../fields/blocks#block-configs) _keyed to the block's slug_. See [Fields](#fields) for more details. ```ts // payload.config.ts formBuilderPlugin({ // ... fields: { text: true, textarea: true, select: true, radio: true, email: true, state: true, country: true, checkbox: true, number: true, message: true, date: false, payment: false, }, }) ``` ### `redirectRelationships` The `redirectRelationships` property is an array of collection slugs that, when enabled, are populated as options in the form's `redirect` field. This field is used to redirect the user to a dedicated confirmation page upon form submission (optional). ```ts // payload.config.ts formBuilderPlugin({ // ... redirectRelationships: ['pages'], }) ``` ### `beforeEmail` The `beforeEmail` property is a [beforeChange](../hooks/globals#beforechange) hook that is called just after emails are prepared, but before they are sent. This is a great place to inject your own HTML template to add custom styles. ```ts // payload.config.ts formBuilderPlugin({ // ... beforeEmail: (emailsToSend, beforeChangeParams) => { // modify the emails in any way before they are sent return emails.map((email) => ({ ...email, html: email.html, // transform the html in any way you'd like (maybe wrap it in an html template?) })) }, }) ``` For full types with `beforeChangeParams`, you can import the types from the plugin: ```ts import type { BeforeEmail } from '@payloadcms/plugin-form-builder' // Your generated FormSubmission type import type { FormSubmission } from '@payload-types' // Pass it through and 'data' or 'originalDoc' will now be typed const beforeEmail: BeforeEmail = ( emailsToSend, beforeChangeParams, ) => { // modify the emails in any way before they are sent return emails.map((email) => ({ ...email, html: email.html, // transform the html in any way you'd like (maybe wrap it in an html template?) })) } ``` ### `defaultToEmail` Provide a fallback for the email address to send form submissions to. If the email in form configuration does not have a to email set, this email address will be used. If this is not provided then it falls back to the `defaultFromAddress` in your [email configuration](../email/overview). ```ts // payload.config.ts formBuilderPlugin({ // ... defaultToEmail: 'test@example.com', }) ``` ### `formOverrides` Override anything on the `forms` collection by sending a [Payload Collection Config](../configuration/collections) to the `formOverrides` property. Note that the `fields` property is a function that receives the default fields and returns an array of fields. This is because the `fields` property is a special case that is merged with the default fields, rather than replacing them. This allows you to map over default fields and modify them as needed. Good to know: The form collection is publicly available to read by default. The emails field is locked for authenticated users only. If you have any frontend users you should override the access permissions for both the collection and the emails field to make sure you don't leak out any private emails. ```ts // payload.config.ts formBuilderPlugin({ // ... formOverrides: { slug: 'contact-forms', access: { read: ({ req: { user } }) => !!user, // authenticated users only update: () => false, }, fields: ({ defaultFields }) => { return [ ...defaultFields, { name: 'custom', type: 'text', }, ] }, }, }) ``` ### `formSubmissionOverrides` Override anything on the `form-submissions` collection by sending a [Payload Collection Config](../configuration/collections) to the `formSubmissionOverrides` property. By default, this plugin relies on [Payload access control](../access-control/collections) to restrict the `update` and `read` operations on the `form-submissions` collection. This is because _anyone_ should be able to create a form submission, even from a public-facing website, but _no one_ should be able to update a submission once it has been created, or read a submission unless they have permission. You can override this behavior or any other property as needed. ```ts // payload.config.ts formBuilderPlugin({ // ... formSubmissionOverrides: { slug: 'leads', fields: ({ defaultFields }) => { return [ ...defaultFields, { name: 'custom', type: 'text', }, ] }, }, }) ``` ### `handlePayment` The `handlePayment` property is a [beforeChange](../hooks/globals#beforechange) hook that is called upon form submission. You can integrate into any third-party payment processing API here to accept payment based on form input. You can use the `getPaymentTotal` function to calculate the total cost after all conditions have been applied. This is only applicable if the form has enabled the `payment` field. First import the utility function. This will execute all of the price conditions that you have set in your form's `payment` field and returns the total price. ```ts // payload.config.ts import { getPaymentTotal } from '@payloadcms/plugin-form-builder' ``` Then in your plugin's config: ```ts // payload.config.ts formBuilderPlugin({ // ... handlePayment: async ({ form, submissionData }) => { // first calculate the price const paymentField = form.fields?.find( (field) => field.blockType === 'payment', ) const price = getPaymentTotal({ basePrice: paymentField.basePrice, priceConditions: paymentField.priceConditions, fieldValues: submissionData, }) // then asynchronously process the payment here }, }) ``` ## Fields Each field represents a form input. To override default settings pass either a boolean value or a partial [Payload Block](../fields/blocks) _keyed to the block's slug_. See [Field Overrides](#field-overrides) for more details on how to do this. **Note:** "Fields" here is in reference to the _fields to build forms with_, not to be confused with the _fields of a collection_ which are set via `formOverrides.fields`. ### Text Maps to a `text` input in your front-end. Used to collect a simple string. | Property | Type | Description | | -------------- | -------- | ---------------------------------------------------- | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | string | The default value of the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | ### Textarea Maps to a `textarea` input on your front-end. Used to collect a multi-line string. | Property | Type | Description | | -------------- | -------- | ---------------------------------------------------- | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | string | The default value of the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | ### Select Maps to a `select` input on your front-end. Used to display a list of options. | Property | Type | Description | | -------------- | -------- | ------------------------------------------------------------------------------- | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | string | The default value of the field. | | `placeholder` | string | The placeholder text for the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | | `options` | array | An array of objects that define the select options. See below for more details. | #### Select Options Each option in the `options` array defines a selectable choice for the select field. | Property | Type | Description | | -------- | ------ | ----------------------------------- | | `label` | string | The display text for the option. | | `value` | string | The value submitted for the option. | ### Radio Maps to radio button inputs on your front-end. Used to allow users to select a single option from a list of choices. | Property | Type | Description | | -------------- | -------- | ------------------------------------------------------------------------------ | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | string | The default value of the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | | `options` | array | An array of objects that define the radio options. See below for more details. | #### Radio Options Each option in the `options` array defines a selectable choice for the radio field. | Property | Type | Description | | -------- | ------ | ----------------------------------- | | `label` | string | The display text for the option. | | `value` | string | The value submitted for the option. | ### Email (field) Maps to a `text` input with type `email` on your front-end. Used to collect an email address. | Property | Type | Description | | -------------- | -------- | ---------------------------------------------------- | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | string | The default value of the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | ### State Maps to a `select` input on your front-end. Used to collect a US state. | Property | Type | Description | | -------------- | -------- | ---------------------------------------------------- | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | string | The default value of the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | ### Country Maps to a `select` input on your front-end. Used to collect a country. | Property | Type | Description | | -------------- | -------- | ---------------------------------------------------- | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | string | The default value of the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | ### Checkbox Maps to a `checkbox` input on your front-end. Used to collect a boolean value. | Property | Type | Description | | -------------- | -------- | ---------------------------------------------------- | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | checkbox | The default value of the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | ### Date Maps to a `date` input on your front-end. Used to collect a date value. | Property | Type | Description | | -------------- | -------- | ---------------------------------------------------- | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | date | The default value of the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | ### Number Maps to a `number` input on your front-end. Used to collect a number. | Property | Type | Description | | -------------- | -------- | ---------------------------------------------------- | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | number | The default value of the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | ### Message Maps to a `RichText` component on your front-end. Used to display an arbitrary message to the user anywhere in the form. | property | type | description | | --------- | -------- | ----------------------------------- | | `message` | richText | The message to display on the form. | ### Payment Add this field to your form if it should collect payment. Upon submission, the `handlePayment` callback is executed with the form and submission data. You can use this to integrate with any third-party payment processing API. | property | type | description | | ----------------- | -------- | --------------------------------------------------------------------------------- | | `name` | string | The name of the field. | | `label` | string | The label of the field. | | `defaultValue` | number | The default value of the field. | | `width` | string | The width of the field on the front-end. | | `required` | checkbox | Whether or not the field is required when submitted. | | `priceConditions` | array | An array of objects that define the price conditions. See below for more details. | #### Price Conditions Each of the `priceConditions` are executed by the `getPaymentTotal` utility that this plugin provides. You can call this function in your `handlePayment` callback to dynamically calculate the total price of a form upon submission based on the user's input. For example, you could create a price condition that says "if the user selects 'yes' for this checkbox, add $10 to the total price". | property | type | description | | ------------------ | ------------ | ------------------------------------------------ | | `fieldToUse` | relationship | The field to use to determine the price. | | `condition` | string | The condition to use to determine the price. | | `valueForOperator` | string | The value to use for the operator. | | `operator` | string | The operator to use to determine the price. | | `valueType` | string | The type of value to use to determine the price. | | `value` | string | The value to use to determine the price. | ### Field Overrides You can provide your own custom fields by passing a new [Payload Block](../fields/blocks#block-configs) object into `fields`. You can override or extend any existing fields by first importing the `fields` from the plugin: ```ts import { fields } from '@payloadcms/plugin-form-builder' ``` Then merging it into your own custom field: ```ts // payload.config.ts formBuilderPlugin({ // ... fields: { text: { ...fields.text, labels: { singular: 'Custom Text Field', plural: 'Custom Text Fields', }, }, }, }) ``` ### Customizing the date field default value You can custommise the default value of the date field and any other aspects of the date block in this way. Note that the end submission source will be responsible for the timezone of the date. Payload only stores the date in UTC format. ```ts import { fields as formFields } from '@payloadcms/plugin-form-builder' // payload.config.ts formBuilderPlugin({ fields: { // date: true, // just enable it without any customizations date: { ...formFields.date, fields: [ ...(formFields.date && 'fields' in formFields.date ? formFields.date.fields.map((field) => { if ('name' in field && field.name === 'defaultValue') { return { ...field, timezone: true, // optionally enable timezone admin: { ...field.admin, description: 'This is a date field', }, } } return field }) : []), ], }, }, }) ``` ### Preventing generated schema naming conflicts Plugin fields can cause GraphQL type name collisions with your own blocks or collections. This results in errors like: ```plaintext Error: Schema must contain uniquely named types but contains multiple types named "Country" ``` You can resolve this by overriding: - `graphQL.singularName` in your collection config (for GraphQL schema conflicts) - `interfaceName` in your block config - `interfaceName` in the plugin field config ```ts // payload.config.ts formBuilderPlugin({ fields: { country: { interfaceName: 'CountryFormBlock', // overrides the generated type name to avoid a conflict }, }, }) ``` ## Email This plugin relies on the [email configuration](../email/overview) defined in your Payload configuration. It will read from your config and attempt to send your emails using the credentials provided. ### Email formatting The email contents supports rich text which will be serialized to HTML on the server before being sent. By default it reads the global configuration of your rich text editor. The email subject and body supports inserting dynamic fields from the form submission data using the `{{field_name}}` syntax. For example, if you have a field called `name` in your form, you can include this in the email body like so: ```html Thank you for your submission, {{name}}! ``` You can also use `{{*}}` as a wildcard to output all the data in a key:value format and `{{*:table}}` to output all the data in a table format. ## TypeScript All types can be directly imported: ```ts import type { PluginConfig, Form, FormSubmission, FieldsConfig, BeforeEmail, HandlePayment, ... } from "@payloadcms/plugin-form-builder/types"; ``` ## Examples The [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples) contains an official [Form Builder Plugin Example](https://github.com/payloadcms/payload/tree/main/examples/form-builder) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. We've also included an in-depth walk-through of how to build a form from scratch in our [Form Builder Plugin Blog Post](https://payloadcms.com/blog/create-custom-forms-with-the-official-form-builder-plugin). ## Troubleshooting Below are some common troubleshooting tips. To help other developers, please contribute to this section as you troubleshoot your own application. #### SendGrid 403 Forbidden Error - If you are using [SendGrid Link Branding](https://docs.sendgrid.com/ui/account-and-settings/how-to-set-up-link-branding) to remove the "via sendgrid.net" part of your email, you must also setup [Domain Authentication](https://docs.sendgrid.com/ui/account-and-settings/how-to-set-up-domain-authentication). This means you can only send emails from an address on this domain — so the `from` addresses in your form submission emails **_cannot_** be anything other than `something@your_domain.com`. This means that from `{{email}}` will not work, but `website@your_domain.com` will. You can still send the form's email address in the body of the email. ## Screenshots ![screenshot 1](https://github.com/payloadcms/plugin-form-builder/blob/main/images/screenshot-1.jpg?raw=true) ![screenshot 2](https://github.com/payloadcms/plugin-form-builder/blob/main/images/screenshot-2.jpg?raw=true) ![screenshot 3](https://github.com/payloadcms/plugin-form-builder/blob/main/images/screenshot-3.jpg?raw=true) ![screenshot 4](https://github.com/payloadcms/plugin-form-builder/blob/main/images/screenshot-4.jpg?raw=true) ![screenshot 5](https://github.com/payloadcms/plugin-form-builder/blob/main/images/screenshot-5.jpg?raw=true) ![screenshot 6](https://github.com/payloadcms/plugin-form-builder/blob/main/images/screenshot-6.jpg?raw=true) # Import Export Plugin Source: https://payloadcms.com/docs/plugins/import-export ![https://www.npmjs.com/package/@payloadcms/plugin-import-export](https://img.shields.io/npm/v/@payloadcms/plugin-import-export) **Note**: This plugin is in **beta** as some aspects of it may change on any minor releases. It is under development and currently only supports exporting of collection data. This plugin adds features that give admin users the ability to download or create export data as an upload collection and import it back into a project. ## Core Features - Export data as CSV or JSON format via the admin UI - Download the export directly through the browser - Create a file upload of the export data - Use the jobs queue for large exports - (Coming soon) Import collection data ## Installation Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com): ```bash pnpm add @payloadcms/plugin-import-export ``` ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin with [options](#options): ```ts import { buildConfig } from 'payload' import { importExportPlugin } from '@payloadcms/plugin-import-export' const config = buildConfig({ collections: [Pages, Media], plugins: [ importExportPlugin({ collections: ['users', 'pages'], // see below for a list of available options }), ], }) export default config ``` ## Options | Property | Type | Description | | -------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `collections` | string[] | Collections to include Import/Export controls in. Defaults to all collections. | | `debug` | boolean | If true, enables debug logging. | | `disableDownload` | boolean | If true, disables the download button in the export preview UI. | | `disableJobsQueue` | boolean | If true, forces the export to run synchronously. | | `disableSave` | boolean | If true, disables the save button in the export preview UI. | | `format` | string | Forces a specific export format (`csv` or `json`), hides the format dropdown, and prevents the user from choosing the export format. | | `overrideExportCollection` | function | Function to override the default export collection; takes the default export collection and allows you to modify and return it. | ## Field Options In addition to the above plugin configuration options, you can granularly set the following field level options using the `custom['plugin-import-export']` properties in any of your collections. | Property | Type | Description | | ---------- | -------- | ----------------------------------------------------------------------------------------------------------------------------- | | `disabled` | boolean | When `true` the field is completely excluded from the import-export plugin. | | `toCSV` | function | Custom function used to modify the outgoing csv data by manipulating the data, siblingData or by returning the desired value. | ### Customizing the output of CSV data To manipulate the data that a field exports you can add `toCSV` custom functions. This allows you to modify the outgoing csv data by manipulating the data, siblingData or by returning the desired value. The toCSV function argument is an object with the following properties: | Property | Type | Description | | ------------ | ------- | ----------------------------------------------------------------- | | `columnName` | string | The CSV column name given to the field. | | `doc` | object | The top level document | | `row` | object | The object data that can be manipulated to assign data to the CSV | | `siblingDoc` | object | The document data at the level where it belongs | | `value` | unknown | The data for the field. | Example function: ```ts const pages: CollectionConfig = { slug: 'pages', fields: [ { name: 'author', type: 'relationship', relationTo: 'users', custom: { 'plugin-import-export': { toCSV: ({ value, columnName, row }) => { // add both `author_id` and the `author_email` to the csv export if ( value && typeof value === 'object' && 'id' in value && 'email' in value ) { row[`${columnName}_id`] = (value as { id: number | string }).id row[`${columnName}_email`] = (value as { email: string }).email } }, }, }, }, ], } ``` ## Exporting Data There are four possible ways that the plugin allows for exporting documents, the first two are available in the admin UI from the list view of a collection: 1. Direct download - Using a `POST` to `/api/exports/download` and streams the response as a file download 2. File storage - Goes to the `exports` collection as an uploads enabled collection 3. Local API - A create call to the uploads collection: `payload.create({ slug: 'uploads', ...parameters })` 4. Jobs Queue - `payload.jobs.queue({ task: 'createCollectionExport', input: parameters })` By default, a user can use the Export drawer to create a file download by choosing `Save` or stream a downloadable file directly without persisting it by using the `Download` button. Either option can be disabled to provide the export experience you desire for your use-case. The UI for creating exports provides options so that users can be selective about which documents to include and also which columns or fields to include. It is necessary to add access control to the uploads collection configuration using the `overrideExportCollection` function if you have enabled this plugin on collections with data that some authenticated users should not have access to. **Note**: Users who have read access to the upload collection may be able to download data that is normally not readable due to [access control](../access-control/overview). The following parameters are used by the export function to handle requests: | Property | Type | Description | | ---------------- | -------- | ----------------------------------------------------------------------------------------------------------------- | | `format` | text | Either `csv` or `json` to determine the shape of data exported | | `limit` | number | The max number of documents to return | | `sort` | select | The field to use for ordering documents | | `locale` | string | The locale code to query documents or `all` | | `draft` | string | Either `yes` or `no` to return documents with their newest drafts for drafts enabled collections | | `fields` | string[] | Which collection fields are used to create the export, defaults to all | | `collectionSlug` | string | The slug to query against | | `where` | object | The WhereObject used to query documents to export. This is set by making selections or filters from the list view | | `filename` | text | What to call the export being created | # Multi-Tenant Plugin Source: https://payloadcms.com/docs/plugins/multi-tenant ![https://www.npmjs.com/package/@payloadcms/plugin-multi-tenant](https://img.shields.io/npm/v/@payloadcms/plugin-multi-tenant) This plugin sets up multi-tenancy for your application from within your [Admin Panel](../admin/overview). It does so by adding a `tenant` field to all specified collections. Your front-end application can then query data by tenant. You must add the Tenants collection so you control what fields are available for each tenant. This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-multi-tenant). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new/choose) with as much detail as possible. ## Core features - Adds a `tenant` field to each specified collection - Adds a tenant selector to the admin panel, allowing you to switch between tenants - Filters list view results by selected tenant - Filters relationship fields by selected tenant - Ability to create "global" like collections, 1 doc per tenant - Automatically assign a tenant to new documents **Warning** By default this plugin cleans up documents when a tenant is deleted. You should ensure you have strong access control on your tenants collection to prevent deletions by unauthorized users. You can disable this behavior by setting `cleanupAfterTenantDelete` to `false` in the plugin options. ## Installation Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com): ```bash pnpm add @payloadcms/plugin-multi-tenant ``` ### Options The plugin accepts an object with the following properties: ```ts type MultiTenantPluginConfig = { /** * Base path for your application * * https://nextjs.org/docs/app/api-reference/config/next-config-js/basePath * * @default undefined */ basePath?: string /** * After a tenant is deleted, the plugin will attempt to clean up related documents * - removing documents with the tenant ID * - removing the tenant from users * * @default true */ cleanupAfterTenantDelete?: boolean /** * Automatically */ collections: { [key in CollectionSlug]?: { /** * Override the access result from the collection access control functions * * The function receives: * - accessResult: the original result from the access control function * - accessKey: 'read', 'create', 'update', 'delete', 'readVersions', or 'unlock' * - ...restOfAccessArgs: the original arguments passed to the access control function */ accessResultOverride?: CollectionAccessResultOverride /** * Opt out of adding the tenant field and place * it manually using the `tenantField` export from the plugin */ customTenantField?: boolean /** * Set to `true` if you want the collection to behave as a global * * @default false */ isGlobal?: boolean /** * Overrides for the tenant field, will override the entire tenantField configuration */ tenantFieldOverrides?: CollectionTenantFieldConfigOverrides /** * Set to `false` if you want to manually apply the baseListFilter * Set to `false` if you want to manually apply the baseFilter * * @default true */ useBaseFilter?: boolean /** * @deprecated Use `useBaseFilter` instead. If both are defined, * `useBaseFilter` will take precedence. This property remains only * for backward compatibility and may be removed in a future version. * * Originally, `baseListFilter` was intended to filter only the List View * in the admin panel. However, base filtering is often required in other areas * such as internal link relationships in the Lexical editor. * * @default true */ useBaseListFilter?: boolean /** * Set to `false` if you want to handle collection access manually without the multi-tenant constraints applied * * @default true */ useTenantAccess?: boolean } } /** * Enables debug mode * - Makes the tenant field visible in the admin UI within applicable collections * * @default false */ debug?: boolean /** * Enables the multi-tenant plugin * * @default true */ enabled?: boolean /** * Localization for the plugin */ i18n?: { translations: { [key in AcceptedLanguages]?: { /** * Shown inside 3 dot menu on edit document view * * @default 'Assign Tenant' */ 'assign-tenant-button-label'?: string /** * Shown as the title of the assign tenant modal * * @default 'Assign "{{title}}"' */ 'assign-tenant-modal-title'?: string /** * Shown as the label for the assigned tenant field in the assign tenant modal * * @default 'Assigned Tenant' */ 'field-assignedTenant-label'?: string /** * Shown as the label for the global tenant selector in the admin UI * * @default 'Filter by Tenant' */ 'nav-tenantSelector-label'?: string } } } /** * Field configuration for the field added to all tenant enabled collections */ tenantField?: RootTenantFieldConfigOverrides /** * Field configuration for the field added to the users collection * * If `includeDefaultField` is `false`, you must include the field on your users collection manually * This is useful if you want to customize the field or place the field in a specific location */ tenantsArrayField?: | { /** * Access configuration for the array field */ arrayFieldAccess?: ArrayField['access'] /** * Name of the array field * * @default 'tenants' */ arrayFieldName?: string /** * Name of the tenant field * * @default 'tenant' */ arrayTenantFieldName?: string /** * When `includeDefaultField` is `true`, the field will be added to the users collection automatically */ includeDefaultField?: true /** * Additional fields to include on the tenants array field */ rowFields?: Field[] /** * Access configuration for the tenant field */ tenantFieldAccess?: RelationshipField['access'] } | { arrayFieldAccess?: never arrayFieldName?: string arrayTenantFieldName?: string /** * When `includeDefaultField` is `false`, you must include the field on your users collection manually */ includeDefaultField?: false rowFields?: never tenantFieldAccess?: never } /** * Customize tenant selector label * * Either a string or an object where the keys are i18n codes and the values are the string labels * * @deprecated Use `i18n.translations` instead. */ tenantSelectorLabel?: | Partial<{ [key in AcceptedLanguages]?: string }> | string /** * The slug for the tenant collection * * @default 'tenants' */ tenantsSlug?: string /** * Function that determines if a user has access to _all_ tenants * * Useful for super-admin type users */ userHasAccessToAllTenants?: ( user: ConfigTypes extends { user: unknown } ? ConfigTypes['user'] : TypedUser, ) => boolean /** * Override the access result on the users collection access control functions * * The function receives: * - accessResult: the original result from the access control function * - accessKey: 'read', 'create', 'update', 'delete', 'readVersions', or 'unlock' * - ...restOfAccessArgs: the original arguments passed to the access control function */ usersAccessResultOverride?: CollectionAccessResultOverride /** * Opt out of adding access constraints to the tenants collection */ useTenantsCollectionAccess?: boolean /** * Opt out including the baseListFilter to filter tenants by selected tenant */ useTenantsListFilter?: boolean /** * Opt out including the baseListFilter to filter users by selected tenant */ useUsersTenantFilter?: boolean } ``` ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin with [options](#options): ```ts import { buildConfig } from 'payload' import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant' import type { Config } from './payload-types' const config = buildConfig({ collections: [ { slug: 'tenants', admin: { useAsTitle: 'name', }, fields: [ // remember, you own these fields // these are merely suggestions/examples { name: 'name', type: 'text', required: true, }, { name: 'slug', type: 'text', required: true, }, { name: 'domain', type: 'text', required: true, }, ], }, ], plugins: [ multiTenantPlugin({ collections: { pages: {}, navigation: { isGlobal: true, }, }, }), ], }) export default config ``` ## Front end usage The plugin scaffolds out everything you will need to separate data by tenant. You can use the `tenant` field to filter data from enabled collections in your front-end application. In your frontend you can query and constrain data by tenant with the following: ```tsx const pagesBySlug = await payload.find({ collection: 'pages', depth: 1, draft: false, limit: 1000, overrideAccess: false, where: { // your constraint would depend on the // fields you added to the tenants collection // here we are assuming a slug field exists // on the tenant collection, like in the example above 'tenant.slug': { equals: 'gold', }, }, }) ``` ### NextJS rewrites Using NextJS rewrites and this route structure `/[tenantDomain]/[slug]`, we can rewrite routes specifically for domains requested: ```ts async rewrites() { return [ { source: '/((?!admin|api)):path*', destination: '/:tenantDomain/:path*', has: [ { type: 'host', value: '(?.*)', }, ], }, ]; } ``` ### React Hooks Below are the hooks exported from the plugin that you can import into your own custom components to consume. #### useTenantSelection You can import this like so: ```tsx import { useTenantSelection } from '@payloadcms/plugin-multi-tenant/client' ... const tenantContext = useTenantSelection() ``` The hook returns the following context: ```ts type ContextType = { /** * Array of options to select from */ options: OptionObject[] /** * The currently selected tenant ID */ selectedTenantID: number | string | undefined /** * Prevents a refresh when the tenant is changed * * If not switching tenants while viewing a "global", * set to true */ setPreventRefreshOnChange: React.Dispatch> /** * Sets the selected tenant ID * * @param args.id - The ID of the tenant to select * @param args.refresh - Whether to refresh the page * after changing the tenant */ setTenant: (args: { id: number | string | undefined refresh?: boolean }) => void } ``` ## Examples The [Examples Directory](https://github.com/payloadcms/payload/tree/main/examples) also contains an official [Multi-Tenant](https://github.com/payloadcms/payload/tree/main/examples/multi-tenant) example. # Nested Docs Plugin Source: https://payloadcms.com/docs/plugins/nested-docs ![https://www.npmjs.com/package/@payloadcms/plugin-nested-docs](https://img.shields.io/npm/v/@payloadcms/plugin-nested-docs) This plugin allows you to easily nest the documents of your application inside of one another. It does so by adding a new `parent` field onto each of your documents that, when selected, attaches itself to the parent's tree. When you edit the great-great-grandparent of a document, for instance, all of its descendants are recursively updated. This is an extremely powerful way of achieving hierarchy within a collection, such as parent/child relationship between pages. Documents also receive a new `breadcrumbs` field. Once a parent is assigned, these breadcrumbs are populated based on each ancestor up the tree. Breadcrumbs allow you to dynamically generate labels and URLs based on the document's position in the hierarchy. Even if the slug of a parent document changes, or the entire tree is nested another level deep, changes will cascade down the entire tree and all breadcrumbs will reflect those changes. With this pattern you can perform whatever side-effects your applications needs on even the most deeply nested documents. For example, you could easily add a custom `fullTitle` field onto each document and inject the parent's title onto it, such as "Parent Title > Child Title". This would allow you to then perform searches and filters based on _that_ field instead of the original title. This is especially useful if you happen to have two documents with identical titles but different parents. This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-nested-docs). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20nested-docs&template=bug_report.md&title=plugin-nested-docs%3A) with as much detail as possible. ## Core features - Automatically adds a `parent` relationship field to each document - Allows for parent/child relationships between documents within the same collection - Recursively updates all descendants when a parent is changed - Automatically populates a `breadcrumbs` field with all ancestors up the tree - Dynamically generate labels and URLs for each breadcrumb - Supports localization ## Installation Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com): ```bash pnpm add @payloadcms/plugin-nested-docs ``` ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin with [options](#options): ```ts import { buildConfig } from 'payload' import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs' const config = buildConfig({ collections: [ { slug: 'pages', fields: [ { name: 'title', type: 'text', }, { name: 'slug', type: 'text', }, ], }, ], plugins: [ nestedDocsPlugin({ collections: ['pages'], generateLabel: (_, doc) => doc.title, generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''), }), ], }) export default config ``` ### Fields #### Parent The `parent` relationship field is automatically added to every document which allows editors to choose another document from the same collection to act as the direct parent. #### Breadcrumbs The `breadcrumbs` field is an array which dynamically populates all parent relationships of a document up to the top level and stores the following fields. | Field | Description | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `label` | The label of the breadcrumb. This field is automatically set to either the `collection.admin.useAsTitle` (if defined) or is set to the `ID` of the document. You can also dynamically define the `label` by passing a function to the options property of [`generateLabel`](#generatelabel). | | `url` | The URL of the breadcrumb. By default, this field is undefined. You can manually define this field by passing a property called function to the plugin options property of [`generateURL`](#generateurl). | ### Options #### `collections` An array of collections slugs to enable nested docs. #### `generateLabel` Each `breadcrumb` has a required `label` field. By default, its value will be set to the collection's `admin.useAsTitle` or fallback the the `ID` of the document. You can also pass a function to dynamically set the `label` of your breadcrumb. ```ts // payload.config.ts nestedDocsPlugin({ //... generateLabel: (_, doc) => doc.title, // NOTE: 'title' is a hypothetical field }) ``` The function takes two arguments and returns a string: | Argument | Type | Description | | ------------ | -------- | --------------------------------------------- | | `docs` | `Array` | An array of the breadcrumbs up to that point | | `doc` | `Object` | The current document being edited | | `collection` | `Object` | The collection config of the current document | #### `generateURL` A function that allows you to dynamically generate each breadcrumb `url`. Each `breadcrumb` has an optional `url` field which is undefined by default. For example, you might want to format a full URL to contain all breadcrumbs up to that point, like `/about-us/company/our-team`. ```ts // payload.config.ts nestedDocsPlugin({ //... generateURL: (docs) => docs.reduce((url, doc) => `${url}/${doc.slug}`, ''), // NOTE: 'slug' is a hypothetical field }) ``` | Argument | Type | Description | | ------------ | -------- | --------------------------------------------- | | `docs` | `Array` | An array of the breadcrumbs up to that point | | `doc` | `Object` | The current document being edited | | `collection` | `Object` | The collection config of the current document | #### `parentFieldSlug` When defined, the `parent` field will not be provided for you automatically, and instead, expects you to add your own `parent` field to each collection manually. This gives you complete control over where you put the field in your admin dashboard, etc. Set this property to the `name` of your custom field. #### `breadcrumbsFieldSlug` When defined, the `breadcrumbs` field will not be provided for you, and instead, expects you to add your own `breadcrumbs` field to each collection manually. Set this property to the `name` of your custom field. **Note:** If you opt out of automatically being provided a `parent` or `breadcrumbs` field, you need to make sure that both fields are placed at the top-level of your document. They cannot exist within any nested data structures like a `group`, `array`, or `blocks`. ## Overrides You can also extend the built-in `parent` and `breadcrumbs` fields per collection by using the `createParentField` and `createBreadcrumbField` methods. They will merge your customizations overtop the plugin's base field configurations. ```ts import type { CollectionConfig } from 'payload' import { createParentField } from '@payloadcms/plugin-nested-docs' import { createBreadcrumbsField } from '@payloadcms/plugin-nested-docs' const examplePageConfig: CollectionConfig = { slug: 'pages', fields: [ createParentField( // First argument is equal to the slug of the collection // that the field references 'pages', // Second argument is equal to field overrides that you specify, // which will be merged into the base parent field config { admin: { position: 'sidebar', }, // Note: if you override the `filterOptions` of the `parent` field, // be sure to continue to prevent the document from referencing itself as the parent like this: // filterOptions: ({ id }) => ({ id: {not_equals: id }}) }, ), createBreadcrumbsField( // First argument is equal to the slug of the collection // that the field references 'pages', // Argument equal to field overrides that you specify, // which will be merged into the base `breadcrumbs` field config { label: 'Page Breadcrumbs', }, ), ], } ``` **Note:** If overriding the `name` of either `breadcrumbs` or `parent` fields, you must specify the `breadcrumbsFieldSlug` or `parentFieldSlug` respectively. ## Localization This plugin supports localization by default. If the `localization` property is set in your Payload Config, the `breadcrumbs` field is automatically localized. For more details on how localization works in Payload, see the [Localization](../configuration/localization) docs. ## TypeScript All types can be directly imported: ```ts import { PluginConfig, GenerateURL, GenerateLabel, } from '@payloadcms/plugin-nested-docs/types' ``` ## Examples The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) also contains an official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website) and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce), both of which use this plugin. # Redirects Plugin Source: https://payloadcms.com/docs/plugins/redirects ![https://www.npmjs.com/package/@payloadcms/plugin-redirects](https://img.shields.io/npm/v/@payloadcms/plugin-redirects) This plugin allows you to easily manage redirects for your application from within your [Admin Panel](../admin/overview). It does so by adding a `redirects` collection to your config that allows you specify a redirect from one URL to another. Your front-end application can use this data to automatically redirect users to the correct page using proper HTTP status codes. This is useful for SEO, indexing, and search engine ranking when re-platforming or when changing your URL structure. For example, if you have a page at `/about` and you want to change it to `/about-us`, you can create a redirect from the old page to the new one, then you can use this data to write HTTP redirects into your front-end application. This will ensure that users are redirected to the correct page without penalty because search engines are notified of the change at the request level. This is a very lightweight plugin that will allow you to integrate managed redirects for any front-end framework. This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-redirects). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%redirects&template=bug_report.md&title=plugin-redirects%3A) with as much detail as possible. ## Core features - Adds a `redirects` collection to your config that: - includes a `from` and `to` fields - allows `to` to be a document reference ## Installation Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com): ```bash pnpm add @payloadcms/plugin-redirects ``` ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin with [options](#options): ```ts import { buildConfig } from 'payload' import { redirectsPlugin } from '@payloadcms/plugin-redirects' const config = buildConfig({ collections: [ { slug: 'pages', fields: [], }, ], plugins: [ redirectsPlugin({ collections: ['pages'], }), ], }) export default config ``` ### Options | Option | Type | Description | | --------------------------- | ---------- | ------------------------------------------------------------------------------------------------------- | | `collections` | `string[]` | An array of collection slugs to populate in the `to` field of each redirect. | | `overrides` | `object` | A partial collection config that allows you to override anything on the `redirects` collection. | | `redirectTypes` | `string[]` | Provide an array of redirects if you want to provide options for the type of redirects to be supported. | | `redirectTypeFieldOverride` | `Field` | A partial Field config that allows you to override the Redirect Type field if enabled above. | Note that the fields in overrides take a function that receives the default fields and returns an array of fields. This allows you to add fields to the collection. ```ts redirectsPlugin({ collections: ['pages'], overrides: { fields: ({ defaultFields }) => { return [ ...defaultFields, { type: 'text', name: 'customField', }, ] }, }, redirectTypes: ['301', '302'], redirectTypeFieldOverride: { label: 'Redirect Type (Overridden)', }, }) ``` ## TypeScript All types can be directly imported: ```ts import { PluginConfig } from '@payloadcms/plugin-redirects/types' ``` ## Examples The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) also contains an official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website) and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce), both of which use this plugin. # Search Plugin Source: https://payloadcms.com/docs/plugins/search ![https://www.npmjs.com/package/@payloadcms/plugin-search](https://img.shields.io/npm/v/@payloadcms/plugin-search) This plugin generates records of your documents that are extremely fast to search on. It does so by creating a new `search` collection that is indexed in the database then saving a static copy of each of your documents using only search-critical data. Search records are automatically created, synced, and deleted behind-the-scenes as you manage your application's documents. For example, if you have a posts collection that is extremely large and complex, this would allow you to sync just the title, excerpt, and slug of each post so you can query on _that_ instead of the original post directly. Search records are static, so querying them also has the significant advantage of bypassing any hooks that may present be on the original documents. You define exactly what data is synced, and you can even modify or fallback this data before it is saved on a per-document basis. To query search results, use all the existing Payload APIs that you are already familiar with. You can also prioritize search results by setting a custom priority for each collection. For example, you may want to list blog posts before pages. Or you may want one specific post to always take appear first. Search records are given a `priority` field that can be used as the `?sort=` parameter in your queries. This plugin is a great way to implement a fast, immersive search experience such as a search bar in a front-end application. Many applications may not need the power and complexity of a third-party service like Algolia or ElasticSearch. This plugin provides a first-party alternative that is easy to set up and runs entirely on your own database. This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-search). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20search&template=bug_report.md&title=plugin-search%3A) with as much detail as possible. ## Core Features - Automatically adds an indexed `search` collection to your database - Automatically creates, syncs, and deletes search records as you manage your documents - Saves only search-critical data that you define (e.g. title, excerpt, etc.) - Allows you to query search results using first-party Payload APIs - Allows you to query documents without triggering any of their underlying hooks - Allows you to easily prioritize search results by collection or document - Allows you to reindex search results by collection on demand ## Installation Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com): ```bash pnpm add @payloadcms/plugin-search ``` ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin with [options](#options): ```js import { buildConfig } from 'payload' import { searchPlugin } from '@payloadcms/plugin-search' const config = buildConfig({ collections: [ { slug: 'pages', fields: [], }, { slug: 'posts', fields: [], }, ], plugins: [ searchPlugin({ collections: ['pages', 'posts'], defaultPriorities: { pages: 10, posts: 20, }, }), ], }) export default config ``` ### Options #### `collections` The `collections` property is an array of collection slugs to enable syncing to search. Enabled collections receive a `beforeChange` and `afterDelete` hook that creates, updates, and deletes its respective search record as it changes over time. #### `localize` By default, the search plugin will add `localization: true` to the `title` field of the newly added `search` collection if you have localization enabled. If you would like to disable this behavior, you can set this to `false`. #### `defaultPriorities` This plugin automatically adds a `priority` field to the `search` collection that can be used as the `?sort=` parameter in your queries. For example, you may want to list blog posts before pages. Or you may want one specific post to always take appear first. The `defaultPriorities` property is used to set a fallback `priority` on search records during the `create` operation. It accepts an object with keys that are your collection slugs and values that can either be a number or a function that returns a number. The function receives the `doc` as an argument, which is the document being created. ```ts // payload.config.ts { // ... searchPlugin({ defaultPriorities: { pages: ({ doc }) => (doc.title.startsWith('Hello, world!') ? 1 : 10), posts: 20, }, }), } ``` #### `searchOverrides` This plugin automatically creates the `search` collection, but you can override anything on this collection via the `searchOverrides` property. It accepts anything from the [Payload Collection Config](../configuration/collections) and merges it in with the default `search` collection config provided by the plugin. Note that the `fields` property is a function that receives an object with a `defaultFields` key. This is an array of fields that are automatically added to the `search` collection. You can modify this array or add new fields to it. ```ts // payload.config.ts { // ... searchPlugin({ searchOverrides: { slug: 'search-results', fields: ({ defaultFields }) => [ ...defaultFields, { name: 'excerpt', type: 'textarea', admin: { position: 'sidebar', }, }, ], }, }), } ``` #### `beforeSync` Before creating or updating a search record, the `beforeSync` function runs. This is an [afterChange](../hooks/globals#afterchange) hook that allows you to modify the data or provide fallbacks before its search record is created or updated. ```ts // payload.config.ts { // ... searchPlugin({ beforeSync: ({ originalDoc, searchDoc }) => ({ ...searchDoc, // - Modify your docs in any way here, this can be async // - You also need to add the `excerpt` field in the `searchOverrides` config excerpt: originalDoc?.excerpt || 'This is a fallback excerpt', }), }), } ``` #### `syncDrafts` When `syncDrafts` is true, draft documents will be synced to search. This is false by default. You must have [Payload Drafts](../versions/drafts) enabled for this to apply. #### `deleteDrafts` If true, will delete documents from search whose status changes to draft. This is true by default. You must have [Payload Drafts](../versions/drafts) enabled for this to apply. #### `reindexBatchSize` A number that, when specified, will be used as the value to determine how many search documents to fetch for reindexing at a time in each batch. If not set, this will default to `50`. ### Collection reindexing Collection reindexing allows you to recreate search documents from your search-enabled collections on demand. This is useful if you have existing documents that don't already have search indexes, commonly when adding `plugin-search` to an existing project. To get started, navigate to your search collection and click the pill in the top right actions slot of the list view labelled `Reindex`. This will open a popup with options to select one of your search-enabled collections, or all, for reindexing. ## TypeScript All types can be directly imported: ```ts import type { SearchConfig, BeforeSync } from '@payloadcms/plugin-search/types' ``` # Sentry Plugin Source: https://payloadcms.com/docs/plugins/sentry ![https://www.npmjs.com/package/@payloadcms/plugin-sentry](https://img.shields.io/npm/v/@payloadcms/plugin-sentry) This plugin allows you to integrate [Sentry](https://sentry.io/) seamlessly with your [Payload](https://github.com/payloadcms/payload) application. ## What is Sentry? Sentry is a powerful error tracking and performance monitoring tool that helps developers identify, diagnose, and resolve issues in their applications. Sentry does smart stuff with error data to make bugs easier to find and fix. - [sentry.io](https://sentry.io/) This multi-faceted software offers a range of features that will help you manage errors with greater ease and ultimately ensure your application is running smoothly: ## Core Features - **Error Tracking**: Instantly captures and logs errors as they occur in your application - **Performance Monitoring**: Tracks application performance to identify slowdowns and bottlenecks - **Detailed Reports**: Provides comprehensive insights into errors, including stack traces and context - **Alerts and Notifications**: Send and customize event-triggered notifications - **Issue Grouping, Filtering and Search**: Automatically groups similar errors, and allows filtering and searching issues by custom criteria - **Breadcrumbs**: Records user actions and events leading up to an error - **Integrations**: Connects with various tools and services for enhanced workflow and issue management This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-sentry). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20seo&template=bug_report.md&title=plugin-sentry%3A) with as much detail as possible. ## Installation Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com): ```bash pnpm add @payloadcms/plugin-sentry ``` ## Sentry for Next.js setup This plugin requires to complete the [Sentry + Next.js setup](https://docs.sentry.io/platforms/javascript/guides/nextjs/) before. You can use either the [automatic setup](https://docs.sentry.io/platforms/javascript/guides/nextjs/#install) with the installation wizard: ```sh npx @sentry/wizard@latest -i nextjs ``` Or the [Manual Setup](https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/) ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin and pass in your Sentry DSN as an option. ```ts import { buildConfig } from 'payload' import { sentryPlugin } from '@payloadcms/plugin-sentry' import { Pages, Media } from './collections' import * as Sentry from '@sentry/nextjs' const config = buildConfig({ collections: [Pages, Media], plugins: [sentryPlugin({ Sentry })], }) export default config ``` ## Instrumenting Database Queries If you want Sentry to capture Postgres query performance traces, you need to inject the Sentry-patched `pg` driver into the Postgres adapter. This ensures Sentry’s instrumentation hooks into your database calls. ```ts import * as Sentry from '@sentry/nextjs' import { buildConfig } from 'payload' import { sentryPlugin } from '@payloadcms/plugin-sentry' import { postgresAdapter } from '@payloadcms/db-postgres' import pg from 'pg' export default buildConfig({ db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URL }, pg, // Inject the patched pg driver for Sentry instrumentation }), plugins: [sentryPlugin({ Sentry })], }) ``` ## Options - `Sentry` : Sentry | **required** The `Sentry` instance Make sure to complete the [Sentry for Next.js Setup](#sentry-for-nextjs-setup) before. - `enabled`: boolean | optional Set to false to disable the plugin. Defaults to `true`. - `context`: `(args: ContextArgs) => Partial | Promise>` Pass additional [contextual data](https://docs.sentry.io/platforms/javascript/enriching-events/context/#passing-context-directly) to Sentry - `captureErrors`: number[] | optional By default, `Sentry.errorHandler` will capture only errors with a status code of 500 or higher. To capture additional error codes, pass the values as numbers in an array. ### Example Configure any of these options by passing them to the plugin: ```ts import { buildConfig } from 'payload' import { sentryPlugin } from '@payloadcms/plugin-sentry' import * as Sentry from '@sentry/nextjs' import { Pages, Media } from './collections' const config = buildConfig({ collections: [Pages, Media], plugins: [ sentryPlugin({ options: { captureErrors: [400, 403], context: ({ defaultContext, req }) => { return { ...defaultContext, tags: { locale: req.locale, }, } }, debug: true, }, Sentry, }), ], }) export default config ``` ## TypeScript All types can be directly imported: ```ts import { PluginOptions } from '@payloadcms/plugin-sentry' ``` # SEO Plugin Source: https://payloadcms.com/docs/plugins/seo ![https://www.npmjs.com/package/@payloadcms/plugin-seo](https://img.shields.io/npm/v/@payloadcms/plugin-seo) This plugin allows you to easily manage SEO metadata for your application from within your [Admin Panel](../admin/overview). When enabled on your [Collections](../configuration/collections) and [Globals](../configuration/globals), it adds a new `meta` field group containing `title`, `description`, and `image` by default. Your front-end application can then use this data to render meta tags however your application requires. For example, you would inject a `title` tag into the `` of your page using `meta.title` as its content. As users are editing documents within the Admin Panel, they have the option to "auto-generate" these fields. When clicked, this plugin will execute your own custom functions that re-generate the title, description, and image. This way you can build your own SEO writing assistance directly into your application. For example, you could append your site name onto the page title, or use the document's excerpt field as the description, or even integrate with some third-party API to generate the image using AI. To help you visualize what your page might look like in a search engine, a preview is rendered on the page just beneath the meta fields. This preview is updated in real-time as you edit your metadata. There are also visual indicators to help you write effective meta, such as a character counter for the title and description fields. You can even inject your own custom fields into the `meta` field group as your application requires, like `og:title` or `json-ld`. If you've ever used something like Yoast SEO, this plugin might feel very familiar. This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-seo). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20seo&template=bug_report.md&title=plugin-seo%3A) with as much detail as possible. ## Core features - Adds a `meta` field group to every SEO-enabled collection or global - Allows you to define custom functions to auto-generate metadata - Displays hints and indicators to help content editor write effective meta - Renders a snippet of what a search engine might display - Extendable so you can define custom fields like `og:title` or `json-ld` - Soon will support dynamic variable injection ## Installation Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com): ```bash pnpm add @payloadcms/plugin-seo ``` ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin with [options](#options): ```ts import { buildConfig } from 'payload'; import { seoPlugin } from '@payloadcms/plugin-seo'; const config = buildConfig({ collections: [ { slug: 'pages', fields: [] }, { slug: 'media', upload: { staticDir: // path to your static directory, }, fields: [] } ], plugins: [ seoPlugin({ collections: [ 'pages', ], uploadsCollection: 'media', generateTitle: ({ doc }) => `Website.com — ${doc.title}`, generateDescription: ({ doc }) => doc.excerpt }) ] }); export default config; ``` ### Options ##### `collections` An array of collections slugs to enable SEO. Enabled collections receive a `meta` field which is an object of title, description, and image subfields. ##### `globals` An array of global slugs to enable SEO. Enabled globals receive a `meta` field which is an object of title, description, and image subfields. ##### `fields` A function that takes in the default fields via an object and expects an array of fields in return. You can use this to modify existing fields or add new ones. ```ts // payload.config.ts { // ... seoPlugin({ fields: ({ defaultFields }) => [ ...defaultFields, { name: 'customField', type: 'text', }, ], }) } ``` ##### `uploadsCollection` Set the `uploadsCollection` to your application's upload-enabled collection slug. This is used to provide an `image` field on the `meta` field group. ##### `tabbedUI` When the `tabbedUI` property is `true`, it appends an `SEO` tab onto your config using Payload's [Tabs Field](../fields/tabs). If your collection is not already tab-enabled, meaning the first field in your config is not of type `tabs`, then one will be created for you called `Content`. Defaults to `false`. Note that the order of plugins or fields in your config may affect whether or not the plugin can smartly merge tabs with your existing fields. If you have a complex structure we recommend you [make use of the fields directly](#direct-use-of-fields) instead of relying on this config option. If you wish to continue to use top-level or sidebar fields with `tabbedUI`, you must not let the default `Content` tab get created for you (see the note above). Instead, you must define the first field of your config with type `tabs` and place all other fields adjacent to this one. ##### `generateTitle` A function that allows you to return any meta title, including from the document's content. ```ts // payload.config.ts { // ... seoPlugin({ generateTitle: ({ doc }) => `Website.com — ${doc?.title}`, }) } ``` All "generate" functions receive the following arguments: | Argument | Description | | -------------------------- | --------------------------------------------------------------------- | | **`collectionConfig`** | The configuration of the collection. | | **`collectionSlug`** | The slug of the collection. | | **`doc`** | The data of the current document. | | **`docPermissions`** | The permissions of the document. | | **`globalConfig`** | The configuration of the global. | | **`globalSlug`** | The slug of the global. | | **`hasPublishPermission`** | Whether the user has permission to publish the document. | | **`hasSavePermission`** | Whether the user has permission to save the document. | | **`id`** | The ID of the document. | | **`initialData`** | The initial data of the document. | | **`initialState`** | The initial state of the document. | | **`locale`** | The locale of the document. | | **`preferencesKey`** | The preferences key of the document. | | **`publishedDoc`** | The published document. | | **`req`** | The Payload request object containing `user`, `payload`, `i18n`, etc. | | **`title`** | The title of the document. | | **`versionsCount`** | The number of versions of the document. | ##### `generateDescription` A function that allows you to return any meta description, including from the document's content. ```ts // payload.config.ts { // ... seoPlugin({ generateDescription: ({ doc }) => doc?.excerpt, }) } ``` For a full list of arguments, see the [`generateTitle`](#generatetitle) function. ##### `generateImage` A function that allows you to return any meta image, including from the document's content. ```ts // payload.config.ts { // ... seoPlugin({ generateImage: ({ doc }) => doc?.featuredImage, }) } ``` For a full list of arguments, see the [`generateTitle`](#generatetitle) function. ##### `generateURL` A function called by the search preview component to display the actual URL of your page. ```ts // payload.config.ts { // ... seoPlugin({ generateURL: ({ doc, collectionSlug }) => `https://yoursite.com/${collectionSlug}/${doc?.slug}`, }) } ``` For a full list of arguments, see the [`generateTitle`](#generatetitle) function. #### `interfaceName` Rename the meta group interface name that is generated for TypeScript and GraphQL. ```ts // payload.config.ts { // ... seoPlugin({ interfaceName: 'customInterfaceNameSEO', }) } ``` ## Direct use of fields There is the option to directly import any of the fields from the plugin so that you can include them anywhere as needed. You will still need to configure the plugin in the Payload Config in order to configure the generation functions. Since these fields are imported and used directly, they don't have access to the plugin config so they may need additional arguments to work the same way. ```ts import { MetaDescriptionField, MetaImageField, MetaTitleField, OverviewField, PreviewField, } from '@payloadcms/plugin-seo/fields' // Used as fields MetaImageField({ // the upload collection slug relationTo: 'media', // if the `generateImage` function is configured hasGenerateFn: true, }) MetaDescriptionField({ // if the `generateDescription` function is configured hasGenerateFn: true, }) MetaTitleField({ // if the `generateTitle` function is configured hasGenerateFn: true, }) PreviewField({ // if the `generateUrl` function is configured hasGenerateFn: true, // field paths to match the target field for data titlePath: 'meta.title', descriptionPath: 'meta.description', }) OverviewField({ // field paths to match the target field for data titlePath: 'meta.title', descriptionPath: 'meta.description', imagePath: 'meta.image', }) ``` Tip: You can override the length rules by changing the minLength and maxLength props on the fields. In the case of the OverviewField you can use `titleOverrides` and `descriptionOverrides` to override the length rules. ## TypeScript All types can be directly imported: ```ts import type { PluginConfig, GenerateTitle, GenerateDescription GenerateURL } from '@payloadcms/plugin-seo/types'; ``` You can then pass the collections from your generated Payload types into the generation types, for example: ```ts import type { Page } from './payload-types.ts' import type { GenerateTitle } from '@payloadcms/plugin-seo/types' const generateTitle: GenerateTitle = async ({ doc, locale }) => { return `Website.com — ${doc?.title}` } ``` ## Examples The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) contains an official [Website Template](https://github.com/payloadcms/payload/tree/main/templates/website) and [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommere) which demonstrates exactly how to configure this plugin in Payload and implement it on your front-end. ## Screenshots ![image](https://user-images.githubusercontent.com/70709113/163850633-f3da5f8e-2527-4688-bc79-17233307a883.png) # Stripe Plugin Source: https://payloadcms.com/docs/plugins/stripe ![https://www.npmjs.com/package/@payloadcms/plugin-stripe](https://img.shields.io/npm/v/@payloadcms/plugin-stripe) With this plugin you can easily integrate [Stripe](https://stripe.com) into Payload. Simply provide your Stripe credentials and this plugin will open up a two-way communication channel between the two platforms. This enables you to easily sync data back and forth, as well as proxy the Stripe REST API through Payload's [Access Control](../access-control/overview). Use this plugin to completely offload billing to Stripe and retain full control over your application's data. For example, you might be building an e-commerce or SaaS application, where you have a `products` or a `plans` collection that requires either a one-time payment or a subscription. You can to tie each of these products to Stripe, then easily subscribe to billing-related events to perform your application's business logic, such as active purchases or subscription cancellations. To build a checkout flow on your front-end you can either use [Stripe Checkout](https://stripe.com/payments/checkout), or you can also build a completely custom checkout experience from scratch using [Stripe Web Elements](https://stripe.com/docs/payments/elements). Then to build fully custom, secure customer dashboards, you can leverage Payload's Access Control to restrict access to your Stripe resources so your users never have to leave your site to manage their accounts. The beauty of this plugin is the entirety of your application's content and business logic can be handled in Payload while Stripe handles solely the billing and payment processing. You can build a completely proprietary application that is endlessly customizable and extendable, on APIs and databases that you own. Hosted services like Shopify or BigCommerce might fracture your application's content then charge you for access. This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-stripe). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%20stripe&template=bug_report.md&title=plugin-stripe%3A) with as much detail as possible. ## Core features - Hides your Stripe credentials when shipping SaaS applications - Allows restricted keys through [Payload access control](../access-control/overview) - Enables a two-way communication channel between Stripe and Payload - Proxies the [Stripe REST API](https://stripe.com/docs/api) - Proxies [Stripe webhooks](https://stripe.com/docs/webhooks) - Automatically syncs data between the two platforms ## Installation Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com): ```bash pnpm add @payloadcms/plugin-stripe ``` ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin with [options](#options): ```ts import { buildConfig } from 'payload' import { stripePlugin } from '@payloadcms/plugin-stripe' const config = buildConfig({ plugins: [ stripePlugin({ stripeSecretKey: process.env.STRIPE_SECRET_KEY, }), ], }) export default config ``` ### Options | Option | Type | Default | Description | | ------------------------------ | ------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------ | | `stripeSecretKey` \* | string | `undefined` | Your Stripe secret key | | `stripeWebhooksEndpointSecret` | string | `undefined` | Your Stripe webhook endpoint secret | | `rest` | boolean | `false` | When `true`, opens the `/api/stripe/rest` endpoint | | `webhooks` | object or function | `undefined` | Either a function to handle all webhooks events, or an object of Stripe webhook handlers, keyed to the name of the event | | `sync` | array | `undefined` | An array of sync configs | | `logs` | boolean | `false` | When `true`, logs sync events to the console as they happen | _\* An asterisk denotes that a property is required._ ## Endpoints The following custom endpoints are automatically opened for you: | Endpoint | Method | Description | | ---------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `/api/stripe/rest` | `POST` | Proxies the [Stripe REST API](https://stripe.com/docs/api) behind [Payload access control](../access-control/overview) and returns the result. See the [REST Proxy](#stripe-rest-proxy) section for more details. | | `/api/stripe/webhooks` | `POST` | Handles all Stripe webhook events | ##### Stripe REST Proxy If `rest` is true, proxies the [Stripe REST API](https://stripe.com/docs/api) behind [Payload access control](../access-control/overview) and returns the result. This flag should only be used for local development, see the security note below for more information. ```ts const res = await fetch(`/api/stripe/rest`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', // Authorization: `JWT ${token}` // NOTE: do this if not in a browser (i.e. curl or Postman) }, body: JSON.stringify({ stripeMethod: 'stripe.subscriptions.list', stripeArgs: [ { customer: 'abc', }, ], }), }) ``` If you need to proxy the API server-side, use the [stripeProxy](#node) function. **Note:** The `/api` part of these routes may be different based on the settings defined in your Payload config. **Warning:** Opening the REST proxy endpoint in production is a potential security risk. Authenticated users will have open access to the Stripe REST API. In production, open your own endpoint and use the [stripeProxy](#node) function to proxy the Stripe API server-side. ## Webhooks [Stripe webhooks](https://stripe.com/docs/webhooks) are used to sync from Stripe to Payload. Webhooks listen for events on your Stripe account so you can trigger reactions to them. Follow the steps below to enable webhooks. Development: 1. Login using Stripe cli `stripe login` 1. Forward events to localhost `stripe listen --forward-to localhost:3000/api/stripe/webhooks` 1. Paste the given secret into your `.env` file as `STRIPE_WEBHOOKS_ENDPOINT_SECRET` Production: 1. Login and [create a new webhook](https://dashboard.stripe.com/test/webhooks/create) from the Stripe dashboard 1. Paste `YOUR_DOMAIN_NAME/api/stripe/webhooks` as the "Webhook Endpoint URL" 1. Select which events to broadcast 1. Paste the given secret into your `.env` file as `STRIPE_WEBHOOKS_ENDPOINT_SECRET` 1. Then, handle these events using the `webhooks` portion of this plugin's config: ```ts import { buildConfig } from 'payload' import stripePlugin from '@payloadcms/plugin-stripe' const config = buildConfig({ plugins: [ stripePlugin({ stripeSecretKey: process.env.STRIPE_SECRET_KEY, stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOKS_ENDPOINT_SECRET, webhooks: { 'customer.subscription.updated': ({ event, stripe, stripeConfig }) => { // do something... }, }, // NOTE: you can also catch all Stripe webhook events and handle the event types yourself // webhooks: (event, stripe, stripeConfig) => { // switch (event.type): { // case 'customer.subscription.updated': { // // do something... // break; // } // default: { // break; // } // } // } }), ], }) export default config ``` For a full list of available webhooks, see [here](https://stripe.com/docs/cli/trigger#trigger-event). ## Node On the server you should interface with Stripe directly using the [stripe](https://www.npmjs.com/package/stripe) npm module. That might look something like this: ```ts import Stripe from 'stripe' const stripeSecretKey = process.env.STRIPE_SECRET_KEY const stripe = new Stripe(stripeSecretKey, { apiVersion: '2022-08-01', }) export const MyFunction = async () => { try { const customer = await stripe.customers.create({ email: data.email, }) // do something... } catch (error) { console.error(error.message) } } ``` Alternatively, you can interface with the Stripe using the `stripeProxy`, which is exactly what the `/api/stripe/rest` endpoint does behind-the-scenes. Here's the same example as above, but piped through the proxy: ```ts import { stripeProxy } from '@payloadcms/plugin-stripe' export const MyFunction = async () => { try { const customer = await stripeProxy({ stripeSecretKey: process.env.STRIPE_SECRET_KEY, stripeMethod: 'customers.create', stripeArgs: [ { email: data.email, }, ], }) if (customer.status === 200) { // do something... } if (customer.status >= 400) { throw new Error(customer.message) } } catch (error) { console.error(error.message) } } ``` ## Sync This option will setup a basic sync between Payload collections and Stripe resources for you automatically. It will create all the necessary hooks and webhooks handlers, so the only thing you have to do is map your Payload fields to their corresponding Stripe properties. As documents are created, updated, and deleted from either Stripe or Payload, the changes are reflected on either side. **Note:** If you wish to enable a _two-way_ sync, be sure to setup [`webhooks`](#webhooks) and pass the `stripeWebhooksEndpointSecret` through your config. ```ts import { buildConfig } from 'payload' import stripePlugin from '@payloadcms/plugin-stripe' const config = buildConfig({ plugins: [ stripePlugin({ stripeSecretKey: process.env.STRIPE_SECRET_KEY, stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOKS_ENDPOINT_SECRET, sync: [ { collection: 'customers', stripeResourceType: 'customers', stripeResourceTypeSingular: 'customer', fields: [ { fieldPath: 'name', // this is a field on your own Payload Config stripeProperty: 'name', // use dot notation, if applicable }, ], }, ], }), ], }) export default config ``` **Note:** Due to limitations in the Stripe API, this currently only works with top-level fields. This is because every Stripe object is a separate entity, making it difficult to abstract into a simple reusable library. In the future, we may find a pattern around this. But for now, cases like that will need to be hard-coded. Using `sync` will do the following: - Adds and maintains a `stripeID` read-only field on each collection, this is a field generated _by Stripe_ and used as a cross-reference - Adds a direct link to the resource on Stripe.com - Adds and maintains an `skipSync` read-only flag on each collection to prevent infinite syncs when hooks trigger webhooks - Adds the following hooks to each collection: - `beforeValidate`: `createNewInStripe` - `beforeChange`: `syncExistingWithStripe` - `afterDelete`: `deleteFromStripe` - Handles the following Stripe webhooks - `STRIPE_TYPE.created`: `handleCreatedOrUpdated` - `STRIPE_TYPE.updated`: `handleCreatedOrUpdated` - `STRIPE_TYPE.deleted`: `handleDeleted` ## TypeScript All types can be directly imported: ```ts import { StripeConfig, StripeWebhookHandler, StripeProxy, ... } from '@payloadcms/plugin-stripe/types'; ``` # Ecommerce Overview Source: https://payloadcms.com/docs/ecommerce/overview ![https://www.npmjs.com/package/@payloadcms/plugin-ecommerce](https://img.shields.io/npm/v/@payloadcms/plugin-ecommerce) This plugin is currently in Beta and may have breaking changes in future releases. Payload provides an Ecommerce Plugin that allows you to add ecommerce functionality to your app. It comes a set of utilities and collections to manage products, orders, and payments. It also integrates with popular payment gateways like Stripe to handle transactions. This plugin is completely open-source and the [source code can be found here](https://github.com/payloadcms/payload/tree/main/packages/plugin-ecommerce). If you need help, check out our [Community Help](https://payloadcms.com/community-help). If you think you've found a bug, please [open a new issue](https://github.com/payloadcms/payload/issues/new?assignees=&labels=plugin%3A%redirects&template=bug_report.md&title=plugin-ecommerce%3A) with as much detail as possible. ## Core features The plugin ships with a wide range of features to help you get started with ecommerce: - Products with Variants are supported by default - Carts are tracked in Payload - Orders and Transactions - Addresses linked to your Customers - Payments adapter pattern to create your own integrations (Stripe currently supported) - Multiple currencies are supported - React UI utilities to help you manage your frontend logic _Currently_ the plugin does not handle shipping, taxes or subscriptions natively, but you can implement these features yourself using the provided collections and hooks. ## Installation Install the plugin using any JavaScript package manager like [pnpm](https://pnpm.io), [npm](https://npmjs.com), or [Yarn](https://yarnpkg.com): ```bash pnpm add @payloadcms/plugin-ecommerce ``` ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin with: ```ts import { buildConfig } from 'payload' import { ecommercePlugin } from '@payloadcms/plugin-ecommerce' const config = buildConfig({ collections: [ { slug: 'pages', fields: [], }, ], plugins: [ ecommercePlugin({ // You must add your access control functions here access: { adminOnly, adminOnlyFieldAccess, adminOrCustomerOwner, adminOrPublishedStatus, customerOnlyFieldAccess, }, customers: { slug: 'users' }, }), ], }) export default config ``` ## Concepts It's important to understand overall how the plugin works and the relationships between the different collections. **Customers** Can be any collection of users in your application. You can then limit access control only to customers depending on individual fields such as roles on the customer collection or by collection slug if you've opted to keep them separate. Customers are linked to Carts and Orders. **Products and Variants** Products are the items you are selling and they will contain a price and optionally variants via a join field as well as allowed Variant Types. Each Variant Type can contain a set of Variant Options. For example, a T-Shirt product can have a Variant Type of Size with options Small, Medium, and Large and each Variant can therefore have those options assigned to it. **Carts** Carts are linked to Customers or they're left entirely public for guests users and can contain multiple Products and Variants. Carts are stored in the database and can be retrieved at any time. Carts are automatically created for Customers when they add a product to their cart for the first time. **Transactions and Orders** Transactions are created when a payment is initiated. They contain the payment status and are linked to a Cart and Customer. Orders are created when a Transaction is successful and contain the final details of the purchase including the items, total, and customer information. **Addresses** Addresses are linked to Customers and can be used for billing and shipping information. They can be reused across multiple Orders. **Payments** The plugin uses an adapter pattern to allow for different payment gateways. The default adapter is for Stripe, but you can create your own by implementing the `PaymentAdapter` interface. **Currencies** The plugin supports using multiple currencies at the configuration level. Each currency will create a separate price field on the Product and Variants collections. The package can also be used piece-meal if you only want to re-use certain parts of it, such as just the creation of Products and Variants. See [Advanced uses and examples](./advanced) for more details. ## TypeScript The plugin will inherit the types from your generated Payload types where possible. We also export the following types: - `Cart` - The cart type as stored in the React state and local storage and on the client side. - `CollectionOverride` - Type for overriding collections. - `CurrenciesConfig` - Type for the currencies configuration. - `EcommercePluginConfig` - The configuration object for the ecommerce plugin. - `FieldsOverride` - Type for overriding fields in collections. All types can be directly imported: ```ts import { EcommercePluginConfig } from '@payloadcms/plugin-ecommerce/types' ``` ## Template The [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) also contains an official [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce), which uses this plugin. # Ecommerce Plugin Source: https://payloadcms.com/docs/ecommerce/plugin ## Basic Usage In the `plugins` array of your [Payload Config](../configuration/overview), call the plugin with: ```ts import { buildConfig } from 'payload' import { ecommercePlugin } from '@payloadcms/plugin-ecommerce' const config = buildConfig({ collections: [ { slug: 'pages', fields: [], }, ], plugins: [ ecommercePlugin({ // You must add your access control functions here access: { adminOnly, adminOnlyFieldAccess, adminOrCustomerOwner, adminOrPublishedStatus, customerOnlyFieldAccess, }, customers: { slug: 'users' }, }), ], }) export default config ``` ## Options | Option | Type | Description | | -------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------ | | `access` | `object` | Configuration to override the default access control, use this when checking for roles or multi tenancy. [More](#access) | | `addresses` | `object` | Configuration for addresses collection and supported fields. [More](#addresses) | | `carts` | `object` | Configuration for carts collection. [More](#carts) | | `currencies` | `object` | Supported currencies by the store. [More](#currencies) | | `customers` | `object` | Used to provide the customers slug. [More](#customers) | | `inventory` | `boolean` `object` | Enable inventory tracking within Payload. Defaults to `true`. [More](#inventory) | | `payments` | `object` | Configuring payments and supported payment methods. [More](#payments) | | `products` | `object` | Configuration for products, variants collections and more. [More](#products) | | `orders` | `object` | Configuration for orders collection. [More](#orders) | | `transactions` | `boolean` `object` | Configuration for transactions collection. [More](#transactions) | Note that the fields in overrides take a function that receives the default fields and returns an array of fields. This allows you to add fields to the collection. ```ts ecommercePlugin({ access: { adminOnly, adminOnlyFieldAccess, adminOrCustomerOwner, adminOrPublishedStatus, customerOnlyFieldAccess, }, customers: { slug: 'users', }, payments: { paymentMethods: [ stripeAdapter({ secretKey: process.env.STRIPE_SECRET_KEY!, publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET!, }), ], }, products: { variants: { variantsCollection: VariantsCollection, }, productsCollection: ProductsCollection, }, }) ``` ## Access The plugin requires access control functions in order to restrict permissions to certain collections or fields. You can override these functions by providing your own in the `access` option. | Option | Type | Description | | ------------------------- | ------------- | ------------------------------------------------------------------------- | | `authenticatedOnly` | `Access` | Authenticated access only, provided by default. | | `publicAccess` | `Access` | Public access, provided by default. | | `adminOnly` | `Access` | Limited to only admin users. | | `adminOnlyFieldAccess` | `FieldAccess` | Limited to only admin users, specifically for Field level access control. | | `adminOrCustomerOwner` | `Access` | Is the owner of the document via the `customer` field or is an admin. | | `adminOrPublishedStatus` | `Access` | The document is published or user is admin. | | `customerOnlyFieldAccess` | `FieldAccess` | Limited to customers only, specifically for Field level access control. | The default access control functions are: ```ts access: { authenticatedOnly: ({ req: { user } }) => Boolean(user), publicAccess: () => true, } ``` ### authenticatedOnly Access control to check if the user is authenticated. By default the following is provided: ```ts authenticatedOnly: ({ req: { user } }) => Boolean(user) ``` ### publicAccess Access control to allow public access. By default the following is provided: ```ts publicAccess: () => true ``` ### adminOnly Access control to check if the user has `admin` permissions. Example: ```ts adminOnly: ({ req: { user } }) => Boolean(user?.roles?.includes('admin')) ``` ### adminOnlyFieldAccess Field level access control to check if the user has `admin` permissions. Example: ```ts adminOnlyFieldAccess: ({ req: { user } }) => Boolean(user?.roles?.includes('admin')) ``` ### adminOrCustomerOwner Access control to check if the user has `admin` permissions or is the owner of the document via the `customer` field. Example: ```ts adminOrCustomerOwner: ({ req: { user } }) => { if (user && Boolean(user?.roles?.includes('admin'))) { return true } if (user?.id) { return { customer: { equals: user.id, }, } } return false } ``` ### adminOrPublishedStatus Access control to check if the user has `admin` permissions or if the document is published. Example: ```ts adminOrPublishedStatus: ({ req: { user } }) => { if (user && Boolean(user?.roles?.includes('admin'))) { return true } return { _status: { equals: 'published', }, } } ``` ### customerOnlyFieldAccess Field level access control to check if the user has `customer` permissions. Example: ```ts customerOnlyFieldAccess: ({ req: { user } }) => Boolean(user?.roles?.includes('customer')) ``` ## Addresses The `addresses` option is used to configure the addresses collection and supported fields. Defaults to `true` which will create an `addresses` collection with default fields. It also takes an object: | Option | Type | Description | | ----------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `addressFields` | `FieldsOverride` | A function that is given the `defaultFields` as an argument and returns an array of fields. Use this to customise the supported fields for stored addresses. | | `addressesCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `addresses` with a function where you can access the `defaultCollection` as an argument. | | `supportedCountries` | `CountryType[]` | An array of supported countries in [ISO 3166-1 alpha-2 format](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). Defaults to all countries. | You can add your own fields or modify the structure of the existing on in the collection. Example for overriding the default fields: ```ts addresses: { addressesCollectionOverride: ({ defaultCollection }) => ({ ...defaultCollection, fields: [ ...defaultCollection.fields, { name: 'googleMapLocation', label: 'Google Map Location', type: 'text', }, ], }) } ``` ### supportedCountries The `supportedCountries` option is an array of country codes in [ISO 3166-1 alpha-2 format](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2). This is used to limit the countries that can be selected when creating or updating an address. If not provided, all countries will be supported. Currently used for storing addresses only. You can import the default list of countries from the plugin: ```ts import { defaultCountries } from '@payloadcms/plugin-ecommerce/client/react' ``` ## Carts The `carts` option is used to configure the carts collection. Defaults to `true` which will create a `carts` collection with default fields. It also takes an object: | Option | Type | Description | | ------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `cartsCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `carts` with a function where you can access the `defaultCollection` as an argument. | You can add your own fields or modify the structure of the existing on in the collection. Example for overriding the default fields: ```ts carts: { cartsCollectionOverride: ({ defaultCollection }) => ({ ...defaultCollection, fields: [ ...defaultCollection.fields, { name: 'notes', label: 'Notes', type: 'textarea', }, ], }) } ``` Carts are created when a customer adds their first item to the cart. The cart is then updated as they add or remove items. The cart is linked to a _Customer_ via the `customer` field. If the user is authenticated, this will be set to their user ID. If the user is not authenticated, this will be `null`. If the user is not authenticated, the cart ID is stored in local storage and used to fetch the cart on subsequent requests. Access control by default works so that if the user is not authenticated then they can only access carts that have no customer linked to them. ## Customers The `customers` option is required and is used to provide the customers collection slug. This collection is used to link orders, carts, and addresses to a customer. | Option | Type | Description | | ------ | -------- | ------------------------------------- | | `slug` | `string` | The slug of the customers collection. | While it's recommended to use just one collection for customers and your editors, you can use any collection you want for your customers. Just make sure that your access control is checking for the correct collections as well. ## Currencies The `currencies` option is used to configure the supported currencies by the store. Defaults to `true` which will support `USD`. It also takes an object: | Option | Type | Description | | --------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- | | `supportedCurrencies` | `Currency[]` | An array of supported currencies by the store. Defaults to `USD`. See [Currencies](#currencies-list) for available currencies. | | `defaultCurrency` | `string` | The default currency code to use for the store. Defaults to the first currency. Must be one of the `supportedCurrencies` codes. | The `Currency` type is as follows: ```ts type Currency = { code: string // The currency code in ISO 4217 format, e.g. 'USD' decimals: number // The number of decimal places for the currency, e.g. 2 for USD label: string // A human-readable label for the currency, e.g. 'US Dollar' symbol: string // The currency symbol, e.g. '$' } ``` For example, to support JYP in addition to USD: ```ts import { ecommercePlugin } from '@payloadcms/plugin-ecommerce' import { USD } from '@payloadcms/plugin-ecommerce' ecommercePlugin({ currencies: { supportedCurrencies: [ USD, { code: 'JPY', decimals: 0, label: 'Japanese Yen', symbol: '¥', }, ], defaultCurrency: 'USD', }, }) ``` Note that adding a new currency could generate a new schema migration as it adds new prices fields in your products. We currently support the following currencies out of the box: - `USD` - `EUR` - `GBP` You can import these from the plugin: ```ts import { EUR } from '@payloadcms/plugin-ecommerce' ``` Note that adding new currencies here does not automatically enable them in your payment gateway. Make sure to enable the currencies in your payment gateway dashboard as well. ## Inventory The `inventory` option is used to enable or disable inventory tracking within Payload. It defaults to `true`. It also takes an object: | Option | Type | Description | | ----------- | -------- | ------------------------------------------------------------------------- | | `fieldName` | `string` | Override the field name used to track inventory. Defaults to `inventory`. | For now it's quite rudimentary tracking with no integrations to 3rd party services. It will simply add an `inventory` field to the `variants` collection and decrement the inventory when an order is placed. ## Payments The `payments` option is used to configure payments and supported payment methods. | Option | Type | Description | | ---------------- | ------- | ------------------------------------------------------------------------------------------------- | | `paymentMethods` | `array` | An array of payment method adapters. Currently, only Stripe is supported. [More](#stripe-adapter) | ### Payment adapters The plugin supports payment adapters to integrate with different payment gateways. Currently, only the [Stripe adapter](#stripe-adapter) is available. Adapters will provide a client side version as well with slightly different arguments. Every adapter supports the following arguments in addition to their own: | Argument | Type | Description | | ---------------- | ---------------------------------- | ----------------------------------------------------------------------- | | `label` | `string` | Human readabale label for this payment adapter. | | `groupOverrides` | `GroupField` with `FieldsOverride` | Use this to override the available fields for the payment adapter type. | Client side base arguments are the following: | Argument | Type | Description | | -------- | -------- | ----------------------------------------------- | | `label` | `string` | Human readabale label for this payment adapter. | See the [Stripe adapter](#stripe-adapter) for an example of client side arguments and the [React section](#react) for usage. #### `groupOverrides` The `groupOverrides` option allows you to customize the fields that are available for a specific payment adapter. It takes a `GroupField` object with a `fields` function that receives the default fields and returns an array of fields. These fields are stored in transactions and can be used to collect additional information for the payment method. Stripe, for example, will track the `paymentIntentID`. Example for overriding the default fields: ```ts payments: { paymentMethods: [ stripeAdapter({ secretKey: process.env.STRIPE_SECRET_KEY, publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET, groupOverrides: { fields: ({ defaultFields }) => { return [ ...defaultFields, { name: 'customField', label: 'Custom Field', type: 'text', }, ] } } }), ], }, ``` ### Stripe Adapter The Stripe adapter is used to integrate with the Stripe payment gateway. It requires a secret key, publishable key, and optionally webhook secret. Note that Payload will not install the Stripe SDK package for you automatically, so you will need to install it yourself: ``` pnpm add stripe ``` | Argument | Type | Description | | ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `secretKey` | `string` | Required for communicating with the Stripe API in the backend. | | `publishableKey` | `string` | Required for communicating with the Stripe API in the client side. | | `webhookSecret` | `string` | The webhook secret used to verify incoming webhook requests from Stripe. | | `webhooks` | `WebhookHandler[]` | An array of webhook handlers to register within Payload's REST API for Stripe to callback. | | `apiVersion` | `string` | The Stripe API version to use. See [docs](https://stripe.com/docs/api/versioning). This will be deprecated soon by Stripe's SDK, configure the API version in your Stripe Dashboard. | | `appInfo` | `object` | The application info to pass to Stripe. See [docs](https://stripe.com/docs/api/app_info). | ```ts import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe' stripeAdapter({ secretKey: process.env.STRIPE_SECRET_KEY!, publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!, webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET!, }) ``` #### Stripe `webhooks` The `webhooks` option allows you to register custom webhook handlers for [Stripe events](https://docs.stripe.com/api/events). This is useful if you want to handle specific events that are not covered by the default handlers provided by the plugin. ```ts stripeAdapter({ webhooks: { 'payment_intent.succeeded': ({ event, req }) => { // Access to Payload's req object and event data }, }, }), ``` #### Stripe client side On the client side, you can use the `publishableKey` to initialize Stripe and handle payments. The client side version of the adapter only requires the `label` and `publishableKey` arguments. Never expose the `secretKey` or `webhookSecret` keys on the client side. ```ts import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe' {children} ``` ## Products The `products` option is used to configure the products and variants collections. Defaults to `true` which will create `products` and `variants` collections with default fields. It also takes an object: | Option | Type | Description | | ---------------------------- | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `productsCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `products` with a function where you can access the `defaultCollection` as an argument. | | `variants` | `boolean` `object` | Configuration for the variants collection. Defaults to true. [More](#variants) | | `validation` | `ProductsValidation` | Customise the validation used for checking products or variants before a transaction is created or a payment can be confirmed. [More](#products-validation) | You can add your own fields or modify the structure of the existing on in the collections. Example for overriding the default fields: ```ts products: { productsCollectionOverride: ({ defaultCollection }) => ({ ...defaultCollection, fields: [ ...defaultCollection.fields, { name: 'notes', label: 'Notes', type: 'textarea', }, ], }) } ``` ### Variants The `variants` option is used to configure the variants collection. It takes an object: | Option | Type | Description | | ---------------------------------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `variantsCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `variants` with a function where you can access the `defaultCollection` as an argument. | | `variantTypesCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `variantTypes` with a function where you can access the `defaultCollection` as an argument. | | `variantOptionsCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `variantOptions` with a function where you can access the `defaultCollection` as an argument. | You can add your own fields or modify the structure of the existing on in the collection. Example for overriding the default fields: ```ts variants: { variantsCollectionOverride: ({ defaultCollection }) => ({ ...defaultCollection, fields: [ ...defaultCollection.fields, { name: 'customField', label: 'Custom Field', type: 'text', }, ], }) } ``` The key differences between these collections: - `variantTypes` are the types of variants that a product can have, e.g. Size, Color. - `variantOptions` are the options for each variant type, e.g. Small, Medium, Large for Size. - `variants` are the actual variants of a product, e.g. a T-Shirt in Size Small and Color Red. ### Products validation We use an addition validation step when creating transactions or confirming payments to ensure that the products and variants being purchased are valid. This is to prevent issues such as purchasing a product that is out of stock or has been deleted. You can customise this validation by providing your own validation function via the `validation` option which receives the following arguments: | Option | Type | Description | | ------------------ | ------------------ | -------------------------------------------------------------------------------------------------------- | | `currenciesConfig` | `CurrenciesConfig` | The full currencies configuration provided in the plugin options. | | `product` | `TypedCollection` | The product being purchased. | | `variant` | `TypedCollection` | The variant being purchased, if a variant was selected for the product otherwise it will be `undefined`. | | `quantity` | `number` | The quantity being purchased. | | `currency` | `string` | The currency code being used for the purchase. | The function should throw an error if the product or variant is not valid. If the function does not throw an error, the product or variant is considered valid. The default validation function checks for the following: - A currency is provided. - The product or variant has a price in the selected currency. - The product or variant has enough inventory for the requested quantity. ```ts export const defaultProductsValidation: ProductsValidation = ({ currenciesConfig, currency, product, quantity = 1, variant, }) => { if (!currency) { throw new Error('Currency must be provided for product validation.') } const priceField = `priceIn${currency.toUpperCase()}` if (variant) { if (!variant[priceField]) { throw new Error( `Variant with ID ${variant.id} does not have a price in ${currency}.`, ) } if ( variant.inventory === 0 || (variant.inventory && variant.inventory < quantity) ) { throw new Error( `Variant with ID ${variant.id} is out of stock or does not have enough inventory.`, ) } } else if (product) { // Validate the product's details only if the variant is not provided as it can have its own inventory and price if (!product[priceField]) { throw new Error(`Product does not have a price in.`, { cause: { code: MissingPrice, codes: [product.id, currency] }, }) } if ( product.inventory === 0 || (product.inventory && product.inventory < quantity) ) { throw new Error( `Product is out of stock or does not have enough inventory.`, { cause: { code: OutOfStock, codes: [product.id] }, }, ) } } } ``` ## Orders The `orders` option is used to configure the orders collection. Defaults to `true` which will create an `orders` collection with default fields. It also takes an object: | Option | Type | Description | | -------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `ordersCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `orders` with a function where you can access the `defaultCollection` as an argument. | You can add your own fields or modify the structure of the existing on in the collection. Example for overriding the default fields: ```ts orders: { ordersCollectionOverride: ({ defaultCollection }) => ({ ...defaultCollection, fields: [ ...defaultCollection.fields, { name: 'notes', label: 'Notes', type: 'textarea', }, ], }) } ``` ## Transactions The `transactions` option is used to configure the transactions collection. Defaults to `true` which will create a `transactions` collection with default fields. It also takes an object: | Option | Type | Description | | -------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `transactionsCollectionOverride` | `CollectionOverride` | Allows you to override the collection for `transactions` with a function where you can access the `defaultCollection` as an argument. | You can add your own fields or modify the structure of the existing on in the collection. Example for overriding the default fields: ```ts transactions: { transactionsCollectionOverride: ({ defaultCollection }) => ({ ...defaultCollection, fields: [ ...defaultCollection.fields, { name: 'notes', label: 'Notes', type: 'textarea', }, ], }) } ``` # Ecommerce Frontend Source: https://payloadcms.com/docs/ecommerce/frontend The package provides a set of React utilities to help you manage your ecommerce frontend. These include context providers, hooks, and components to handle carts, products, and payments. The following hooks and components are available: | Hook / Component | Description | | ------------------- | ------------------------------------------------------------------------------ | | `EcommerceProvider` | A context provider to wrap your application and provide the ecommerce context. | | `useCart` | A hook to manage the cart state and actions. | | `useAddresses` | A hook to fetch and manage products. | | `usePayments` | A hook to manage the checkout process. | | `useCurrency` | A hook to format prices based on the selected currency. | | `useEcommerce` | A hook that encompasses all of the above in one. | ### EcommerceProvider The `EcommerceProvider` component is used to wrap your application and provide the ecommerce context. It takes the following props: | Prop | Type | Description | | ------------------ | ------------------ | ----------------------------------------------------------------------------------------------------------- | | `addressesSlug` | `string` | The slug of the addresses collection. Defaults to `addresses`. | | `api` | `object` | API configuration for the internal fetches of the provider. [More](#api) | | `cartsSlug` | `string` | The slug of the carts collection. Defaults to `carts`. | | `children` | `ReactNode` | The child components that will have access to the ecommerce context. | | `currenciesConfig` | `CurrenciesConfig` | Configuration for supported currencies. See [Currencies](./plugin#currencies). | | `customersSlug` | `string` | The slug of the customers collection. Defaults to `users`. | | `debug` | `boolean` | Enable or disable debug mode. This will send more information to the console. | | `enableVariants` | `boolean` | Enable or disable product variants support. Defaults to `true`. | | `paymentMethods` | `PaymentMethod[]` | An array of payment method adapters for the client side. See [Payment adapters](./plugin#payment-adapters). | | `syncLocalStorage` | `boolean` `object` | Whether to sync the cart ID to local storage. Defaults to `true`. Takes an object for configuration | Example usage: ```tsx import { EcommerceProvider } from '@payloadcms/plugin-ecommerce/client/react' // Import any payment adapters you want to use on the client side import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe' import { USD, EUR } from '@payloadcms/plugin-ecommerce' export const Providers = () => ( {children} ) ``` #### api The `api` prop is used to configure the API settings for the internal fetches of the provider. It takes an object with the following properties: | Property | Type | Description | | ----------------- | -------- | ----------------------------------------------------------------- | | `apiRoute` | `string` | The base route for accessing the Payload API. Defaults to `/api`. | | `serverURL` | `string` | The full URL of your Payload server. | | `cartsFetchQuery` | `object` | Additional query parameters to include when fetching the cart. | #### cartsFetchQuery The `cartsFetchQuery` property allows you to specify additional query parameters to include when fetching the cart. This can be useful for including related data or customizing the response. This accepts: | Property | Type | Description | | ---------- | -------------- | --------------------------------------------------------------- | | `depth` | `string` | Defaults to 0. [See Depth](../queries/depth) | | `select` | `SelectType` | Select parameters. [See Select](../queries/select) | | `populate` | `PopulateType` | Populate parameters. [See Populate](../queries/select#populate) | Example usage: ```tsx {children} ``` #### syncLocalStorage The `syncLocalStorage` prop is used to enable or disable syncing the cart ID to local storage. This allows the cart to persist across page reloads and sessions. It defaults to `true`. You can also provide an object with the following properties for more configuration: | Property | Type | Description | | -------- | -------- | ---------------------------------------------------------------------------- | | `key` | `string` | The key to use for storing the cart ID in local storage. Defaults to `cart`. | ### useCart The `useCart` hook is used to manage the cart state and actions. It provides methods to add, remove, and update items in the cart, as well as to fetch the current cart state. It has the following properties: | Property | Type | Description | | --------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------- | | `addItem` | `(item: CartItemInput, quantity?: number) => void` | Method to add an item to the cart, optionally accepts a quantity to add multiple at once. | | `cart` | `Cart` `null` | The current cart state. Null or undefined if it doesn't exist. | | `clearCart` | `() => void` | Method to clear the cart. | | `decrementItem` | `(item: IDType) => void` | Method to decrement the quantity of an item. Will remove it entirely if it reaches 0. | | `incrementItem` | `(item: IDType) => void` | Method to increment the quantity of an item. | | `removeItem` | `(item: IDType) => void` | Method to remove an item from the cart. | Example usage: ```tsx import { useCart } from '@payloadcms/plugin-ecommerce/client/react' const CartComponent = () => { const { addItem, cart, clearCart, decrementItem, incrementItem, removeItem } = useCart() // Your component logic here } ``` ### useAddresses The `useAddresses` hook is used to fetch and manage addresses. It provides methods to create, update, and delete addresses, as well as to fetch the list of addresses. It has the following properties: | Property | Type | Description | | --------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | | `addresses` | `Address[]` | The list of addresses, if any are available for the current user. | | `createAddress` | `(data: Address) => Promise
    ` | Method to create a new address. | | `updateAddress` | `(addressID: IDType, data: Partial
    ) => Promise
    ` | Method to update an existing address by ID. | Example usage: ```tsx import { useAddresses } from '@payloadcms/plugin-ecommerce/client/react' const AddressesComponent = () => { const { addresses, createAddress, updateAddress } = useAddresses() // Your component logic here } ``` ### usePayments The `usePayments` hook is used to manage the checkout process. It provides methods to initiate payments, confirm orders, and handle payment status. It has the following properties: | Property | Type | Description | | ----------------------- | -------------------------- | ------------------------------------------------------------------- | | `confirmOrder` | `(args) => Promise` | Method to confirm an order by ID. [More](#confirmOrder) | | `initiatePayment` | `(args) => Promise` | Method to initiate a payment for an order. [More](#initiatePayment) | | `paymentMethods` | `PaymentMethod[]` | The list of available payment methods. | | `selectedPaymentMethod` | `PaymentMethod` | The currently selected payment method, if any. | Example usage: ```tsx import { usePayments } from '@payloadcms/plugin-ecommerce/client/react' const CheckoutComponent = () => { const { confirmOrder, initiatePayment, paymentMethods, selectedPaymentMethod, } = usePayments() // Your component logic here } ``` #### confirmOrder Use this method to confirm an order by its ID. It requires the payment method ID and will return the order ID. ```ts try { const data = await confirmOrder('stripe', { additionalData: { paymentIntentID: paymentIntent.id, customerEmail, }, }) // Return type will contain `orderID` // use data to redirect to your order page } catch (error) { // handle error } ``` If the payment gateway requires additional confirmations offsite then you will need another landing page to handle that. For example with Stripe you may need to use a callback URL, just make sure the relevant information is routed back. This will mark the transaction as complete in the backend and create the order for the user. #### initiatePayment Use this method to initiate a payment for an order. It requires the cart ID and the payment method ID. Depending on the payment method, additional data may be required. Depending on the payment method used you may need to provide additional data, for example with Stripe: ```ts try { const data = await initiatePayment('stripe', { additionalData: { customerEmail, billingAddress, shippingAddress, }, }) } catch (error) { // handle error } ``` This function will hit the Payload API endpoint for `/stripe/initiate` and return the payment data required to complete the payment on the client side, which by default will include a `client_secret` to complete the payment with Stripe.js. The next step is to call the `confirmOrder` once payment is confirmed on the client side by Stripe. At this step the cart is verified and a transaction is created in the backend with the address details provided. No order is created yet until you call `confirmOrder`, which should be done after payment is confirmed on the client side or via webhooks if you opt for that approach instead. ### useCurrency The `useCurrency` hook is used to format prices based on the selected currency. It provides methods to format prices and to get the current currency. It has the following properties: | Property | Type | Description | | ------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `currenciesConfig` | `CurrenciesConfig` | The configuration for supported currencies. Directly matching the config provided to the Context Provider. [More](#ecommerceprovider) | | `currency` | `Currency` | The currently selected currency. | | `formatPrice` | `(amount: number) => string` | Method to format a price based on the selected currency. | | `setCurrency` | `(currencyCode: string) => void` | Method to set the current currency by code. It will update all price formats when used in conjunction with the `formatPrice` utility. | `formatPrice` in particular is very helpful as all prices are stored as integers to avoid any potential issues with decimal calculations, therefore on the frontend you can use this utility to format your price accounting for the currency and decimals. Example usage: ```tsx import { useCurrency } from '@payloadcms/plugin-ecommerce/client/react' const PriceComponent = ({ amount }) => { const { currenciesConfig, currency, setCurrency } = useCurrency() return
    {formatPrice(amount)}
    } ``` ### useEcommerce The `useEcommerce` hook encompasses all of the above hooks in one. It provides access to the cart, addresses, and payments hooks. Example usage: ```tsx import { useEcommerce } from '@payloadcms/plugin-ecommerce/client/react' const EcommerceComponent = () => { const { cart, addresses, selectedPaymentMethod } = useEcommerce() // Your component logic here } ``` # Payment Adapters Source: https://payloadcms.com/docs/ecommerce/payments A deeper look into the payment adapter pattern used by the Ecommerce Plugin, and how to create your own. The current list of supported payment adapters are: - [Stripe](#stripe) ## REST API The plugin will create REST API endpoints for each payment adapter you add to your configuration. The endpoints will be available at `/api/payments/{provider_name}/{action}` where `provider_name` is the name of the payment adapter and `action` is one of the following: | Action | Method | Description | | --------------- | ------ | --------------------------------------------------------------------------------------------------- | | `initiate` | POST | Initiate a payment for an order. See [initiatePayment](#initiatePayment) for more details. | | `confirm-order` | POST | Confirm an order after a payment has been made. See [confirmOrder](#confirmOrder) for more details. | ## Stripe Out of the box we integrate with Stripe to handle one-off purchases. To use Stripe, you will need to install the Stripe package: ```bash pnpm add stripe ``` We recommend at least `18.5.0` to ensure compatibility with the plugin. Then, in your `plugins` array of your [Payload Config](../configuration/overview), call the plugin with: ```ts import { ecommercePlugin } from '@payloadcms/plugin-ecommerce' import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe' import { buildConfig } from 'payload' export default buildConfig({ // Payload config... plugins: [ ecommercePlugin({ // rest of config... payments: { paymentMethods: [ stripeAdapter({ secretKey: process.env.STRIPE_SECRET_KEY, publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, // Optional - only required if you want to use webhooks webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET, }), ], }, }), ], }) ``` ### Configuration The Stripe payment adapter takes the following configuration options: | Option | Type | Description | | -------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | secretKey | `string` | Your Stripe Secret Key, found in the [Stripe Dashboard](https://dashboard.stripe.com/apikeys). | | publishableKey | `string` | Your Stripe Publishable Key, found in the [Stripe Dashboard](https://dashboard.stripe.com/apikeys). | | webhookSecret | `string` | (Optional) Your Stripe Webhooks Signing Secret, found in the [Stripe Dashboard](https://dashboard.stripe.com/webhooks). Required if you want to use webhooks. | | appInfo | `object` | (Optional) An object containing `name` and `version` properties to identify your application to Stripe. | | webhooks | `object` | (Optional) An object where the keys are Stripe event types and the values are functions that will be called when that event is received. See [Webhooks](#stripe-webhooks) for more details. | | groupOverrides | `object` | (Optional) An object to override the default fields of the payment group. See [Payment Fields](./advanced#payment-fields) for more details. | ### Stripe Webhooks You can also add your own webhooks to handle [events from Stripe](https://docs.stripe.com/api/events). This is optional and the plugin internally does not use webhooks for any core functionality. It receives the following arguments: | Argument | Type | Description | | -------- | ---------------- | ------------------------------- | | event | `Stripe.Event` | The Stripe event object | | req | `PayloadRequest` | The Payload request object | | stripe | `Stripe` | The initialized Stripe instance | You can add a webhook like so: ```ts import { ecommercePlugin } from '@payloadcms/plugin-ecommerce' import { stripeAdapter } from '@payloadcms/plugin-ecommerce/payments/stripe' import { buildConfig } from 'payload' export default buildConfig({ // Payload config... plugins: [ ecommercePlugin({ // rest of config... payments: { paymentMethods: [ stripeAdapter({ secretKey: process.env.STRIPE_SECRET_KEY, publishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, // Required webhookSecret: process.env.STRIPE_WEBHOOKS_SIGNING_SECRET, webhooks: { 'payment_intent.succeeded': ({ event, req }) => { console.log({ event, data: event.data.object }) req.payload.logger.info('Payment succeeded') }, }, }), ], }, }), ], }) ``` To use webhooks you also need to have them configured in your Stripe Dashboard. You can use the [Stripe CLI](https://stripe.com/docs/stripe-cli) to forward webhooks to your local development environment. ### Frontend usage The most straightforward way to use Stripe on the frontend is with the `EcommerceProvider` component and the `stripeAdapterClient` function. Wrap your application in the provider and pass in the Stripe adapter with your publishable key: ```ts import { EcommerceProvider } from '@payloadcms/plugin-ecommerce/client/react' import { stripeAdapterClient } from '@payloadcms/plugin-ecommerce/payments/stripe' {children} ``` Then you can use the `usePayments` hook to access the `initiatePayment` and `confirmOrder` functions, see the [Frontend docs](./frontend#usePayments) for more details. ## Making your own Payment Adapter You can make your own payment adapter by implementing the `PaymentAdapter` interface. This interface requires you to implement the following methods: | Property | Type | Description | | ----------------- | --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | The name of the payment method. This will be used to identify the payment method in the API and on the frontend. | | `label` | `string` | (Optional) A human-readable label for the payment method. This will be used in the admin panel and on the frontend. | | `initiatePayment` | `(args: InitiatePaymentArgs) => Promise` | The function that is called via the `/api/payments/{provider_name}/initiate` endpoint to initiate a payment for an order. [More](#initiatePayment) | | `confirmOrder` | `(args: ConfirmOrderArgs) => Promise` | The function that is called via the `/api/payments/{provider_name}/confirm-order` endpoint to confirm an order after a payment has been made. [More](#confirmOrder) | | `endpoints` | `Endpoint[]` | (Optional) An array of endpoints to be bootstrapped to Payload's API in order to support the payment method. All API paths are relative to `/api/payments/{provider_name}` | | `group` | `GroupField` | A group field config to be used in transactions to track the necessary data for the payment processor, eg. PaymentIntentID for Stripe. See [Payment Fields](#payment-fields) for more details. | The arguments can be extended but should always include the `PaymentAdapterArgs` type which has the following types: | Property | Type | Description | | ---------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- | | `label` | `string` | (Optional) Allow overriding the default UI label for this adaper. | | `groupOverrides` | `FieldsOverride` | (Optional) Allow overriding the default fields of the payment group. See [Payment Fields](#payment-fields) for more details. | #### initiatePayment The `initiatePayment` function is called when a payment is initiated. At this step the transaction is created with a status "Processing", an abandoned purchaase will leave this transaction in this state. It receives an object with the following properties: | Property | Type | Description | | ------------------ | ---------------- | --------------------------------------------- | | `transactionsSlug` | `Transaction` | The transaction being processed. | | `data` | `object` | The cart associated with the transaction. | | `customersSlug` | `string` | The customer associated with the transaction. | | `req` | `PayloadRequest` | The Payload request object. | The data object will contain the following properties: | Property | Type | Description | | ----------------- | --------- | ----------------------------------------------------------------------------------------------------------------- | | `billingAddress` | `Address` | The billing address associated with the transaction. | | `shippingAddress` | `Address` | (Optional) The shipping address associated with the transaction. If this is missing then use the billing address. | | `cart` | `Cart` | The cart collection item. | | `customerEmail` | `string` | In the case that `req.user` is missing, `customerEmail` should be required in order to process guest checkouts. | | `currency` | `string` | The currency for the cart associated with the transaction. | The return type then only needs to contain the following properties though the type supports any additional data returned as needed for the frontend: | Property | Type | Description | | --------- | -------- | ----------------------------------------------- | | `message` | `string` | A success message to be returned to the client. | At any point in the function you can throw an error to return a 4xx or 5xx response to the client. A heavily simplified example of implementing `initiatePayment` could look like: ```ts import { PaymentAdapter, PaymentAdapterArgs, } from '@payloadcms/plugin-ecommerce' import Stripe from 'stripe' export const initiatePayment: NonNullable['initiatePayment'] = async ({ data, req, transactionsSlug }) => { const payload = req.payload // Check for any required data const currency = data.currency const cart = data.cart if (!currency) { throw new Error('Currency is required.') } const stripe = new Stripe(secretKey) try { let customer = ( await stripe.customers.list({ email: customerEmail, }) ).data[0] // Ensure stripe has a customer for this email if (!customer?.id) { customer = await stripe.customers.create({ email: customerEmail, }) } const shippingAddressAsString = JSON.stringify(shippingAddressFromData) const paymentIntent = await stripe.paymentIntents.create() // Create a transaction for the payment intent in the database const transaction = await payload.create({ collection: transactionsSlug, data: {}, }) // Return the client_secret so that the client can complete the payment const returnData: InitiatePaymentReturnType = { clientSecret: paymentIntent.client_secret || '', message: 'Payment initiated successfully', paymentIntentID: paymentIntent.id, } return returnData } catch (error) { payload.logger.error(error, 'Error initiating payment with Stripe') throw new Error( error instanceof Error ? error.message : 'Unknown error initiating payment', ) } } ``` #### confirmOrder The `confirmOrder` function is called after a payment is completed on the frontend and at this step the order is created in Payload. It receives the following properties: | Property | Type | Description | | ------------------ | ---------------- | ----------------------------------------- | | `ordersSlug` | `string` | The orders collection slug. | | `transactionsSlug` | `string` | The transactions collection slug. | | `cartsSlug` | `string` | The carts collection slug. | | `customersSlug` | `string` | The customers collection slug. | | `data` | `object` | The cart associated with the transaction. | | `req` | `PayloadRequest` | The Payload request object. | The data object will contain any data the frontend chooses to send through and at a minimum the following: | Property | Type | Description | | --------------- | -------- | --------------------------------------------------------------------------------------------------------------- | | `customerEmail` | `string` | In the case that `req.user` is missing, `customerEmail` should be required in order to process guest checkouts. | The return type can also contain any additional data with a minimum of the following: | Property | Type | Description | | --------------- | -------- | ----------------------------------------------- | | `message` | `string` | A success message to be returned to the client. | | `orderID` | `string` | The ID of the created order. | | `transactionID` | `string` | The ID of the associated transaction. | A heavily simplified example of implementing `confirmOrder` could look like: ```ts import { PaymentAdapter, PaymentAdapterArgs, } from '@payloadcms/plugin-ecommerce' import Stripe from 'stripe' export const confirmOrder: NonNullable['confirmOrder'] = async ({ data, ordersSlug = 'orders', req, transactionsSlug = 'transactions', }) => { const payload = req.payload const customerEmail = data.customerEmail const paymentIntentID = data.paymentIntentID as string const stripe = new Stripe(secretKey) try { // Find our existing transaction by the payment intent ID const transactionsResults = await payload.find({ collection: transactionsSlug, where: { 'stripe.paymentIntentID': { equals: paymentIntentID, }, }, }) const transaction = transactionsResults.docs[0] // Verify the payment intent exists and retrieve it const paymentIntent = await stripe.paymentIntents.retrieve(paymentIntentID) // Create the order in the database const order = await payload.create({ collection: ordersSlug, data: {}, }) const timestamp = new Date().toISOString() // Update the cart to mark it as purchased, this will prevent further updates to the cart await payload.update({ id: cartID, collection: 'carts', data: { purchasedAt: timestamp, }, }) // Update the transaction with the order ID and mark as succeeded await payload.update({ id: transaction.id, collection: transactionsSlug, data: { order: order.id, status: 'succeeded', }, }) return { message: 'Payment initiated successfully', orderID: order.id, transactionID: transaction.id, } } catch (error) { payload.logger.error(error, 'Error initiating payment with Stripe') } } ``` #### Payment Fields Payment fields are used primarily on the transactions collection to store information about the payment method used. Each payment adapter must provide a `group` field which will be used to store this information. For example, the Stripe adapter provides the following group field: ```ts const groupField: GroupField = { name: 'stripe', type: 'group', admin: { condition: (data) => { const path = 'paymentMethod' return data?.[path] === 'stripe' }, }, fields: [ { name: 'customerID', type: 'text', label: 'Stripe Customer ID', }, { name: 'paymentIntentID', type: 'text', label: 'Stripe PaymentIntent ID', }, ], } ``` ### Client side Payment Adapter The client side adapter should implement the `PaymentAdapterClient` interface: | Property | Type | Description | | ----------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `string` | The name of the payment method. This will be used to identify the payment method in the API and on the frontend. | | `label` | `string` | (Optional) A human-readable label for the payment method. This can be used as a human readable format. | | `initiatePayment` | `boolean` | Flag to toggle on the EcommerceProvider's ability to call the `/api/payments/{provider_name}/initiate` endpoint. If your payment method does not require this step, set this to `false`. | | `confirmOrder` | `boolean` | Flag to toggle on the EcommerceProvider's ability to call the `/api/payments/{provider_name}/confirm-order` endpoint. If your payment method does not require this step, set this to `false`. | And for the args use the `PaymentAdapterClientArgs` type: | Property | Type | Description | | -------- | -------- | ----------------------------------------------------------------- | | `label` | `string` | (Optional) Allow overriding the default UI label for this adaper. | ## Best Practices Always handle sensitive operations like creating payment intents and confirming payments on the server side. Use webhooks to listen for events from Stripe and update your orders accordingly. Never expose your secret key on the frontend. By default Nextjs will only expose environment variables prefixed with `NEXT_PUBLIC_` to the client. While we validate the products and prices on the server side when creating a payment intent, you should override the validation function to add any additional checks you may need for your specific use case. You are safe to pass the ID of a transaction to the frontend however you shouldn't pass any sensitive information or the transaction object itself. When passing price information to your payment provider it should always come from the server and it should be verified against the products in your database. Never trust price information coming from the client. When using webhooks, ensure that you verify the webhook signatures to confirm that the requests are genuinely from Stripe. This helps prevent unauthorized access and potential security vulnerabilities. # Advanced uses and examples Source: https://payloadcms.com/docs/ecommerce/advanced The plugin also exposes its internal utilities so that you can use only the parts that you need without using the entire plugin. This is useful if you want to build your own ecommerce solution on top of Payload. ## Using only the collections You can import the collections directly from the plugin and add them to your Payload configuration. This way, you can use the collections without using the entire plugin: | Name | Collection | Description | | -------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | | `createAddressesCollection` | `addresses` | Used for customer addresses (like shipping and billing). [More](#createAddressesCollection) | | `createCartsCollection` | `carts` | Carts can be used by customers, guests and once purchased are kept for records and analytics. [More](#createCartsCollection) | | `createOrdersCollection` | `orders` | Orders are used to store customer-side information and are related to at least one transaction. [More](#createOrdersCollection) | | `createTransactionsCollection` | `transactions` | Handles payment information accessable by admins only, related to Orders. [More](#createTransactionsCollection) | | `createProductsCollection` | `products` | All the product information lives here, contains prices, relations to Variant Types and joins to Variants. [More](#createProductsCollection) | | `createVariantsCollection` | `variants` | Product variants, unique purchasable items that are linked to a product and Variant Options. [More](#createVariantsCollection) | | `createVariantTypesCollection` | `variantTypes` | A taxonomy used by Products to relate Variant Options together. An example of a Variant Type is "size". [More](#createVariantTypesCollection) | | `createVariantOptionsCollection` | `variantOptions` | Related to a Variant Type to handle a unique property of it. An example of a Variant Option is "small". [More](#createVariantOptionsCollection) | ### createAddressesCollection Use this to create the `addresses` collection. This collection is used to store customer addresses. It takes the following properties: | Property | Type | Description | | -------------------- | --------------- | --------------------------------------------------------------------- | | `access` | `object` | Access control for the collection. | | `addressFields` | `Field[]` | Custom fields to add to the address. | | `customersSlug` | `string` | (Optional) Slug of the customers collection. Defaults to `customers`. | | `supportedCountries` | `CountryType[]` | (Optional) List of supported countries. Defaults to all countries. | The access object can contain the following properties: | Property | Type | Description | | ------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `adminOrCustomerOwner` | `Access` | Access control to check if the user has `admin` permissions or is the owner of the document via the `customer` field. Used to limit read, update or delete to only the customers that own this address. | | `authenticatedOnly` | `Access` | Access control to check if the user is authenticated. Use on the `create` access to allow any customer to create a new address. | | `customerOnlyFieldAccess` | `FieldAccess` | Field level access control to check if the user has `customer` permissions. | See the [access control section](./plugin#access) for more details on each of these functions. Example usage: ```ts import { createAddressesCollection } from 'payload-plugin-ecommerce' const Addresses = createAddressesCollection({ access: { adminOrCustomerOwner, authenticatedOnly, customerOnlyFieldAccess, }, addressFields: [ { name: 'company', type: 'text', label: 'Company', }, ], }) ``` ### createCartsCollection Use this to create the `carts` collection to store customer carts. It takes the following properties: | Property | Type | Description | | ------------------ | ------------------ | ----------------------------------------------------------------------- | | `access` | `object` | Access control for the collection. | | `customersSlug` | `string` | (Optional) Slug of the customers collection. Defaults to `customers`. | | `productsSlug` | `string` | (Optional) Slug of the products collection. Defaults to `products`. | | `variantsSlug` | `string` | (Optional) Slug of the variants collection. Defaults to `variants`. | | `enableVariants` | `boolean` | (Optional) Whether to enable variants in the cart. Defaults to `true`. | | `currenciesConfig` | `CurrenciesConfig` | (Optional) Currencies configuration to enable a subtotal to be tracked. | The access object can contain the following properties: | Property | Type | Description | | ---------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `adminOrCustomerOwner` | `Access` | Access control to check if the user has `admin` permissions or is the owner of the document via the `customer` field. Used to limit read, update or delete to only the customers that own this cart. | | `publicAccess` | `Access` | Allow anyone to create a new cart, useful for guests. | See the [access control section](./plugin#access) for more details on each of these functions. Example usage: ```ts import { createCartsCollection } from 'payload-plugin-ecommerce' const Carts = createCartsCollection({ access: { adminOrCustomerOwner, publicAccess, }, enableVariants: true, currenciesConfig: { defaultCurrency: 'usd', currencies: [ { code: 'usd', symbol: '$', }, { code: 'eur', symbol: '€', }, ], }, }) ``` ### createOrdersCollection Use this to create the `orders` collection to store customer orders. It takes the following properties: | Property | Type | Description | | ------------------ | ------------------ | --------------------------------------------------------------------------- | | `access` | `object` | Access control for the collection. | | `customersSlug` | `string` | (Optional) Slug of the customers collection. Defaults to `customers`. | | `transactionsSlug` | `string` | (Optional) Slug of the transactions collection. Defaults to `transactions`. | | `productsSlug` | `string` | (Optional) Slug of the products collection. Defaults to `products`. | | `variantsSlug` | `string` | (Optional) Slug of the variants collection. Defaults to `variants`. | | `enableVariants` | `boolean` | (Optional) Whether to enable variants in the order. Defaults to `true`. | | `currenciesConfig` | `CurrenciesConfig` | (Optional) Currencies configuration to enable the amount to be tracked. | | `addressFields` | `Field[]` | (Optional) The fields to be used for the shipping address. | The access object can contain the following properties: | Property | Type | Description | | ---------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `adminOrCustomerOwner` | `Access` | Access control to check if the user has `admin` permissions or is the owner of the document via the `customer` field. Used to limit read to only the customers that own this order. | | `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit create, update and delete access to only admins. | | `adminOnlyFieldAccess` | `FieldAccess` | Field level access control to check if the user has `admin` permissions. Limits the transaction ID field to admins only. | See the [access control section](./plugin#access) for more details on each of these functions. Example usage: ```ts import { createOrdersCollection } from 'payload-plugin-ecommerce' const Orders = createOrdersCollection({ access: { adminOrCustomerOwner, adminOnly, adminOnlyFieldAccess, }, enableVariants: true, currenciesConfig: { defaultCurrency: 'usd', currencies: [ { code: 'usd', symbol: '$', }, { code: 'eur', symbol: '€', }, ], }, addressFields: [ { name: 'deliveryInstructions', type: 'text', label: 'Delivery Instructions', }, ], }) ``` ### createTransactionsCollection Use this to create the `transactions` collection to store payment transactions. It takes the following properties: | Property | Type | Description | | ------------------ | ------------------ | ----------------------------------------------------------------------------- | | `access` | `object` | Access control for the collection. | | `customersSlug` | `string` | (Optional) Slug of the customers collection. Defaults to `customers`. | | `cartsSlug` | `string` | (Optional) Slug of the carts collection. Defaults to `carts`. | | `ordersSlug` | `string` | (Optional) Slug of the orders collection. Defaults to `orders`. | | `productsSlug` | `string` | (Optional) Slug of the products collection. Defaults to `products`. | | `variantsSlug` | `string` | (Optional) Slug of the variants collection. Defaults to `variants`. | | `enableVariants` | `boolean` | (Optional) Whether to enable variants in the transaction. Defaults to `true`. | | `currenciesConfig` | `CurrenciesConfig` | (Optional) Currencies configuration to enable the amount to be tracked. | | `addressFields` | `Field[]` | (Optional) The fields to be used for the billing address. | | `paymentMethods` | `PaymentAdapter[]` | (Optional) The payment methods to be used for the transaction. | The access object can contain the following properties: | Property | Type | Description | | ----------- | -------- | ----------------------------------------------------------------------------------------------------- | | `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit all access to only admins. | See the [access control section](./plugin#access) for more details on each of these functions. Example usage: ```ts import { createTransactionsCollection } from 'payload-plugin-ecommerce' const Transactions = createTransactionsCollection({ access: { adminOnly, }, enableVariants: true, currenciesConfig: { defaultCurrency: 'usd', currencies: [ { code: 'usd', symbol: '$', }, { code: 'eur', symbol: '€', }, ], }, addressFields: [ { name: 'billingInstructions', type: 'text', label: 'Billing Instructions', }, ], paymentMethods: [ // Add your payment adapters here ], }) ``` ### createProductsCollection Use this to create the `products` collection to store products. It takes the following properties: | Property | Type | Description | | ------------------ | --------------------------- | -------------------------------------------------------------------------------- | | `access` | `object` | Access control for the collection. | | `variantsSlug` | `string` | (Optional) Slug of the variants collection. Defaults to `variants`. | | `variantTypesSlug` | `string` | (Optional) Slug of the variant types collection. Defaults to `variantTypes`. | | `enableVariants` | `boolean` | (Optional) Whether to enable variants on products. Defaults to `true`. | | `currenciesConfig` | `CurrenciesConfig` | (Optional) Currencies configuration to enable price fields. | | `inventory` | `boolean` `InventoryConfig` | (Optional) Inventory configuration to enable stock tracking. Defaults to `true`. | The access object can contain the following properties: | Property | Type | Description | | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit create, update or delete to only admins. | | `adminOrPublishedStatus` | `Access` | Access control to check if the user has `admin` permissions or if the product has a `published` status. Used to limit read access to published products for non-admins. | See the [access control section](./plugin#access) for more details on each of these functions. Example usage: ```ts import { createProductsCollection } from 'payload-plugin-ecommerce' const Products = createProductsCollection({ access: { adminOnly, adminOrPublishedStatus, }, enableVariants: true, currenciesConfig: { defaultCurrency: 'usd', currencies: [ { code: 'usd', symbol: '$', }, { code: 'eur', symbol: '€', }, ], }, inventory: { enabled: true, trackByVariant: true, lowStockThreshold: 5, }, }) ``` ### createVariantsCollection Use this to create the `variants` collection to store product variants. It takes the following properties: | Property | Type | Description | | -------------------- | --------------------------- | -------------------------------------------------------------------------------- | | `access` | `object` | Access control for the collection. | | `productsSlug` | `string` | (Optional) Slug of the products collection. Defaults to `products`. | | `variantOptionsSlug` | `string` | (Optional) Slug of the variant options collection. Defaults to `variantOptions`. | | `currenciesConfig` | `CurrenciesConfig` | (Optional) Currencies configuration to enable price fields. | | `inventory` | `boolean` `InventoryConfig` | (Optional) Inventory configuration to enable stock tracking. Defaults to `true`. | The access object can contain the following properties: | Property | Type | Description | | ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit all access to only admins. | | `adminOrPublishedStatus` | `Access` | Access control to check if the user has `admin` permissions or if the related product has a `published` status. Used to limit read access to variants of published products for non-admins. | See the [access control section](./plugin#access) for more details on each of these functions. Example usage: ```ts import { createVariantsCollection } from 'payload-plugin-ecommerce' const Variants = createVariantsCollection({ access: { adminOnly, adminOrPublishedStatus, }, currenciesConfig: { defaultCurrency: 'usd', currencies: [ { code: 'usd', symbol: '$', }, { code: 'eur', symbol: '€', }, ], }, inventory: { enabled: true, lowStockThreshold: 5, }, }) ``` ### createVariantTypesCollection Use this to create the `variantTypes` collection to store variant types. It takes the following properties: | Property | Type | Description | | -------------------- | -------- | -------------------------------------------------------------------------------- | | `access` | `object` | Access control for the collection. | | `variantOptionsSlug` | `string` | (Optional) Slug of the variant options collection. Defaults to `variantOptions`. | The access object can contain the following properties: | Property | Type | Description | | -------------- | -------- | ----------------------------------------------------------------------------------------------------- | | `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit all access to only admins. | | `publicAccess` | `Access` | Allow anyone to read variant types. | See the [access control section](./plugin#access) for more details on each of these functions. Example usage: ```ts import { createVariantTypesCollection } from 'payload-plugin-ecommerce' const VariantTypes = createVariantTypesCollection({ access: { adminOnly, publicAccess, }, }) ``` ### createVariantOptionsCollection Use this to create the `variantOptions` collection to store variant options. It takes the following properties: | Property | Type | Description | | ------------------ | -------- | ---------------------------------------------------------------------------- | | `access` | `object` | Access control for the collection. | | `variantTypesSlug` | `string` | (Optional) Slug of the variant types collection. Defaults to `variantTypes`. | The access object can contain the following properties: | Property | Type | Description | | -------------- | -------- | ----------------------------------------------------------------------------------------------------- | | `adminOnly` | `Access` | Access control to check if the user has `admin` permissions. Used to limit all access to only admins. | | `publicAccess` | `Access` | Allow anyone to read variant options. | See the [access control section](./plugin#access) for more details on each of these functions. Example usage: ```ts import { createVariantOptionsCollection } from 'payload-plugin-ecommerce' const VariantOptions = createVariantOptionsCollection({ access: { adminOnly, publicAccess, }, }) ``` ## Typescript There are several common types that you'll come across when working with this package. These are export from the package as well and are used across individual utilities as well. ### CurrenciesConfig Defines the supported currencies in Payload and the frontend. It has the following properties: | Property | Type | Description | | ----------------- | ---------------- | --------------------------------------------------------------------------------- | | `defaultCurrency` | `string` | The default currency code. Must match one of the codes in the `currencies` array. | | `currencies` | `CurrencyType[]` | An array of supported currencies. Each currency must have a unique code. | ### Currency Defines a currency to be used in the application. It has the following properties: | Property | Type | Description | | ---------- | -------- | ------------------------------------------------ | | `code` | `string` | The ISO 4217 currency code. Example `'usd'`. | | `symbol` | `string` | The symbol of the currency. Example `'$'` | | `label` | `string` | The name of the currency. Example `'USD'` | | `decimals` | `number` | The number of decimal places to use. Example `2` | The decimals is very important to provide as we store all prices as integers to avoid floating point issues. For example, if you're using USD, you would store a price of $10.00 as `1000` (10 \* 10^2), so when formatting the price for display we need to know how many decimal places the currency supports. ### CountryType Used to define a country in address fields and supported countries lists. It has the following properties: | Property | Type | Description | | -------- | -------- | ---------------------------- | | `value` | `string` | The ISO 3166-1 alpha-2 code. | | `label` | `string` | The name of the country. | ### InventoryConfig It's used to customise the inventory tracking settings on products and variants. It has the following properties: | Property | Type | Description | | ----------- | -------- | ------------------------------------------------------------------------------------ | | `fieldName` | `string` | (Optional) The name of the field to use for tracking stock. Defaults to `inventory`. | # Examples Source: https://payloadcms.com/docs/examples/overview Payload provides a vast array of examples to help you get started with your project no matter what you are working on. These examples are designed to be easy to get up and running, and to be easy to understand. They showcase nothing more than the specific features being demonstrated so you can easily decipher precisely what is going on. - [Auth](https://github.com/payloadcms/payload/tree/main/examples/auth) - [Custom Components](https://github.com/payloadcms/payload/tree/main/examples/custom-components) - [Draft Preview](https://github.com/payloadcms/payload/tree/main/examples/draft-preview) - [Email](https://github.com/payloadcms/payload/tree/main/examples/email) - [Form Builder](https://github.com/payloadcms/payload/tree/main/examples/form-builder) - [Live Preview](https://github.com/payloadcms/payload/tree/main/examples/live-preview) - [Multi-tenant](https://github.com/payloadcms/payload/tree/main/examples/multi-tenant) - [Tailwind / Shadcn-ui](https://github.com/payloadcms/payload/tree/main/examples/tailwind-shadcn-ui) - [White-label Admin UI](https://github.com/payloadcms/payload/tree/main/examples/whitelabel) If you'd like to run the examples, you can use `create-payload-app` to create a project from one: ```sh npx create-payload-app --example example_name ``` We are adding new examples every day, so if your particular use case is not demonstrated in any existing example, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions) or open a new [PR](https://github.com/payloadcms/payload/pulls) to add it yourself. # Vercel Content Link Source: https://payloadcms.com/docs/integrations/vercel-content-link [Vercel Content Link](https://vercel.com/docs/workflow-collaboration/edit-mode#content-link) will allow your editors to navigate directly from the content rendered on your front-end to the fields in Payload that control it. This requires no changes to your front-end code and very few changes to your Payload Config. ![Versions](/images/docs/vercel-visual-editing.jpg) Vercel Content Link is an enterprise-only feature and only available for deployments hosted on Vercel. If you are an existing enterprise customer, [contact our sales team](https://payloadcms.com/for-enterprise) for help with your integration. ## How it works To power Vercel Content Link, Payload embeds Content Source Maps into its API responses. Content Source Maps are invisible, encoded JSON values that include a link back to the field in the CMS that generated the content. When rendered on the page, Vercel detects and decodes these values to display the Content Link interface. For full details on how the encoding and decoding algorithm works, check out [`@vercel/stega`](https://www.npmjs.com/package/@vercel/stega). ## Getting Started Setting up Payload with Vercel Content Link is easy. First, install the `@payloadcms/plugin-csm` plugin into your project. This plugin requires an API key to install, [contact our sales team](https://payloadcms.com/for-enterprise) if you don't already have one. ```bash npm i @payloadcms/plugin-csm ``` Then in the `plugins` array of your Payload Config, call the plugin and enable any collections that require Content Source Maps. ```ts import { buildConfig } from 'payload/config' import contentSourceMaps from '@payloadcms/plugin-csm' const config = buildConfig({ collections: [ { slug: 'pages', fields: [ { name: 'slug', type: 'text', }, { name: 'title', type: 'text', }, ], }, ], plugins: [ contentSourceMaps({ collections: ['pages'], }), ], }) export default config ``` ## Enabling Content Source Maps Now in your Next.js app, you need to add the `encodeSourceMaps` query parameter to your API requests. This will tell Payload to include the Content Source Maps in the API response. **Note:** For performance reasons, this should only be done when in draft mode or on preview deployments. #### REST API If you're using the REST API, include the `?encodeSourceMaps=true` search parameter. ```ts if (isDraftMode || process.env.VERCEL_ENV === 'preview') { const res = await fetch( `${process.env.NEXT_PUBLIC_PAYLOAD_CMS_URL}/api/pages?encodeSourceMaps=true&where[slug][equals]=${slug}`, ) } ``` #### Local API If you're using the Local API, include the `encodeSourceMaps` via the `context` property. ```ts if (isDraftMode || process.env.VERCEL_ENV === 'preview') { const res = await payload.find({ collection: 'pages', where: { slug: { equals: slug, }, }, context: { encodeSourceMaps: true, }, }) } ``` And that's it! You are now ready to enter Edit Mode and begin visually editing your content. ## Edit Mode To see Content Link on your site, you first need to visit any preview deployment on Vercel and login using the Vercel Toolbar. When Content Source Maps are detected on the page, a pencil icon will appear in the toolbar. Clicking this icon will enable Edit Mode, highlighting all editable fields on the page in blue. ![Versions](/images/docs/vercel-toolbar.jpg) ## Troubleshooting ### Date Fields The plugin does not encode `date` fields by default, but for some cases like text that uses negative CSS letter-spacing, it may be necessary to split the encoded data out from the rendered text. This way you can safely use the cleaned data as expected. ```ts import { vercelStegaSplit } from '@vercel/stega' const { cleaned, encoded } = vercelStegaSplit(text) ``` ### Blocks and array fields All `blocks` and `array` fields by definition do not have plain text strings to encode. For this reason, they are automatically given an additional `_encodedSourceMap` property, which you can use to enable Content Link on entire _sections_ of your site. You can then specify the editing container by adding the `data-vercel-edit-target` HTML attribute to any top-level element of your block. ```ts
    {_encodedSourceMap} {children}
    ``` # Building without a DB connection Source: https://payloadcms.com/docs/production/building-without-a-db-connection One of the most common problems when building a site for production, especially with Docker - is the DB connection requirement. The important note is that Payload by itself does not have this requirement, But [Next.js' SSG ](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation) does if any of your route segments have SSG enabled (which is default, unless you opted out or used a [Dynamic API](https://nextjs.org/docs/app/deep-dive/caching#dynamic-apis)) and use the Payload Local API. Solutions: ## Using the experimental-build-mode Next.js build flag You can run Next.js build using the `pnpm next build --experimental-build-mode compile` command to only compile the code without static generation, which does not require a DB connection. In that case, your pages will be rendered dynamically, but after that, you can still generate static pages using the `pnpm next build --experimental-build-mode generate` command when you have a DB connection. When running `pnpm next build --experimental-build-mode compile`, environment variables prefixed with `NEXT_PUBLIC` will not be inlined and will be `undefined` on the client. To make these variables available, either run `pnpm next build --experimental-build-mode generate` if a DB connection is available, or use `pnpm next build --experimental-build-mode generate-env` if you do not have a DB connection. [Next.js documentation](https://nextjs.org/docs/pages/api-reference/cli/next#next-build-options) ## Opting-out of SSG You can opt out of SSG by adding this all the route segment files: ```ts export const dynamic = 'force-dynamic' ``` **Note that it will disable static optimization and your site will be slower**. More on [Next.js documentation](https://nextjs.org/docs/app/deep-dive/caching#opting-out-2) # Production Deployment Source: https://payloadcms.com/docs/production/deployment So you've developed a Payload app, it's fully tested, and running great locally. Now it's time to launch. **Awesome! Great work!** Now, what's next? There are many ways to deploy Payload to a production environment. When evaluating how you will deploy Payload, you need to consider these main aspects: 1. [Basics](#basics) 1. [Security](#security) 1. [Your database](#database) 1. [Permanent File Storage](#file-storage) 1. [Docker](#docker) Payload can be deployed _anywhere that Next.js can run_ - including Vercel, Netlify, SST, DigitalOcean, AWS, and more. Because it's open source, you can self-host it. But it's important to remember that most Payload projects will also need a database, file storage, an email provider, and a CDN. Make sure you have all of the requirements that your project needs, no matter what deployment platform you choose. ## Basics Payload runs fully in Next.js, so the [Next.js build process](https://nextjs.org/docs/app/building-your-application/deploying) is used for building Payload. If you've used `create-payload-app` to create your project, executing the `build` npm script will build Payload for production. ## Security Payload features a suite of security features that you can rely on to strengthen your application's security. When deploying to Production, it's a good idea to double-check that you are making proper use of each of them. ### The Secret Key When you initialize Payload, you provide it with a `secret` property. This property should be impossible to guess and extremely difficult for brute-force attacks to crack. Make sure your Production `secret` is a long, complex string. ### Double-check and thoroughly test all Access Control Because _**you**_ are in complete control of who can do what with your data, you should double and triple-check that you wield that power responsibly before deploying to Production. **By default, all Access Control functions require that a user is successfully logged in to Payload to create, read, update, or delete data.** But, if you allow public user registration, for example, you will want to make sure that your access control functions are more strict - permitting **only appropriate users** to perform appropriate actions. ### Running in Production Depending on where you deploy Payload, you may need to provide a start script to your deployment platform in order to start up Payload in production mode. Note that this is different than running `next dev`. Generally, Next.js apps come configured with a `start` script which runs `next start`. ### Secure Cookie Settings You should be using an SSL certificate for production Payload instances, which means you can [enable secure cookies](../authentication/overview) in your Authentication-enabled Collection configs. ### Preventing API Abuse Payload comes with a robust set of built-in anti-abuse measures, such as locking out users after X amount of failed login attempts, GraphQL query complexity limits, max `depth` settings, and more. [Click here to learn more](../production/preventing-abuse). ## Database Payload can be used with any Postgres database or MongoDB-compatible database including AWS DocumentDB or Azure Cosmos DB. Make sure your production environment has access to the database that Payload uses. Out of the box, Payload templates pass the `process.env.DATABASE_URI` environment variable to its database adapters, so make sure you've got that environment variable (and all others that you use) assigned in your deployment platform. ### DocumentDB When using AWS DocumentDB, you will need to configure connection options for authentication in the `connectOptions` passed to the `mongooseAdapter` . You also need to set `connectOptions.useFacet` to `false` to disable use of the unsupported `$facet` aggregation. ### CosmosDB When using Azure Cosmos DB, an index is needed for any field you may want to sort on. To add the sort index for all fields that may be sorted in the admin UI use the [indexSortableFields](../configuration/overview) option. ## File storage If you are using Payload to [manage file uploads](../upload/overview), you need to consider where your uploaded files will be permanently stored. If you do not use Payload for file uploads, then this section does not impact your app whatsoever. ### Persistent vs Ephemeral Filesystems Some cloud app hosts such as [Heroku](https://heroku.com) use `ephemeral` file systems, which means that any files uploaded to your server only last until the server restarts or shuts down. Heroku and similar providers schedule restarts and shutdowns without your control, meaning your uploads will accidentally disappear without any way to get them back. Alternatively, persistent filesystems will never delete your files and can be trusted to reliably host uploads perpetually. **Popular cloud providers with ephemeral filesystems:** - Heroku - DigitalOcean Apps **Popular cloud providers with persistent filesystems:** - DigitalOcean Droplets - Amazon EC2 - GoDaddy - Many other more traditional web hosts **Warning:** If you rely on Payload's **Upload** functionality, make sure you either use a host with a persistent filesystem or have an integration with a third-party file host like Amazon S3. ### Using cloud storage providers If you don't use Payload's `upload` functionality, you can completely disregard this section. But, if you do, and you still want to use an ephemeral filesystem provider, you can use one of Payload's official cloud storage plugins or write your own to save the files your users upload to a more permanent storage solution like Amazon S3 or DigitalOcean Spaces. Payload provides a list of official cloud storage adapters for you to use: - [Azure Blob Storage](https://github.com/payloadcms/payload/tree/main/packages/storage-azure) - [Google Cloud Storage](https://github.com/payloadcms/payload/tree/main/packages/storage-gcs) - [AWS S3](https://github.com/payloadcms/payload/tree/main/packages/storage-s3) - [Uploadthing](https://github.com/payloadcms/payload/tree/main/packages/storage-uploadthing) - [Vercel Blob Storage](https://github.com/payloadcms/payload/tree/main/packages/storage-vercel-blob) Follow the docs to configure any one of these storage providers. For local development, it might be handy to simply store uploads on your own computer, and then when it comes to production, simply enable the plugin for the cloud storage vendor of your choice. ## Docker This is an example of a multi-stage docker build of Payload for production. Ensure you are setting your environment variables on deployment, like `PAYLOAD_SECRET`, `PAYLOAD_CONFIG_PATH`, and `DATABASE_URI` if needed. If you don't want to have a DB connection and your build requires that, learn [here](./building-without-a-db-connection) how to prevent that. In your Next.js config, set the `output` property `standalone`. ```js // next.config.js const nextConfig = { output: 'standalone', } ``` Dockerfile ```dockerfile # Dockerfile # From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile FROM node:18-alpine AS base # Install dependencies only when needed FROM base AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat WORKDIR /app # Install dependencies based on the preferred package manager COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ RUN \ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ else echo "Lockfile not found." && exit 1; \ fi # Rebuild the source code only when needed FROM base AS builder WORKDIR /app COPY --from=deps /app/node_modules ./node_modules COPY . . # Next.js collects completely anonymous telemetry data about general usage. # Learn more here: https://nextjs.org/telemetry # Uncomment the following line in case you want to disable telemetry during the build. # ENV NEXT_TELEMETRY_DISABLED 1 RUN \ if [ -f yarn.lock ]; then yarn run build; \ elif [ -f package-lock.json ]; then npm run build; \ elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \ else echo "Lockfile not found." && exit 1; \ fi # Production image, copy all the files and run next FROM base AS runner WORKDIR /app ENV NODE_ENV production # Uncomment the following line in case you want to disable telemetry during runtime. # ENV NEXT_TELEMETRY_DISABLED 1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs COPY --from=builder /app/public ./public # Set the correct permission for prerender cache RUN mkdir .next RUN chown nextjs:nodejs .next # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static USER nextjs EXPOSE 3000 ENV PORT 3000 # server.js is created by next build from the standalone output # https://nextjs.org/docs/pages/api-reference/next-config-js/output CMD HOSTNAME="0.0.0.0" node server.js ``` ## Docker Compose Here is an example of a docker-compose.yml file that can be used for development ```yml version: '3' services: payload: image: node:18-alpine ports: - '3000:3000' volumes: - .:/home/node/app - node_modules:/home/node/app/node_modules working_dir: /home/node/app/ command: sh -c "corepack enable && corepack prepare pnpm@latest --activate && pnpm install && pnpm dev" depends_on: - mongo # - postgres env_file: - .env # Ensure your DATABASE_URI uses 'mongo' as the hostname ie. mongodb://mongo/my-db-name mongo: image: mongo:latest ports: - '27017:27017' command: - --storageEngine=wiredTiger volumes: - data:/data/db logging: driver: none # Uncomment the following to use postgres # postgres: # restart: always # image: postgres:latest # volumes: # - pgdata:/var/lib/postgresql/data # ports: # - "5432:5432" volumes: data: # pgdata: node_modules: ``` # Preventing Production API Abuse Source: https://payloadcms.com/docs/production/preventing-abuse ## Introduction Payload has built-in security best practices that can be configured to your application-specific needs. ## Limit Failed Login Attempts Set the max number of failed login attempts before a user account is locked out for a period of time. Set the `maxLoginAttempts` on the collections that feature Authentication to a reasonable but low number for your users to get in. Use the `lockTime` to set a number in milliseconds from the time a user fails their last allowed attempt that a user must wait to try again. ## Max Depth Querying a collection and automatically including related documents via `depth` incurs a performance cost. Also, it's possible that your configs may have circular relationships, meaning scenarios where an infinite amount of relationships might populate back and forth until your server times out and crashes. You can prevent any potential of depth-related issues by setting a `maxDepth` property on your Payload Config.. The maximum allowed depth should be as small as possible without interrupting dev experience, and it defaults to `10`. ## Cross-Site Request Forgery (CSRF) CSRF prevention will verify the authenticity of each request to your API to prevent a malicious action from another site from authorized users. See how to configure CSRF [here](../authentication/cookies#csrf-attacks). ## Cross Origin Resource Sharing (CORS) To securely allow headless operation you will need to configure the allowed origins for requests to be able to use the Payload API. You can see how to set CORS as well as other Payload configuration settings [here](../configuration/overview) ## Limiting GraphQL Complexity Because GraphQL gives the power of query writing outside a server's control, someone with bad intentions might write a maliciously complex query and bog down your server. To prevent resource-intensive GraphQL requests, Payload provides a way to specify complexity limits. These limits are based on a complexity score calculated for each request. Any GraphQL request that is calculated to be too expensive is rejected. On the Payload Config, in `graphQL` you can set the `maxComplexity` value as an integer. For reference, the default complexity value for each added field is 1, and all `relationship` and `upload` fields are assigned a value of 10. If you do not need GraphQL it is advised that you disable it altogether with the Payload Config by setting `graphQL.disable: true`. Should you wish to enable GraphQL again, you can remove this property or set it `false`, any time. By turning it off, Payload will bypass creating schemas from your collections and will not register the route. ## Malicious File Uploads Payload does not execute uploaded files on the server, but depending on your setup it may be used to transmit and store potentially dangerous files. If your configuration allows file uploads there is the potential that a bad actor uploads a malicious file that is then served to other users. Consider the following ways to mitigate the risks. First, enable email [verification](../authentication/email#email-verification) when users are allowed to register new accounts and add other bot prevention services. Review that `create` and `update` access on file upload collections are as restrictive as your application needs allow. Consider limiting `read` access of uploaded user's files and how you might limit user uploaded files from being served outside of Payload. You can also add a [3rd party library](https://github.com/Cisco-Talos/clamav) to scan files in a [hook](../hooks/collections) or have antivirus software in place. # Performance Source: https://payloadcms.com/docs/performance/overview Payload is designed with performance in mind, but its customizability means that there are many ways to configure your app that can impact performance. With this in mind, Payload provides several options and best practices to help you optimize your app's specific performance needs. This includes the database, APIs, and Admin Panel. Whether you're building an app or troubleshooting an existing one, follow these guidelines to ensure that it runs as quickly and efficiently as possible. ## Building your application ### Database proximity The proximity of your database to your server can significantly impact performance. Ensure that your database is hosted in the same region as your server to minimize latency and improve response times. ### Indexing your fields If a particular field is queried often, build an [Index](../database/indexes) for that field to produce faster queries. When your query runs, the database will not search the entire document to find that one field, but will instead use the index to quickly locate the data. To learn more, see the [Indexes](../database/indexes) docs. ### Querying your data There are several ways to optimize your [Queries](../queries/overview). Many of these options directly impact overall database overhead, response sizes, and/or computational load and can significantly improve performance. When building queries, combine as many of these options together as possible. This will ensure your queries are as efficient as they can be. To learn more, see the [Query Performance](../queries/overview#performance) docs. ### Optimizing your APIs When querying data through Payload APIs, the request lifecycle includes running hooks, access control, validations, and other operations that can add significant overhead to the request. To optimize your APIs, any custom logic should be as efficient as possible. This includes writing lightweight hooks, preventing memory leaks, offloading long-running tasks, and optimizing custom validations. To learn more, see the [Hooks Performance](../hooks/overview#performance) docs. ### Writing efficient validations If your validation functions are asynchronous or computationally heavy, ensure they only run when necessary. To learn more, see the [Validation Performance](../fields/overview#validation-performance) docs. ### Optimizing custom components When building custom components in the Admin Panel, ensure that they are as efficient as possible. This includes using React best practices such as memoization, lazy loading, and avoiding unnecessary re-renders. To learn more, see the [Custom Components Performance](../admin/custom-components#performance) docs. ## Other Best Practices ### Block references Use [Block References](../fields/blocks#block-references) to share the same block across multiple fields without bloating the config. This will reduce the number of fields to traverse when processing permissions, etc. and can significantly reduce the amount of data sent from the server to the client in the Admin Panel. For example, if you have a block that is used in multiple fields, you can define it once and reference it in each field. To do this, use the `blockReferences` option in your blocks field: ```ts import { buildConfig } from 'payload' const config = buildConfig({ // ... blocks: [ { slug: 'TextBlock', fields: [ { name: 'text', type: 'text', }, ], }, ], collections: [ { slug: 'posts', fields: [ { name: 'content', type: 'blocks', // highlight-start blockReferences: ['TextBlock'], blocks: [], // Required to be empty, for compatibility reasons // highlight-end }, ], }, { slug: 'pages', fields: [ { name: 'content', type: 'blocks', // highlight-start blockReferences: ['TextBlock'], blocks: [], // Required to be empty, for compatibility reasons // highlight-end }, ], }, ], }) ``` ### Using the cached Payload instance Ensure that you do not instantiate Payload unnecessarily. Instead, Payload provides a caching mechanism to reuse the same instance across your app. To do this, use the `getPayload` function to get the cached instance of Payload: ```ts import { getPayload } from 'payload' import config from '@payload-config' const myFunction = async () => { const payload = await getPayload({ config }) // use payload here } ``` ### When to make direct-to-db calls **Warning:** Direct database calls bypass all hooks and validations. Only use this method when you are certain that the operation is safe and does not require any of these features. Making direct database calls can significantly improve performance by bypassing much of the request lifecycle such as hooks, validations, and other overhead associated with Payload APIs. For example, this can be especially useful for the `update` operation, where Payload would otherwise need to make multiple API calls to fetch, update, and fetch again. Making a direct database call can reduce this to a single operation. To do this, use the `payload.db` methods: ```ts await payload.db.updateOne({ collection: 'posts', id: post.id, data: { title: 'New Title', }, }) ``` **Note:** Direct database methods do not start a [transaction](../database/transactions). You have to start that yourself. #### Returning To prevent unnecessary database computation and reduce the size of the response, you can also set `returning: false` in your direct database calls if you don't need the updated document returned to you. ```ts await payload.db.updateOne({ collection: 'posts', id: post.id, data: { title: 'New Title' }, // See note above ^ about Postgres // highlight-start returning: false, // highlight-end }) ``` **Note:** The `returning` option is only available on direct-to-db methods. E.g. those on the `payload.db` object. It is not exposed to the Local API. ### Avoid bundling the entire UI library in your front-end If your front-end imports from `@payloadcms/ui`, ensure that you do not bundle the entire package as this can significantly increase your bundle size. To do this, import using the full path to the specific component you need: ```ts import { Button } from '@payloadcms/ui/elements/Button' ``` Custom components within the Admin Panel, however, do not have this same restriction and can import directly from `@payloadcms/ui`: ```ts import { Button } from '@payloadcms/ui' ``` **Tip:** Use [`@next/bundle-analyzer`](https://nextjs.org/docs/app/guides/package-bundling) to analyze your component tree and identify unnecessary re-renders or large components that could be optimized. ## Optimizing local development Everything mentioned above applies to local development as well, but there are a few additional steps you can take to optimize your local development experience. ### Enable Turbopack **Note:** In the future this will be the default. Use at your own risk. Add `--turbo` to your dev script to significantly speed up your local development server start time. ```json { "scripts": { "dev": "next dev --turbo" } } ``` ### Only bundle server packages in production **Note:** This is enabled by default in `create-payload-app` since v3.28.0. If you created your app after this version, you don't need to do anything. By default, Next.js bundles both server and client code. However, during development, bundling certain server packages isn't necessary. Payload has thousands of modules, slowing down compilation. Setting this option skips bundling Payload server modules during development. Fewer files to compile means faster compilation speeds. To do this, add the `devBundleServerPackages` option to `withPayload` in your `next.config.js` file: ```ts const nextConfig = { // your existing next config } export default withPayload(nextConfig, { devBundleServerPackages: false }) ```