| title |
aliases |
tags |
sources |
created |
updated |
| Payload CMS — Configuration |
| payload-config |
| payload-buildconfig |
|
| payloadcms |
| tech-patterns |
| configuration |
|
| configuration__overview.md |
| configuration__collections.md |
| configuration__globals.md |
| configuration__i18n.md |
| configuration__localization.md |
| configuration__environment-vars.md |
|
2026-05-15 |
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
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
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
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
{
name: 'title',
type: 'text',
localized: true, // store per-locale in DB
}
I18n config
// 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
cors: '*' // allow all
cors: ['http://localhost:3000'] // allowlist
cors: { origins: ['...'], headers: ['x-custom-header'] }
Code Examples
Minimal working config
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)
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
'use client'
import { useTranslation } from '@payloadcms/ui'
export const MyComponent = () => {
const { t } = useTranslation()
return t('general:myCustomKey')
}
Collection labels with translations
labels: {
singular: { en: 'Article', es: 'Artículo' },
plural: { en: 'Articles', es: 'Artículos' },
}
Environment variables
// .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
{
"scripts": {
"payload": "PAYLOAD_CONFIG_PATH=/path/to/custom-config.ts payload"
}
}
Custom bin script (e.g. seed)
// 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
# Run any script on a cron schedule via the built-in `run` bin
pnpm payload run ./myScript.ts --cron "0 * * * *"
Logger (Pino) configuration
// 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)
// 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