# Payload User Guide

Payload is a TypeScript-native, code-first headless CMS and application framework that installs directly into a [Next.js](https://nextjs.org) App Router project. You define your content model as a TypeScript config object, and Payload generates a full admin panel, a REST API, a GraphQL API, and a fully typed server-side "Local API" — all backed by the database of your choice (MongoDB, PostgreSQL, or SQLite).

This guide shows you how to install Payload, model your content, query your data, secure it, and ship features like authentication, file uploads, rich text, drafts, localization, and background jobs. Configuration deep-dives, deployment, and internals live in the companion **Admin Guide**.

> [!NOTE]
> Payload is **code-first**. There is no clicking around a UI to define fields — your content model is a versioned TypeScript file (`payload.config.ts`) that lives in your repository. The admin panel is generated *from* that config.

---

## Table of Contents

1. [What Payload Is](#1-what-payload-is)
2. [Requirements](#2-requirements)
3. [Quick Start](#3-quick-start)
4. [Installation](#4-installation)
5. [Your First Collection](#5-your-first-collection)
6. [Fields Reference](#6-fields-reference)
7. [Collections and Globals](#7-collections-and-globals)
8. [The Local API](#8-the-local-api)
9. [The REST API](#9-the-rest-api)
10. [The GraphQL API](#10-the-graphql-api)
11. [Querying Data](#11-querying-data)
12. [Authentication](#12-authentication)
13. [Access Control](#13-access-control)
14. [Hooks](#14-hooks)
15. [File Uploads and Storage](#15-file-uploads-and-storage)
16. [Rich Text (Lexical)](#16-rich-text-lexical)
17. [Drafts, Versions, and Scheduled Publishing](#17-drafts-versions-and-scheduled-publishing)
18. [Localization](#18-localization)
19. [The Jobs Queue](#19-the-jobs-queue)
20. [Plugins](#20-plugins)
21. [Email](#21-email)
22. [The Command-Line Interface](#22-the-command-line-interface)
23. [Generating Types](#23-generating-types)
24. [Common Workflows](#24-common-workflows)
25. [Troubleshooting](#25-troubleshooting)
26. [FAQ](#26-faq)
27. [Reference](#27-reference)

---

## 1. What Payload Is

Payload turns a single TypeScript configuration file into a complete content backend. From your config, Payload builds:

- **An admin panel** — a polished React (Server Components) dashboard for editors, generated from your fields. Mounted at `/admin` by default.
- **A REST API** — automatic CRUD endpoints for every collection and global, served under `/api`.
- **A GraphQL API** — a full schema with queries and mutations, served at `/graphql`, with a playground at `/graphql-playground`.
- **A Local API** — a fully typed, in-process JavaScript/TypeScript API (`payload.find(...)`, `payload.create(...)`) that talks to your database directly with no HTTP overhead. This is the fastest way to read and write data from your own server code.
- **TypeScript types** — Payload regenerates an interface file (`payload-types.ts`) from your config so your whole app is type-safe end to end.

Because Payload installs *into* a Next.js app, your CMS and your front end share one codebase, one deployment, and one runtime. You can call the Local API from a React Server Component and render content with zero network round-trips.

> [!TIP]
> A useful mental model: **your config is the schema, the database is storage, and the admin panel, REST, GraphQL, and Local API are four different doors into the same data.** Define a field once and it appears in all four.

### Core building blocks

| Concept | What it is |
|---------|-----------|
| **Collection** | A group of documents that share a schema (e.g. `posts`, `users`, `media`). Maps to a table/collection in your database. |
| **Global** | A single, one-off document (e.g. `header`, `site-settings`). No list — just one editable record. |
| **Field** | A typed piece of data within a collection or global (text, number, relationship, rich text, etc.). |
| **Block** | A reusable group of fields that editors can add, remove, and reorder — ideal for page builders. |
| **Hook** | A function that runs at a point in a document's lifecycle (before save, after read, etc.). |
| **Access control** | A function that decides whether a user may perform an operation, returning `true`, `false`, or a query constraint. |

---

## 2. Requirements

| Requirement | Minimum | Notes |
|-------------|---------|-------|
| **Node.js** | `24.15.0` | Payload's bundler targets this version; older Node is unsupported. |
| **Next.js** | `>= 15` | Payload installs into a Next.js App Router app. The installer refuses Next.js below 15. |
| **React** | `19` | Bundled with current Next.js. |
| **Database** | MongoDB, PostgreSQL, or SQLite | Pick one adapter. See [§15](#15-file-uploads-and-storage) and the Admin Guide. |
| **Package manager** | pnpm, npm, yarn, or bun | The installer detects and supports all four. |
| **`sharp`** | optional | Required only if you use image resizing on upload collections. |

> [!IMPORTANT]
> Payload is a **Next.js App Router** product. It does not run on the Pages Router, and it is not a standalone Express server. If you don't already have a Next.js app, the installer scaffolds one for you.

---

## 3. Quick Start

The fastest path from nothing to a running CMS:

```bash
npx create-payload-app@latest
```

Answer the prompts (project name, database, template), then:

```bash
cd my-project
npm run dev
```

Open [http://localhost:3000/admin](http://localhost:3000/admin) and create your first admin user.

```text
▲ Next.js 16.x
- Local:    http://localhost:3000
- Admin:    http://localhost:3000/admin
✓ Ready
```

That's it — you now have a working admin panel, REST API, and GraphQL API.

> [!NOTE]
> The first time you visit `/admin`, Payload shows a **"Create your first user"** screen. The first user you create becomes the seed admin account. The home route `/` redirects to `/admin` in the starter templates.

---

## 4. Installation

There are two installation paths: scaffold a brand-new project, or add Payload to an existing Next.js app.

### 4.1 Scaffold a new project

Run the installer with your preferred package manager:

=== "npm"

    ```bash
    npx create-payload-app@latest
    ```

=== "pnpm"

    ```bash
    pnpm create payload-app
    ```

=== "yarn"

    ```bash
    yarn create payload-app
    ```

=== "bun"

    ```bash
    bunx create-payload-app
    ```

The installer walks you through:

1. **Project name** — becomes the folder and package name (slugified).
2. **Template** — see the table below.
3. **Database** — MongoDB, PostgreSQL, SQLite, and more. You can paste a connection string or accept a recommended local default.
4. **AI coding agent** (optional) — scaffolds editor rules for Claude, Codex, or Cursor.

#### Available templates

| Template | Best for |
|----------|----------|
| `blank` | A minimal Payload project — just the CMS, no front end opinions. |
| `website` | A full marketing/website starter with pages, posts, SEO, and a layout builder. |
| `ecommerce` | A storefront starter wired up with the ecommerce plugin. |
| `plugin` | A scaffold for **authoring your own Payload plugin** (not an app). |

#### Installer flags

You can skip the prompts by passing flags:

```bash
npx create-payload-app@latest \
  --name my-app \
  --template website \
  --db postgres \
  --db-connection-string "postgres://user:pass@localhost:5432/mydb" \
  --use-pnpm
```

| Flag | Alias | Purpose |
|------|-------|---------|
| `--name <name>` | `-n` | Project name. |
| `--template <name>` | `-t` | One of `blank`, `website`, `ecommerce`, `plugin`. |
| `--db <type>` | `-d` | Database adapter: `mongodb`, `postgres`, `sqlite`, etc. |
| `--db-connection-string <uri>` | | Pre-fill the database URL. |
| `--db-accept-recommended` | | Use the recommended local DB without prompting. |
| `--secret <value>` | | Set `PAYLOAD_SECRET` instead of auto-generating one. |
| `--example <name>` | `-e` | Scaffold from an official example instead of a template. |
| `--branch <name>` | | Git branch to pull the template/example from. |
| `--use-npm` / `--use-pnpm` / `--use-yarn` / `--use-bun` | | Force a package manager. |
| `--no-deps` | | Skip installing dependencies. |
| `--no-git` | | Skip `git init`. |
| `--agent <type>` | `-a` | AI agent rules: `claude`, `codex`, or `cursor`. |
| `--no-agent` | | Skip AI agent setup. |
| `--beta` | | Use beta releases of Payload. |
| `--debug` | | Verbose logging. |
| `--dry-run` | | Print what would happen without writing files. |
| `--help` | `-h` | Show help. |

> [!TIP]
> Run `npx create-payload-app --help` at any time to see the live flag list for your installed version.

### 4.2 Add Payload to an existing Next.js app

Run the installer **inside** an existing Next.js (>= 15) project directory. Payload detects the Next.js app and offers to install into it:

```bash
cd my-existing-next-app
npx create-payload-app@latest
```

```text
▲ Next.js project detected!
? Install Payload in this project? › yes
```

The installer wraps your `next.config`, adds the `app/(payload)/` route group, sets up a database adapter, and writes the required environment variables.

> [!WARNING]
> If your Next.js app has a **top-level `layout.tsx`**, the installer will warn you and ask you to move your front-end routes into a route group first. Payload needs to own the root layout for the `(payload)` route group. The installer prints the exact move it expects.

### 4.3 What gets created

A new Payload project contains:

```text
my-project/
├── src/
│   ├── payload.config.ts        # Your content model — the heart of Payload
│   ├── collections/             # Collection definitions
│   └── app/
│       └── (payload)/           # Admin panel + API routes (Payload-owned)
│           ├── admin/[[...segments]]/   # Admin panel catch-all
│           └── api/[...slug]/            # REST + GraphQL catch-all
├── next.config.mjs              # Wrapped with withPayload()
├── .env                         # PAYLOAD_SECRET, DATABASE_URL, ...
└── package.json
```

### 4.4 Environment variables

A fresh project needs at minimum:

```bash
# .env
DATABASE_URI=mongodb://127.0.0.1/my-project
PAYLOAD_SECRET=a-long-random-string
```

`PAYLOAD_SECRET` is **required** — Payload hashes it to encrypt data and sign tokens. If it is missing, Payload refuses to start with `Error: missing secret key. A secret key is needed to secure Payload.`

The connection-string variable name varies by adapter (`DATABASE_URI`, `POSTGRES_URL`, `MONGODB_URL`, etc.) and is whatever your `payload.config.ts` reads. The starter templates use `DATABASE_URI`.

> [!CAUTION]
> Never commit `.env` or your real `PAYLOAD_SECRET`. The installer generates a secret automatically and adds `.env` to `.gitignore`. Rotating `PAYLOAD_SECRET` in production invalidates existing sessions and breaks any encrypted field values.

---

## 5. Your First Collection

A collection is a TypeScript object. Here is a complete, minimal `posts` collection:

```typescript
// src/collections/Posts.ts
import type { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    useAsTitle: 'title', // which field labels each row in the admin list
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    {
      name: 'content',
      type: 'textarea',
    },
    {
      name: 'publishedDate',
      type: 'date',
    },
  ],
}
```

Register it in your config:

```typescript
// src/payload.config.ts
import { buildConfig } from 'payload'
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { Posts } from './collections/Posts'

export default buildConfig({
  secret: process.env.PAYLOAD_SECRET!,
  db: mongooseAdapter({ url: process.env.DATABASE_URI! }),
  editor: lexicalEditor(),
  collections: [Posts],
})
```

Restart the dev server. The `posts` collection now appears in the admin sidebar, gets REST endpoints at `/api/posts`, joins the GraphQL schema, and is reachable through the Local API as `payload.find({ collection: 'posts' })`.

> [!NOTE]
> The `slug` is the collection's identity — it names the database table/collection, the REST path, and the Local API selector. Use a stable, URL-safe, lowercase slug; renaming it later is a data migration.

> [!TIP]
> `admin.useAsTitle` controls which field is shown as the document title in list views and relationship pickers. Without it, the admin falls back to the document ID.

---

## 6. Fields Reference

Fields are the atoms of your content model. Every field has at least a `type`; data-storing fields also have a `name`. Payload ships these field types:

### Data fields

| Type | Stores | Typical use |
|------|--------|-------------|
| `text` | string | Titles, names, slugs. |
| `textarea` | string (multiline) | Short descriptions, summaries. |
| `number` | number | Prices, counts, ratings. |
| `email` | string (validated) | Email addresses. |
| `code` | string | Code snippets (monospace editor). |
| `json` | arbitrary JSON | Structured data, raw config. |
| `date` | date | Timestamps, publish dates. |
| `point` | `[lng, lat]` | Geospatial coordinates. |
| `checkbox` | boolean | Toggles, flags. |
| `select` | string / string[] | Single or multi choice from options. |
| `radio` | string | Single choice, shown as radio buttons. |
| `richText` | structured rich text | Body content (see [§16](#16-rich-text-lexical)). |
| `relationship` | reference(s) to other docs | Linking documents together. |
| `upload` | reference to an upload doc | Attaching files/images. |
| `array` | repeating list of subfields | Repeatable rows (e.g. a list of links). |
| `blocks` | ordered list of typed blocks | Page builders, flexible content. |
| `group` | nested object of subfields | Grouping related fields. |
| `join` | virtual reverse relationship | Showing "documents that point here." |

### Presentational fields

| Type | Purpose |
|------|---------|
| `row` | Lay out fields side by side in the admin. |
| `collapsible` | Group fields under a collapsible heading. |
| `tabs` | Organize fields into tabs. |
| `ui` | Inject a custom admin component with no stored data. |

### Common field options

Most data fields accept these:

```typescript
{
  name: 'slug',
  type: 'text',
  required: true,          // validation: must have a value
  unique: true,            // no two documents may share this value
  index: true,             // database index for faster queries
  defaultValue: 'draft',   // value (or function) used when none is provided
  localized: true,         // store a separate value per locale (see §18)
  label: 'URL Slug',       // admin label
  admin: {
    description: 'Auto-generated from the title',
    position: 'sidebar',   // place in the sidebar instead of the main column
    readOnly: true,
  },
  access: {                // field-level access control (see §13)
    read: () => true,
    update: ({ req }) => Boolean(req.user),
  },
  hooks: {                 // field-level hooks (see §14)
    beforeChange: [/* ... */],
  },
}
```

> [!TIP]
> `defaultValue` can be a **function** — useful for dynamic defaults like the current user or date: `defaultValue: ({ user }) => user?.id`.

### Relationship fields

Link documents to one another:

```typescript
// One author per post
{
  name: 'author',
  type: 'relationship',
  relationTo: 'users',
}

// Many categories per post
{
  name: 'categories',
  type: 'relationship',
  relationTo: 'categories',
  hasMany: true,
}

// Polymorphic: relate to several collections
{
  name: 'related',
  type: 'relationship',
  relationTo: ['posts', 'pages'],
  hasMany: true,
}
```

### Blocks — the page-builder field

Blocks let editors compose a document from reusable, typed sections:

```typescript
{
  name: 'layout',
  type: 'blocks',
  blocks: [
    {
      slug: 'hero',
      fields: [
        { name: 'heading', type: 'text' },
        { name: 'image', type: 'upload', relationTo: 'media' },
      ],
    },
    {
      slug: 'richText',
      fields: [{ name: 'content', type: 'richText' }],
    },
  ],
}
```

Editors can add, remove, duplicate, and reorder blocks in the admin. Each saved block records its `blockType`, so your front end can `switch` on it to render the right component.

> [!NOTE]
> Field `name`s must be unique within their level and may not collide with reserved names. If you reuse a name, Payload throws `DuplicateFieldName`; reserved names throw `ReservedFieldName`.

---

## 7. Collections and Globals

### Collection options

Beyond `slug` and `fields`, a collection accepts:

```typescript
const Posts: CollectionConfig = {
  slug: 'posts',
  labels: { singular: 'Post', plural: 'Posts' },
  fields: [/* ... */],
  admin: {
    useAsTitle: 'title',
    defaultColumns: ['title', 'author', 'publishedDate'],
    group: 'Content',          // sidebar grouping
    description: 'Blog posts',
  },
  access: { /* see §13 */ },
  hooks: { /* see §14 */ },
  auth: true,                  // make this an auth-enabled collection (see §12)
  upload: true,                // make this an upload collection (see §15)
  versions: { drafts: true },  // enable drafts + version history (see §17)
  timestamps: true,            // adds createdAt / updatedAt (default true)
}
```

Every document automatically gets an `id`, and (with `timestamps`, on by default) `createdAt` and `updatedAt`.

### Globals

A global is a single editable document — perfect for site-wide settings:

```typescript
import type { GlobalConfig } from 'payload'

export const Header: GlobalConfig = {
  slug: 'header',
  fields: [
    {
      name: 'navItems',
      type: 'array',
      fields: [
        { name: 'label', type: 'text' },
        { name: 'url', type: 'text' },
      ],
    },
  ],
}
```

Register globals under `globals: [Header]`. Read and write them with `payload.findGlobal({ slug: 'header' })` and `payload.updateGlobal(...)`, or via REST at `/api/globals/header`.

> [!TIP]
> Use a **collection** when you have many documents of the same shape (posts, products, users). Use a **global** when there is exactly one (header, footer, homepage settings).

---

## 8. The Local API

The Local API is Payload's fastest interface: it runs in the same process as your app and queries the database directly — no HTTP, no serialization overhead. It is fully typed against your config. Use it in Server Components, route handlers, scripts, hooks, and jobs.

### Getting a Payload instance

```typescript
import { getPayload } from 'payload'
import config from '@payload-config'

const payload = await getPayload({ config })
```

`getPayload` caches the instance, so calling it repeatedly is cheap.

### Reading data

```typescript
// Find many (paginated)
const { docs, totalDocs, page, totalPages } = await payload.find({
  collection: 'posts',
  where: { status: { equals: 'published' } },
  limit: 10,
  page: 1,
  sort: '-publishedDate',
  depth: 1,
})

// Find one by ID
const post = await payload.findByID({
  collection: 'posts',
  id: '6634a1...',
})

// Count
const { totalDocs } = await payload.count({
  collection: 'posts',
  where: { status: { equals: 'published' } },
})

// Distinct values of a field
const { values } = await payload.findDistinct({
  collection: 'posts',
  field: 'category',
})
```

### Writing data

```typescript
// Create
const created = await payload.create({
  collection: 'posts',
  data: { title: 'Hello', content: 'World' },
})

// Update by ID
const updated = await payload.update({
  collection: 'posts',
  id: created.id,
  data: { title: 'Hello (edited)' },
})

// Update many by query
await payload.update({
  collection: 'posts',
  where: { status: { equals: 'draft' } },
  data: { status: 'published' },
})

// Delete
await payload.delete({ collection: 'posts', id: created.id })

// Duplicate
await payload.duplicate({ collection: 'posts', id: created.id })
```

### Globals

```typescript
const header = await payload.findGlobal({ slug: 'header' })

await payload.updateGlobal({
  slug: 'header',
  data: { navItems: [{ label: 'Home', url: '/' }] },
})
```

### Common Local API options

| Option | Applies to | Effect |
|--------|-----------|--------|
| `where` | find, count, update-many, delete-many | Query constraint (see [§11](#11-querying-data)). |
| `depth` | find, create, update | How many relationship levels to populate (default `2`). |
| `limit` / `page` | find | Pagination. |
| `sort` | find | Field to sort by; prefix `-` for descending. |
| `select` | find, create, update | Return only specific fields. |
| `locale` / `fallbackLocale` | most | Which locale to read/write (see [§18](#18-localization)). |
| `draft` | find, findByID | Read draft versions instead of published. |
| `user` / `overrideAccess` | most | Run as a specific user, or bypass access control. |

> [!IMPORTANT]
> By default the Local API runs with `overrideAccess: true` — it **bypasses access control**, because it is server-side and trusted. To enforce access control (e.g. when acting on behalf of a logged-in user), pass `overrideAccess: false` and a `user`.

> [!TIP]
> In a React Server Component, fetch with the Local API and render directly — no API call needed:
> ```tsx
> export default async function Page() {
>   const payload = await getPayload({ config })
>   const { docs } = await payload.find({ collection: 'posts' })
>   return <PostList posts={docs} />
> }
> ```

---

## 9. The REST API

Every collection automatically gets REST endpoints under your `api` route (default `/api`).

### Collection endpoints

| Method | Path | Action |
|--------|------|--------|
| `GET` | `/api/{collection}` | List documents (paginated, filterable). |
| `POST` | `/api/{collection}` | Create a document. |
| `GET` | `/api/{collection}/{id}` | Get one document. |
| `PATCH` | `/api/{collection}/{id}` | Update one document. |
| `DELETE` | `/api/{collection}/{id}` | Delete one document. |
| `GET` | `/api/{collection}/count` | Count documents. |

### Global endpoints

| Method | Path | Action |
|--------|------|--------|
| `GET` | `/api/globals/{slug}` | Read a global. |
| `POST` | `/api/globals/{slug}` | Update a global. |

### Examples

```bash
# List published posts, newest first
curl "http://localhost:3000/api/posts?where[status][equals]=published&sort=-publishedDate&limit=5"
```

```text
{
  "docs": [ { "id": "...", "title": "Hello", "status": "published" } ],
  "totalDocs": 1,
  "limit": 5,
  "page": 1,
  "totalPages": 1,
  "hasNextPage": false
}
```

```bash
# Create a post (requires auth if the collection's create access demands it)
curl -X POST "http://localhost:3000/api/posts" \
  -H "Content-Type: application/json" \
  -d '{"title":"New post"}'
```

> [!NOTE]
> REST query parameters use the `qs` bracket syntax for nested objects: `where[field][operator]=value`, `depth=1`, `locale=en`, `select[title]=true`. The same `where` operators as the Local API apply (see [§11](#11-querying-data)).

### Custom endpoints

Add your own routes via `endpoints` on a collection, global, or the root config:

```typescript
{
  slug: 'posts',
  endpoints: [
    {
      path: '/featured',
      method: 'get',
      handler: async (req) => {
        const result = await req.payload.find({
          collection: 'posts',
          where: { featured: { equals: true } },
        })
        return Response.json(result)
      },
    },
  ],
}
```

This is reachable at `GET /api/posts/featured`. Handlers receive the `PayloadRequest` (with `req.payload`, `req.user`, etc.) and return a standard web `Response`.

---

## 10. The GraphQL API

Payload generates a GraphQL schema from your collections and globals, served at `/graphql` with an interactive playground at `/graphql-playground`.

```graphql
query {
  Posts(where: { status: { equals: published } }, limit: 5) {
    docs {
      id
      title
      author {
        email
      }
    }
    totalDocs
  }
}
```

Mutations are generated too:

```graphql
mutation {
  createPost(data: { title: "Hello from GraphQL" }) {
    id
    title
  }
}
```

> [!WARNING]
> By default the GraphQL **playground and introspection are disabled in production** (`disablePlaygroundInProduction` / `disableIntrospectionInProduction` default to `true`). Queries are also bounded by a complexity limit (`maxComplexity`, default `1000`) to prevent abusive nested queries. Configure these in the Admin Guide's GraphQL section.

---

## 11. Querying Data

All three APIs share the same query language. A query is a `where` object mapping field paths to operators.

```typescript
where: {
  and: [
    { status: { equals: 'published' } },
    { publishedDate: { less_than_equal: new Date().toISOString() } },
    {
      or: [
        { title: { like: 'payload' } },
        { 'author.name': { contains: 'jane' } },
      ],
    },
  ],
}
```

### Operators

| Operator | Meaning |
|----------|---------|
| `equals` / `not_equals` | Exact match / mismatch. |
| `greater_than` / `greater_than_equal` | Numeric/date comparison. |
| `less_than` / `less_than_equal` | Numeric/date comparison. |
| `like` / `contains` | Substring / case-insensitive substring match. |
| `in` / `not_in` | Value is (not) one of a list. |
| `all` | Array field contains all listed values. |
| `exists` | Field is (or isn't) set. |
| `near` | Geospatial proximity (for `point` fields). |
| `within` / `intersects` | Geospatial region queries. |

### Combining and nesting

- `and: [...]` — all conditions must match.
- `or: [...]` — any condition matches.
- Use dot paths (`'author.name'`) to query across relationships.

> [!TIP]
> When building queries on the client to send to REST, serialize the `where` object with the `qs-esm` package so the bracket notation is correct. The Local API and GraphQL take the object directly.

### Selecting and populating

- **`select`** — return only the fields you need (faster, smaller payloads).
- **`depth`** — how deep to resolve relationships. `depth: 0` returns related IDs only; `depth: 2` (the default) resolves two levels of related documents.
- **`populate`** — override `depth` for specific relationship fields.

```typescript
await payload.find({
  collection: 'posts',
  select: { title: true, author: true },
  depth: 1,
})
```

---

## 12. Authentication

Enable authentication by setting `auth` on a collection. That collection's documents become **users** who can log in.

```typescript
export const Users: CollectionConfig = {
  slug: 'users',
  auth: true, // or an options object (below)
  fields: [
    { name: 'name', type: 'text' },
    // email + password fields are added automatically
  ],
}
```

### What `auth` gives you

- **Email + password login** out of the box (the local strategy).
- **JWT tokens** and **HTTP-only cookies** for sessions.
- Auth operations: login, logout, refresh, "me", forgot password, reset password, unlock, verify email, register first user.
- Auth REST endpoints under the collection, e.g. `POST /api/users/login`, `POST /api/users/logout`, `GET /api/users/me`, `POST /api/users/forgot-password`, `POST /api/users/reset-password`.
- Local API auth methods: `payload.login`, `payload.forgotPassword`, `payload.resetPassword`, `payload.unlock`, `payload.verifyEmail`, and `payload.auth` (verify a request's user).

### Logging in via REST

```bash
curl -X POST "http://localhost:3000/api/users/login" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email removed]","password":"secret"}'
```

```text
{
  "user": { "id": "...", "email": "[email removed]" },
  "token": "eyJhbGc...",
  "exp": 1717603200
}
```

### Auth options

```typescript
auth: {
  useAPIKey: true,            // issue per-user API keys for machine access
  maxLoginAttempts: 5,        // lock the account after N failures
  lockTime: 600000,           // lock duration in ms
  verify: true,               // require email verification before login
  tokenExpiration: 7200,      // JWT lifetime in seconds
  loginWithUsername: true,    // allow username (not just email) login
  disableLocalStrategy: true, // turn off email/password (e.g. SSO-only)
  strategies: [/* custom auth strategies */],
}
```

| Feature | Option | Notes |
|---------|--------|-------|
| **API keys** | `useAPIKey` | Adds a per-user API key. Send as `Authorization: {collection} API-Key {key}`. |
| **Account lockout** | `maxLoginAttempts`, `lockTime` | Locks after repeated failures (`LockedAuth` error). |
| **Email verification** | `verify` | Users must verify before logging in (`UnverifiedEmail` error). |
| **Username login** | `loginWithUsername` | Log in with a username instead of/alongside email. |
| **Custom strategies** | `strategies` | Plug in SSO, OAuth, etc. |

The order in which tokens are read is configurable via the root `auth.jwtOrder` (default `['JWT', 'Bearer', 'cookie']`).

> [!CAUTION]
> The first user is created through the **"Create first user"** flow at `/admin` (or `POST /api/{collection}/first-register`). After that, creating users is governed by your access control. Don't leave user-creation open to the public unless you intend public sign-up.

---

## 13. Access Control

Access control decides **who can do what**. You attach access functions to collections, globals, fields, and operations. Each function returns:

- `true` — allow.
- `false` — deny.
- a **`where` query** — allow, but only for documents matching the query (row-level security).

```typescript
export const Posts: CollectionConfig = {
  slug: 'posts',
  access: {
    // Anyone can read published posts; authors see their own drafts too
    read: ({ req: { user } }) => {
      if (user) return true
      return { status: { equals: 'published' } }
    },
    create: ({ req: { user } }) => Boolean(user),
    update: ({ req: { user } }) => {
      if (!user) return false
      return { author: { equals: user.id } } // only your own posts
    },
    delete: ({ req: { user } }) => user?.role === 'admin',
  },
  fields: [/* ... */],
}
```

### Access points

| Level | Operations |
|-------|-----------|
| **Collection** | `create`, `read`, `update`, `delete`, plus `readVersions` and auth ops (`unlock`, `admin`). |
| **Global** | `read`, `update`, `readVersions`. |
| **Field** | `read`, `create`, `update` — control individual field visibility/editability. |

> [!TIP]
> Returning a **query** instead of a boolean is the idiomatic way to do multi-tenant or "owner-only" access. Payload merges that query into every read/update/delete, so users physically cannot touch rows they aren't allowed to see.

> [!NOTE]
> When a user is denied, Payload returns a `Forbidden` (403) error for writes and silently filters results for reads, depending on the operation. Unauthenticated requests to protected resources get `UnauthorizedError` (401).

---

## 14. Hooks

Hooks are functions that run at lifecycle points. Use them to transform data, enforce rules, send notifications, or sync external systems.

### Collection hooks

```typescript
export const Posts: CollectionConfig = {
  slug: 'posts',
  hooks: {
    beforeValidate: [({ data }) => data],
    beforeChange: [({ data, req, operation }) => {
      if (operation === 'create') data.author = req.user?.id
      return data
    }],
    afterChange: [({ doc, operation }) => {
      if (operation === 'create') console.log('New post:', doc.id)
      return doc
    }],
    beforeRead: [({ doc }) => doc],
    afterRead: [({ doc }) => doc],
    beforeDelete: [({ id }) => { /* ... */ }],
    afterDelete: [({ doc }) => { /* ... */ }],
    afterOperation: [({ result }) => result],
    afterError: [({ error }) => { /* log */ }],
  },
  fields: [/* ... */],
}
```

| Hook | Fires |
|------|-------|
| `beforeOperation` | Before any create/update/read/delete begins. |
| `beforeValidate` | Before field validation runs. |
| `beforeChange` | After validation, before the database write. |
| `afterChange` | After a successful create/update. |
| `beforeRead` | Before a document is returned. |
| `afterRead` | After a document is read (transform output). |
| `beforeDelete` / `afterDelete` | Around deletion. |
| `afterOperation` | After the whole operation completes. |
| `afterError` | When an operation throws. |

Auth-enabled collections add `beforeLogin`, `afterLogin`, `afterLogout`, `afterMe`, `afterRefresh`, `afterForgotPassword`.

### Field hooks

Fields accept their own `beforeChange`, `afterChange`, `beforeValidate`, and `afterRead` hooks — handy for computing one field from others (e.g. auto-generating a `slug` from a `title`).

> [!IMPORTANT]
> Hooks run for **all** write paths — admin panel, REST, GraphQL, and Local API. Logic you put in a `beforeChange` hook is enforced everywhere, which is exactly what you usually want. Keep hooks fast; long-running work belongs in the [Jobs Queue](#19-the-jobs-queue).

---

## 15. File Uploads and Storage

### Enabling uploads

Set `upload` on a collection to turn it into a file/media collection:

```typescript
export const Media: CollectionConfig = {
  slug: 'media',
  upload: {
    staticDir: 'media',
    mimeTypes: ['image/*'],
    imageSizes: [
      { name: 'thumbnail', width: 400, height: 300 },
      { name: 'card', width: 768, height: 1024 },
    ],
  },
  fields: [
    { name: 'alt', type: 'text' }, // custom metadata alongside the file
  ],
}
```

Upload documents automatically gain `filename`, `mimeType`, `filesize`, `width`, `height`, and `url` fields. Other collections attach files with an `upload` field (`relationTo: 'media'`).

> [!WARNING]
> **Image resizing requires `sharp`.** If you define `imageSizes` or `formatOptions` without installing and passing `sharp` to your config, Payload logs: *"Image resizing is enabled for one or more collections, but sharp not installed."* Install `sharp` and add `sharp` to your `buildConfig`.

### Storage adapters

By default, files are written to local disk. For production (especially serverless), use a cloud storage adapter:

| Package | Adapter | Backend |
|---------|---------|---------|
| `@payloadcms/storage-s3` | `s3Storage` | Amazon S3 (and S3-compatible). |
| `@payloadcms/storage-gcs` | `gcsStorage` | Google Cloud Storage. |
| `@payloadcms/storage-azure` | `azureStorage` | Azure Blob Storage. |
| `@payloadcms/storage-r2` | `r2Storage` | Cloudflare R2. |
| `@payloadcms/storage-vercel-blob` | `vercelBlobStorage` | Vercel Blob. |
| `@payloadcms/storage-uploadthing` | `uploadthingStorage` | UploadThing. |

```typescript
import { s3Storage } from '@payloadcms/storage-s3'

export default buildConfig({
  // ...
  storage: [
    s3Storage({
      collections: { media: true },
      bucket: process.env.S3_BUCKET!,
      config: { region: process.env.S3_REGION },
    }),
  ],
})
```

> [!CAUTION]
> When deploying to **Vercel**, every upload collection needs a cloud storage adapter — the serverless filesystem is ephemeral. Payload warns at startup: *"Collections with uploads enabled require a storage adapter when deploying to Vercel."*

---

## 16. Rich Text (Lexical)

Payload's rich text editor is built on Meta's [Lexical](https://lexical.dev). Enable it as the default editor or per `richText` field:

```typescript
import { lexicalEditor } from '@payloadcms/richtext-lexical'

export default buildConfig({
  editor: lexicalEditor(),
  // ...
})
```

```typescript
{
  name: 'body',
  type: 'richText',
  // optionally override features for this field:
  editor: lexicalEditor({
    features: ({ defaultFeatures }) => [...defaultFeatures /* , customFeature */],
  }),
}
```

### Features

The Lexical editor is feature-based. Built-in features include headings, bold/italic/underline/strikethrough, ordered/unordered/checklist lists, links, blockquotes, horizontal rules, indentation, uploads, relationships, inline and block-level **Blocks**, and an experimental table feature.

### Rendering rich text on the front end

Rich text is stored as structured JSON. Convert it for display with the bundled converters:

| Converter | Output |
|-----------|--------|
| Lexical → JSX/React | Render in a React front end (recommended for Next.js). |
| Lexical → HTML | Server-side HTML string (sync and async variants). |
| Lexical → Markdown | Plain Markdown. |
| Lexical → Plaintext | Stripped text (e.g. for search indexing or excerpts). |

```tsx
import { RichText } from '@payloadcms/richtext-lexical/react'

export default function Post({ content }) {
  return <RichText data={content} />
}
```

> [!TIP]
> Because rich text is structured data (not an HTML blob), you control exactly how each node renders — including mapping embedded **Blocks** and **relationships** to your own React components.

---

## 17. Drafts, Versions, and Scheduled Publishing

Enable versioning per collection (or global):

```typescript
{
  slug: 'posts',
  versions: {
    maxPerDoc: 100,        // cap stored versions per document
    drafts: {
      autosave: { interval: 800 },   // autosave drafts as the editor types
      schedulePublish: true,         // allow scheduling future publish
    },
  },
}
```

### How drafts work

With `drafts` enabled, documents gain a `_status` field (`draft` or `published`). Saving creates a draft; publishing promotes it. The public API returns the **published** version by default; pass `draft: true` (and authenticate) to preview drafts.

```typescript
// Public read — published only
const live = await payload.find({ collection: 'posts' })

// Preview drafts
const preview = await payload.find({ collection: 'posts', draft: true })
```

### Version history

Every save records a version. List, inspect, and restore them:

```typescript
const versions = await payload.findVersions({ collection: 'posts' })
await payload.restoreVersion({ collection: 'posts', id: versionId })
```

REST equivalents live under `/api/posts/versions`.

### Scheduled publishing

With `schedulePublish: true`, editors can set a future publish (or unpublish) time. Payload registers a background job that flips `_status` at the scheduled moment — so scheduling depends on the [Jobs Queue](#19-the-jobs-queue) running.

> [!NOTE]
> When `localization` is enabled, drafts can carry **per-locale status** (`localizeStatus: true`) so you can publish English while Spanish stays in draft. Enabling this on existing data requires a one-time migration (see the `localizeStatus` migration in the Admin Guide).

---

## 18. Localization

Turn on localization at the root, then mark individual fields `localized: true`:

```typescript
export default buildConfig({
  localization: {
    locales: [
      { code: 'en', label: 'English' },
      { code: 'es', label: 'Español' },
      { code: 'ar', label: 'العربية', rtl: true },
    ],
    defaultLocale: 'en',
    fallback: true,
  },
  // ...
})
```

```typescript
{ name: 'title', type: 'text', localized: true }
```

Only `localized` fields store a value per locale; everything else is shared. Read and write a specific locale with the `locale` option:

```typescript
const es = await payload.findByID({ collection: 'posts', id, locale: 'es' })

await payload.update({
  collection: 'posts',
  id,
  locale: 'es',
  data: { title: 'Hola' },
})
```

Pass `locale: 'all'` to get every locale's value at once. REST and GraphQL accept a `locale` parameter too.

> [!TIP]
> Locales may be plain strings (`['en', 'es']`) or rich objects with labels and `rtl` flags. Use the object form when you want friendly labels in the admin and right-to-left layout for languages like Arabic or Hebrew. With `fallback: true`, missing translations fall back to the default locale.

---

## 19. The Jobs Queue

Payload includes a built-in background job system for work that shouldn't block a request: sending email, generating thumbnails, syncing to third parties, or scheduled publishing.

### Tasks and workflows

- A **task** is a single unit of work with typed input and output.
- A **workflow** chains tasks together with retry and control flow.

```typescript
export default buildConfig({
  jobs: {
    tasks: [
      {
        slug: 'sendWelcomeEmail',
        inputSchema: [{ name: 'userId', type: 'text' }],
        handler: async ({ input, req }) => {
          const user = await req.payload.findByID({ collection: 'users', id: input.userId })
          await req.payload.sendEmail({ to: user.email, subject: 'Welcome!' })
          return { output: {} }
        },
      },
    ],
    deleteJobOnComplete: true, // remove finished jobs (default true)
  },
})
```

### Queuing and running jobs

```typescript
// Queue a job
await payload.jobs.queue({
  task: 'sendWelcomeEmail',
  input: { userId: user.id },
})

// Run queued jobs (e.g. from a cron route or worker)
await payload.jobs.run({ limit: 10 })
```

### Autorun

Set `jobs.autoRun` to run jobs on an internal cron, and `jobs.scheduling` to enable scheduled tasks:

```typescript
jobs: {
  autoRun: [{ cron: '* * * * *', limit: 10, queue: 'default' }],
}
```

> [!NOTE]
> Jobs are stored in the `payload-jobs` collection. On serverless platforms, prefer triggering `payload.jobs.run()` from a scheduled function (e.g. a Vercel cron route) rather than relying on an always-on internal cron. Scheduled publishing ([§17](#17-drafts-versions-and-scheduled-publishing)) is implemented as a job, so it needs the queue to be running.

---

## 20. Plugins

Plugins extend or modify your config. Add them to the `plugins` array; each runs in order and returns a (possibly modified) config.

```typescript
import { seoPlugin } from '@payloadcms/plugin-seo'

export default buildConfig({
  plugins: [
    seoPlugin({ collections: ['posts'], uploadsCollection: 'media' }),
  ],
  // ...
})
```

### Official plugins

| Package | What it adds |
|---------|--------------|
| `@payloadcms/plugin-seo` | Meta title/description/OG fields and previews. |
| `@payloadcms/plugin-search` | A synced search collection for fast full-text lookups. |
| `@payloadcms/plugin-form-builder` | Editor-built forms with submissions. |
| `@payloadcms/plugin-nested-docs` | Parent/child document hierarchies (breadcrumbs). |
| `@payloadcms/plugin-redirects` | Manage URL redirects from the admin. |
| `@payloadcms/plugin-stripe` | Sync collections with Stripe. |
| `@payloadcms/plugin-sentry` | Error reporting via Sentry. |
| `@payloadcms/plugin-multi-tenant` | Multi-tenant data isolation. |
| `@payloadcms/plugin-import-export` | Bulk import/export of documents (CSV). |
| `@payloadcms/plugin-mcp` | Expose Payload to AI agents over the Model Context Protocol. |
| `@payloadcms/plugin-ecommerce` | Products, carts, payments. |
| `@payloadcms/plugin-cloud-storage` | The base layer the storage adapters build on. |

> [!TIP]
> Plugins are just functions of `config → config`. You can write your own with the `definePlugin` helper, which standardizes ordering (`order`) and identity (`slug`) so plugins can discover and cooperate with one another.

---

## 21. Email

Configure an email adapter so Payload can send password resets, verifications, and your own messages. Without one, email is printed to the console.

| Package | Adapter |
|---------|---------|
| `@payloadcms/email-nodemailer` | `nodemailerAdapter` — SMTP and any Nodemailer transport. |
| `@payloadcms/email-resend` | `resendAdapter` — the Resend API. |

```typescript
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'

export default buildConfig({
  email: nodemailerAdapter({
    defaultFromAddress: '[email removed]',
    defaultFromName: 'My App',
    transportOptions: { host: 'smtp.example.com', port: 587, auth: { /* ... */ } },
  }),
  // ...
})
```

Send your own email from anywhere with a Payload instance:

```typescript
await payload.sendEmail({
  to: '[email removed]',
  subject: 'Hello',
  html: '<p>Hi there</p>',
})
```

> [!NOTE]
> If you don't configure an adapter, Payload warns at startup — *"No email adapter provided. Email will be written to console."* — and password-reset/verification links appear in your server logs. That's fine for local development.

---

## 22. The Command-Line Interface

Payload ships a `payload` binary. The starter templates wire it up in `package.json`:

```json
{
  "scripts": {
    "payload": "cross-env NODE_OPTIONS=--no-deprecation payload"
  }
}
```

Then run commands with your package manager:

| Command | Purpose |
|---------|---------|
| `payload generate:types` | Regenerate `payload-types.ts` from your config. |
| `payload generate:importmap` | Regenerate the admin import map for custom components. |
| `payload migrate` | Run pending database migrations. |
| `payload migrate:create <name>` | Create a new migration file from schema changes. |
| `payload migrate:status` | Show which migrations have run. |
| `payload migrate:down` | Roll back the last migration batch. |
| `payload migrate:refresh` | Roll back and re-run all migrations. |
| `payload migrate:reset` | Roll back every migration. |

```bash
npm run payload generate:types
npm run payload migrate:create add_posts_table
npm run payload migrate
```

> [!IMPORTANT]
> Use Payload's `migrate:*` commands for SQL databases (PostgreSQL/SQLite) rather than calling `drizzle-kit` directly — Payload records applied migrations in its own `payload-migrations` collection and keeps them consistent with your config. MongoDB is schemaless and needs migrations only for explicit data transforms.

---

## 23. Generating Types

Payload keeps a generated `payload-types.ts` in sync with your config. In development it regenerates automatically on startup; you can also run it on demand:

```bash
npm run payload generate:types
```

Use the generated types throughout your app for end-to-end type safety:

```typescript
import type { Post, User } from './payload-types'

const post: Post = await payload.findByID({ collection: 'posts', id })
```

> [!TIP]
> Commit `payload-types.ts` so teammates and CI get the same types without running Payload. Regenerate it whenever you change your fields, and re-run it (the file even reminds you: *"and re-run `payload generate:types` to regenerate this file."*).

---

## 24. Common Workflows

### Build a blog

- [ ] Create a `posts` collection with `title`, `slug`, `content` (richText), `author` (relationship to `users`), `publishedDate`, and `featuredImage` (upload).
- [ ] Enable `versions: { drafts: true }` so editors can preview before publishing.
- [ ] Add a `read` access function that returns `{ _status: { equals: 'published' } }` for anonymous users.
- [ ] Render posts in your Next.js front end via the Local API.

### Build a page builder

- [ ] Create a `pages` collection with a `layout` field of type `blocks`.
- [ ] Define blocks like `hero`, `richText`, `gallery`, and `cta`.
- [ ] On the front end, map each saved block's `blockType` to a React component.

### Add a media library with cloud storage

- [ ] Create a `media` collection with `upload: { imageSizes: [...] }`.
- [ ] Install `sharp` for image resizing.
- [ ] Add the `s3Storage` (or other) adapter to `storage`.
- [ ] Reference media from other collections with `upload` fields.

### Protect content behind login

- [ ] Set `auth: true` on a `users` collection.
- [ ] Add per-collection `access` functions keyed off `req.user`.
- [ ] Log in via `POST /api/users/login`; the cookie authenticates subsequent requests.

---

## 25. Troubleshooting

### Error: `the payload config is required to initialize payload.`

**Cause:** `getPayload`/`init` was called without a `config`.
**Solution:** Pass your config: `getPayload({ config })`, importing it from `@payload-config`.
**Prevention:** Always import the config alias provided by the starter; don't call Payload before it's built.

### Error: `missing secret key. A secret key is needed to secure Payload.`

**Cause:** `PAYLOAD_SECRET` is unset, so `secret` is empty in your config.
**Solution:** Set `PAYLOAD_SECRET` in `.env` and read it in `buildConfig({ secret: process.env.PAYLOAD_SECRET })`.
**Prevention:** Ensure `.env` is loaded before Payload starts; never ship without a secret.

### Error: `cannot find Payload config. Please create a configuration file ... "payload.config.js" or "payload.config.ts".`

**Cause:** Payload can't locate your config file.
**Solution:** Place `payload.config.ts` where your tsconfig's `@payload-config` path points, or set `PAYLOAD_CONFIG_PATH` to its absolute/relative path.
**Prevention:** Keep the `@payload-config` alias in `tsconfig.json` (the installer adds it).

### Error: `cannot connect to MongoDB` / `cannot connect to Postgres` / `cannot connect to SQLite`

**Cause:** The database isn't running or the connection string is wrong.
**Solution:** Verify your database is up and `DATABASE_URI` (or `POSTGRES_URL`, etc.) is correct. For local development, start your DB (e.g. via Docker).
**Prevention:** Use the connection strings the installer suggests for local dev; double-check host/port/credentials in production.

### Warning: `No email adapter provided. Email will be written to console.`

**Cause:** No `email` adapter is configured.
**Solution:** Add `@payloadcms/email-nodemailer` or `@payloadcms/email-resend`. For local dev, find reset/verification links in the server console.
**Prevention:** Configure an adapter before going to production so users actually receive email.

### Warning: `Image resizing is enabled ... but sharp not installed.`

**Cause:** A collection defines `imageSizes`/`formatOptions` but `sharp` isn't installed/passed.
**Solution:** `npm install sharp` and pass `sharp` into `buildConfig`.
**Prevention:** Install `sharp` whenever you use image resizing.

### HMR / dev server doesn't update with a Next.js `basePath`

**Cause:** A known interaction between Payload's hot-reload websocket and Next.js `basePath` (community issue [#16317](https://github.com/payloadcms/payload/issues/16317)).
**Solution:** Develop without `basePath`, or set `PAYLOAD_HMR_URL_OVERRIDE` to the correct websocket URL. You can disable HMR entirely with `DISABLE_PAYLOAD_HMR=true`.
**Prevention:** Track the upstream issue; test `basePath` setups carefully.

### `@payloadcms/plugin-multi-tenant` causes a 404 with Next.js + Turbopack

**Cause:** A reported incompatibility with certain Next.js + Turbopack versions (issue [#16286](https://github.com/payloadcms/payload/issues/16286)).
**Solution:** Run the dev server with Webpack instead of Turbopack, or pin to a known-good Next.js version while a fix lands.
**Prevention:** Verify plugin compatibility when upgrading Next.js.

### MongoDB write fails with `TransientTransactionError` / `WriteConflict`

**Cause:** Concurrent Local API writes can hit transient transaction conflicts that aren't auto-retried (issue [#16766](https://github.com/payloadcms/payload/issues/16766)).
**Solution:** Retry the operation in your own code, or reduce write contention.
**Prevention:** Serialize hot writes where possible; watch the upstream issue for a built-in retry.

### `fetch` results seem cached across job runs even with `cache: 'no-store'`

**Cause:** Next.js fetch caching behavior in job contexts (issue [#14229](https://github.com/payloadcms/payload/issues/14229)).
**Solution:** Add an explicit cache-buster or use `revalidate: 0`; verify your Next.js version's caching semantics.
**Prevention:** Be explicit about cache options for any fetch inside jobs.

---

## 26. FAQ

**Q: Is Payload a hosted service or self-hosted?**
Self-hosted by default — you run it as part of your Next.js app on your own infrastructure. (A managed cloud offering exists separately.)

**Q: Do I define fields in a UI?**
No. Your content model is the `payload.config.ts` file. The admin panel is generated from it. This keeps your schema in version control and type-safe.

**Q: Which database should I choose?**
MongoDB (via Mongoose) is schemaless and great for flexible content. PostgreSQL and SQLite (via Drizzle) give you relational integrity and SQL tooling. All are first-class; pick what your team knows.

**Q: Can I use Payload without the admin panel?**
Yes — you can use it as a headless data layer through the Local, REST, or GraphQL APIs. The admin panel is optional UI.

**Q: How do I query my own data on the server?**
Use the Local API: `const payload = await getPayload({ config })` then `payload.find(...)`. It runs in-process with no HTTP overhead and full types.

**Q: What's the difference between a collection and a global?**
A collection holds many documents of one shape; a global is a single document (site settings, header, footer).

**Q: How do drafts work?**
Enable `versions: { drafts: true }`. Documents get a `_status`; public reads return published versions, and `draft: true` previews unpublished ones.

**Q: How do I add authentication?**
Set `auth: true` on a collection. You get email/password login, JWTs, cookies, API keys, and password-reset flows automatically.

**Q: How do I restrict who sees what?**
Use access control functions that return `true`, `false`, or a `where` query for row-level security.

**Q: How do I deploy to Vercel?**
Use a cloud storage adapter for uploads (the filesystem is ephemeral) and trigger the jobs queue from a cron route. See the Admin Guide for full deployment guidance.

**Q: Can I extend the admin UI?**
Yes — custom field components, views, and dashboard widgets are supported via the admin config. See the Admin Guide.

**Q: How do I keep my TypeScript types in sync?**
Run `payload generate:types` (automatic in dev) and import from `payload-types.ts`.

**Q: Where do background tasks run?**
In the built-in Jobs Queue (`jobs.tasks` / `jobs.workflows`), triggered by `payload.jobs.run()` or an autorun cron.

**Q: Can I migrate my database schema?**
Yes — use `payload migrate:create` and `payload migrate` for SQL databases. MongoDB only needs migrations for explicit data transforms.

**Q: Does Payload support localization?**
Yes — configure `localization` with your locales and mark fields `localized: true`. Right-to-left languages are supported.

**Q: How do I render rich text?**
Rich text is structured JSON; use the Lexical converters (JSX/HTML/Markdown/plaintext) — e.g. the `<RichText>` React component.

---

## 27. Reference

### Default routes

| Route | Default | Config key |
|-------|---------|-----------|
| Admin panel | `/admin` | `routes.admin` |
| REST API | `/api` | `routes.api` |
| GraphQL | `/graphql` | `routes.graphQL` |
| GraphQL playground | `/graphql-playground` | `routes.graphQLPlayground` |

### Key config defaults

| Setting | Default |
|---------|---------|
| `defaultDepth` (relationship population) | `2` |
| `maxDepth` | `10` |
| `defaultMaxTextLength` | `40000` |
| `cookiePrefix` | `payload` |
| `graphQL.maxComplexity` | `1000` |
| `telemetry` | `true` (anonymous; can be disabled) |
| `auth.jwtOrder` | `['JWT', 'Bearer', 'cookie']` |

### Common environment variables

| Variable | Purpose |
|----------|---------|
| `PAYLOAD_SECRET` | **Required.** Secret used to secure Payload. |
| `DATABASE_URI` (or `POSTGRES_URL`, `MONGODB_URL`, …) | Database connection string. |
| `PAYLOAD_CONFIG_PATH` | Explicit path to your config file. |
| `PAYLOAD_LOG_LEVEL` | Logger verbosity. |
| `DISABLE_PAYLOAD_HMR` | Disable hot reload in development. |
| `PAYLOAD_HMR_URL_OVERRIDE` | Override the HMR websocket URL (basePath setups). |
| `NODE_ENV` | `development` / `production` toggles. |

### Local API methods (quick reference)

| Method | Action |
|--------|--------|
| `find`, `findByID`, `count`, `findDistinct` | Read collection documents. |
| `create`, `update`, `delete`, `duplicate` | Write collection documents. |
| `findGlobal`, `updateGlobal` | Read/write globals. |
| `findVersions`, `findVersionByID`, `restoreVersion`, `countVersions` | Version operations. |
| `login`, `auth`, `forgotPassword`, `resetPassword`, `unlock`, `verifyEmail` | Auth operations. |
| `jobs.queue`, `jobs.run` | Background jobs. |
| `sendEmail` | Send email through the configured adapter. |

### Field types (quick reference)

`text`, `textarea`, `number`, `email`, `code`, `json`, `date`, `point`, `checkbox`, `select`, `radio`, `richText`, `relationship`, `upload`, `array`, `blocks`, `group`, `join` (data) · `row`, `collapsible`, `tabs`, `ui` (presentational).

---

_Generated June 2026 from payloadcms/payload at 98c7c14. AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it. Unofficial; not affiliated with or reviewed by the project._
