obsidian/wiki/payloadcms/configuration.md
2026-05-15 15:13:56 +01:00

10 KiB

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 binpnpm 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