---
title: "Payload CMS — Configuration"
aliases: [payload-config, payload-buildconfig]
tags: [payloadcms, tech-patterns, configuration]
sources: [configuration__overview.md, configuration__collections.md, configuration__globals.md, configuration__i18n.md, configuration__localization.md, configuration__environment-vars.md]
created: 2026-05-15
updated: 2026-05-15
---

# PayloadCMS — Configuration

## Key Takeaways

- `payload.config.ts` is the single source of truth — server-only, never sent to client
- Two required fields: `secret` (encryption key) and `db` (database adapter)
- Config location auto-detected from `tsconfig.json`; override with `PAYLOAD_CONFIG_PATH`
- Logger uses Pino; pre-instantiated logger is safest for custom transports in ESM/bundled envs
- `typescript.strictDraftTypes` makes required fields optional for draft queries — will be default in v4
- Custom CLI scripts registered under `bin` → `pnpm payload <key>`; support `--cron` scheduling
- `cors: '*'` for open access; `{ origins, headers }` object for fine-grained control
- Disable telemetry with `telemetry: false`

## Overview

The Payload Config (`payload.config.ts`) is the single source of truth for your entire application — schema, auth, admin UI, localization, APIs, and plugins all flow from it. It is server-only, strongly typed, and never included in the client bundle.

## Key Concepts

- **Collections** — recurring data types (many documents); generate full CRUD APIs automatically
- **Globals** — singletons (one document per Global); same field/hook/access system as Collections
- **Localization** — per-field content translation; field-level `localized: true` flag
- **I18n** — Admin Panel UI language (separate from content localization)
- **Environment vars** — accessed via `process.env`; client-side vars require `NEXT_PUBLIC_` prefix
- **Config location** — auto-detected from `tsconfig.rootDir`/`outDir`; override with `PAYLOAD_CONFIG_PATH`

## API / Config Reference

### Top-level `buildConfig` options

| Option | Type | Notes |
|--------|------|-------|
| `secret` * | `string` | Encryption key; use a long random string |
| `db` * | `DatabaseAdapter` | One of: `mongooseAdapter`, `postgresAdapter`, `sqliteAdapter` |
| `collections` | `CollectionConfig[]` | Recurring data types |
| `globals` | `GlobalConfig[]` | Singleton documents |
| `editor` | `RichTextAdapter` | Rich text editor (e.g. `lexicalEditor()`) |
| `serverURL` | `string` | Full URL incl. protocol, no trailing path |
| `cors` | `string \| string[] \| CORSConfig` | `'*'` or allowlist or `{ origins, headers }` |
| `localization` | `LocalizationConfig` | Enable multi-locale content |
| `i18n` | `I18nConfig` | Admin UI language settings |
| `plugins` | `Plugin[]` | Extend Payload functionality |
| `hooks` | `RootHooks` | Root-level lifecycle hooks |
| `endpoints` | `Endpoint[]` | Custom REST routes |
| `email` | `EmailAdapter` | Email sending adapter |
| `upload` | `UploadConfig` | App-wide upload settings |
| `defaultDepth` | `number` | Default relationship population depth |
| `maxDepth` | `number` | Max allowed depth (default: `10`) |
| `debug` | `boolean` | Expose detailed error info |
| `telemetry` | `boolean` | Set `false` to opt out |
| `typescript` | `TypeScriptConfig` | `autoGenerate`, `outputFile`, `declare`, `strictDraftTypes` (will be default in v4) |
| `logger` | `LoggerConfig \| PinoLogger` | Pino logger — options+destination, pre-instantiated, or omit for defaults |
| `loggingLevels` | `object` | Override per-error log level |
| `indexSortableFields` | `boolean` | Auto-index sortable fields (needed for Azure Cosmos) |
| `compatibility` | `object` | Flags for older DBs (e.g. `allowLocalizedWithinLocalized`) |
| `bin` | `BinScript[]` | Custom CLI scripts runnable via `pnpm payload <key>` |
| `sharp` | `Sharp` | Enable image resizing/cropping |

### `CollectionConfig` key options

```ts
import type { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
  slug: 'posts',          // * required; URL-friendly identifier
  fields: [...],          // * required; schema definition
  access: { ... },        // CRUD access control functions
  hooks: { ... },         // lifecycle hooks
  auth: { ... },          // enable auth on this collection
  versions: true,         // enable drafts/versions
  timestamps: true,       // auto createdAt/updatedAt (default true)
  upload: { ... },        // enable file uploads
  trash: false,           // soft deletes
  orderable: false,       // drag-and-drop ordering
  defaultSort: '-createdAt',
  admin: {
    useAsTitle: 'title',
    defaultColumns: ['title', 'status'],
    pagination: { defaultLimit: 10, limits: [10, 20, 50] },
    group: 'Content',
    hidden: false,
  },
}
```

### `GlobalConfig` key options

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

export const Nav: GlobalConfig = {
  slug: 'nav',            // * required
  fields: [...],          // * required
  access: { ... },
  hooks: { ... },
  versions: true,
  admin: {
    group: 'Site Settings',
    preview: (doc) => `https://myapp.com/?preview=true`,
    livePreview: { url: '...' },
  },
}
```

### Localization config

```ts
localization: {
  locales: [
    { label: 'English', code: 'en' },
    { label: 'Arabic', code: 'ar', rtl: true },
  ],
  defaultLocale: 'en',   // required
  fallback: true,        // fall back to defaultLocale if locale missing
}
```

Locale object fields: `code` *, `label`, `rtl`, `fallbackLocale`

### Field-level localization

```ts
{
  name: 'title',
  type: 'text',
  localized: true,   // store per-locale in DB
}
```

### I18n config

```ts
// Install: pnpm install @payloadcms/translations
import { en } from '@payloadcms/translations/languages/en'
import { de } from '@payloadcms/translations/languages/de'

i18n: {
  fallbackLanguage: 'en',
  supportedLanguages: { en, de },
  translations: {
    en: {
      general: { dashboard: 'Home' },   // override built-in keys
      custom: { myKey: 'My text {{variable}}' },
    },
  },
}
```

### CORS config variants

```ts
cors: '*'                                 // allow all
cors: ['http://localhost:3000']           // allowlist
cors: { origins: ['...'], headers: ['x-custom-header'] }
```

## Code Examples

### Minimal working config

```ts
import { buildConfig } from 'payload'
import { mongooseAdapter } from '@payloadcms/db-mongodb'

export default buildConfig({
  secret: process.env.PAYLOAD_SECRET,
  db: mongooseAdapter({ url: process.env.DATABASE_URL }),
  collections: [
    { slug: 'pages', fields: [{ name: 'title', type: 'text' }] },
  ],
})
```

### Querying with locale (Local API)

```ts
const posts = await payload.find({
  collection: 'posts',
  locale: 'es',
  fallbackLocale: false,
})
```

### Querying with locale (REST API)

```
GET /api/pages?locale=es&fallback-locale=none
GET /api/pages?locale=all   // returns all locales keyed by code
```

### Custom translations in a component

```tsx
'use client'
import { useTranslation } from '@payloadcms/ui'

export const MyComponent = () => {
  const { t } = useTranslation()
  return t('general:myCustomKey')
}
```

### Collection labels with translations

```ts
labels: {
  singular: { en: 'Article', es: 'Artículo' },
  plural: { en: 'Articles', es: 'Artículos' },
}
```

### Environment variables

```ts
// .env
PAYLOAD_SECRET=super-secret-string
DATABASE_URL=mongodb://localhost:27017/mydb
NEXT_PUBLIC_STRIPE_KEY=pk_test_xxx   // exposed to client bundle

// payload.config.ts
serverURL: process.env.SERVER_URL,
secret: process.env.PAYLOAD_SECRET,
```

### Custom config location

```json
{
  "scripts": {
    "payload": "PAYLOAD_CONFIG_PATH=/path/to/custom-config.ts payload"
  }
}
```

### Custom bin script (e.g. seed)

```ts
// seed.ts
export const script = async (config: SanitizedConfig) => {
  await payload.init({ config })
  await payload.create({ collection: 'pages', data: { title: 'Home' } })
  process.exit(0)
}

// payload.config.ts
bin: [{ scriptPath: path.resolve(dirname, 'seed.ts'), key: 'seed' }]
// Usage: pnpm payload seed
```

### Running bin scripts on a schedule

```sh
# Run any script on a cron schedule via the built-in `run` bin
pnpm payload run ./myScript.ts --cron "0 * * * *"
```

### Logger (Pino) configuration

```ts
// Option 1: logger options + optional destination
logger: {
  options: { level: 'debug' },
}

// Option 2: write logs to file
import { pino } from 'pino'
logger: {
  options: { level: 'info' },
  destination: pino.destination('/var/log/payload.log'),
}

// Option 3: pre-instantiated logger (recommended for custom transports)
import pino from 'pino'
import pinoPretty from 'pino-pretty'
const logger = pino({ level: 'debug' }, pinoPretty({ colorize: true }))
// pass as: logger

// Disable all logging
// DISABLE_LOGGING=true node server.js
```

> **Gotcha:** Pino's `transport` property can fail with "unable to determine transport target" when modules are bundled or use ESM. Use a pre-instantiated logger instead.

### Localized draft status (experimental)

```ts
// Step 1: global flag
experimental: { localizeStatus: true }

// Step 2: per-collection
versions: { drafts: { localizeStatus: true } }

// Resulting DB shape:
// status: { en: 'published', es: 'draft' }
```

## Gotchas

- Localization is **field-level**, not document-level; you must add `localized: true` to each field individually
- Changing `localized: true` on an existing field changes the DB structure — existing data for that field is **lost**
- I18n (Admin UI language) and Localization (content translation) are completely separate systems
- Only include languages you actually need in `supportedLanguages` — each adds to the client JS bundle
- The Payload Config is server-only; do not import client-side modules (React components, `.scss`) into it
- `allowLocalizedWithinLocalized` compatibility flag: only needed if migrating a pre-3.0 MongoDB database with nested localized fields
- `NEXT_PUBLIC_` prefix is required for any env var accessed in Admin Panel custom components — leaking sensitive keys is a real risk
- Outside of Next.js, use `dotenv` and call `dotenv.config()` before importing the Payload config
- Without `pnpm payload seed` having the `bin` entry in config, the script won't be accessible via CLI
- `filterAvailableLocales` runs server-side and is cached per-route; call `router.refresh()` if locale availability changes dynamically

## Related

- [[wiki/payloadcms/getting-started|Getting Started]]
- [[wiki/payloadcms/fields|Fields]]
- [[wiki/payloadcms/hooks|Hooks]]
- [[wiki/payloadcms/authentication|Authentication]]
