Skip to main content

Core Concepts — Config-First / Collections / Globals

Goals for this chapter

  • Put into words what the Config-First philosophy of PayloadCMS actually means
  • Get a feel for the role of payload.config.ts and what changes when you change it
  • Develop a working rule for choosing between Collections (recurring documents) and Globals (singletons)
  • Intuit how a "working CMS" grows from a minimal config

What Config-First means

The official docs describe PayloadCMS as a "config-based, code-first CMS and application framework" (Configuration Overview). Instead of clicking through an admin UI to define your model, you declare data models, authentication, access control, and even Admin UI behavior in one TypeScript config file, and everything grows from there.

Concretely, the following are generated automatically from the config:

  • Admin UI — React components for Collections / Globals / Fields are generated from the config
  • REST / GraphQL APIs — endpoints and schemas are auto-generated per Collection
  • TypeScript types — emitted as payload-types.ts, ready to import from your frontend
  • DB schema / migrations — with the Postgres adapter, migrations are generated via Drizzle ORM

The flip side: changing the config changes the DB schema, the Admin UI, and the APIs all at once. The practical upshot is that putting your config in Git turns "CMS schema changes" into reviewable PRs.

A walking tour of payload.config.ts

Where it lives

According to the official docs, payload.config.ts lives at the root of your project by default, and you can change the location via the PAYLOAD_CONFIG_PATH environment variable. The Next.js co-located setup (covered in chapter 06) also keeps it at the root.

Minimal config

The official minimal example, adapted to Postgres:

import { buildConfig } from 'payload'
import { postgresAdapter } from '@payloadcms/db-postgres'

export default buildConfig({
secret: process.env.PAYLOAD_SECRET,
db: postgresAdapter({
pool: {
connectionString: process.env.DATABASE_URL,
},
}),
collections: [
{
slug: 'posts',
fields: [
{ name: 'title', type: 'text' },
],
},
],
})

With this alone you get:

  • Admin UI: CRUD screens for posts under /admin (the default pattern is /admin/collections/posts)
  • REST: GET /api/posts, POST /api/posts, GET /api/posts/:id, etc.
  • GraphQL: a Posts query and a createPost mutation
  • DB: a posts table on Postgres (id, title, createdAt, updatedAt)

…all standing up at the same time.

Main top-level config properties

Grouping the properties listed in the official docs by role:

GroupPropertiesRole
Requiredsecret / dbJWT signing key / DB adapter
Data modelcollections / globalsRecurring documents and singletons
Admin UIadmin / editorAdmin Panel behavior / rich text editor (Lexical)
APIroutes / endpoints / graphQL / cors / csrfURL prefix / custom endpoints / GraphQL config / security
Extensionsplugins / hooks / onInitPlugins / config-wide hooks / startup hooks
i18nlocalization / i18nField-level localization / Admin UI language
OtherserverURL / email / upload / typescript / logger / telemetryPublic URL / email / uploads / type emission / logging / telemetry

The full list is in the Configuration Overview.

Importing the config type

The config types are exported from payload:

import type { Config, SanitizedConfig } from 'payload'

Config is the raw shape you write, and SanitizedConfig is the normalized shape after PayloadCMS processes it internally.

Collections (recurring documents)

What a Collection is

A Collection is the unit that gathers "recurring documents" that share a schema. A typical blog will have posts / users / media / categories, each as its own Collection.

According to the official docs, every Collection automatically gets:

  • Three APIs — Local, REST, and GraphQL
  • List and edit screens in the Admin UI
  • A same-named DB table or collection (Postgres or Mongo)
  • Configurable options like Access Control, Hooks, and Versions

Main properties

PropertyRole
slug (required)The identifier behind the URL and the table name
fields (required)Array of field definitions (covered in 03 Data Modeling)
authTurn this Collection into an auth endpoint (users that can log in)
uploadEnable uploads (images / PDFs / etc. — i.e. turn it into a Media collection)
versionsDocument versioning / Draft mode
hooksbeforeChange / afterChange / etc. — inject logic into the lifecycle
accessAccess control written as functions (covered in 04 Security & Lifecycle)
adminCustomize Admin UI behavior — list columns, sort order, grouping, etc.
labelsSingular / plural labels like "Post" / "Posts" for the Admin UI
timestampsAuto-populate createdAt / updatedAt (on by default)

Minimal Collection

The minimal example from the official docs:

import type { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
},
],
}

That's already enough to grow the admin UI, APIs, and table for posts. In real projects you stack on options like auth: true (for Users), upload: { staticDir: '...' } (for Media), or versions: { drafts: true } (for drafts).

Globals (singletons)

What a Global is

A Global is the unit for "a document that exists exactly once in this CMS". Where Collections are "recurring", Globals are "singletons".

Typical examples from the official docs:

  • Header Navigation — the site header menu
  • Site-wide Banner Alerts — banners shown on every page
  • App-wide Localized Strings — a translation dictionary for UI copy

If something "can't possibly exist more than once, or one is enough," it's a Global. Otherwise, it's a Collection.

Main properties

slug and fields are required; access / hooks / versions / admin / lockDocuments and friends look almost identical to Collection options.

Minimal Global

import type { GlobalConfig } from 'payload'

export const Header: GlobalConfig = {
slug: 'header',
fields: [
{
name: 'nav',
type: 'array',
fields: [
{ name: 'label', type: 'text' },
{ name: 'page', type: 'relationship', relationTo: 'posts' },
],
},
],
}

The Admin UI gets a single page (the default pattern is under /admin/globals/header), and the API exposes a singleton endpoint such as GET /api/globals/header (verified against the official REST API docs).

Collection vs Global — quick decision table

The thing in questionCollection / Global
Blog posts / products / users / imagesCollection
Site header / footer menusGlobal
Terms of service / privacy policy textGlobal (with versioning)
Categories (taxonomy attached to posts)Collection
Site settings (default OGP, etc.)Global
Announcements (a feed of updates)Collection
Hero section on the top pageGlobal (or Collection if you have multiple landing pages)

When unsure, ask "can there be more than one of this?" — it's the simplest filter.

The Admin UI grows from the config

From the config we've seen so far, the Admin UI is generated without writing any explicit component. List screens, edit forms, validation, related-resource pickers (Relationships) — all derived from the Field type.

  • type: 'text' → input
  • type: 'textarea' → textarea
  • type: 'richText' → Lexical editor
  • type: 'relationship' → a picker (filter and pick from a related Collection)
  • type: 'array' → a repeatable UI where you add child rows
  • type: 'blocks' → block-typed content (compose Hero / RichText / etc. blocks in order)

The details are in 03 Data Modeling. Further Admin UI customization (logo, navigation, plugging in your own React components) is covered in 07 Admin / Media / Deploy.

Implementation example — ordering documents in a Collection (Fractional Indexing)

Drag-and-drop reordering in the Admin UI is a classic Collection design need. PayloadCMS ships Fractional Indexing as a first-class Collection feature (Collections doc).

Turn it on with orderable: true:

import type { CollectionConfig } from 'payload'

export const Categories: CollectionConfig = {
slug: 'categories',
orderable: true,
fields: [
{ name: 'name', type: 'text', required: true },
],
}

For programmatic ordering, the utilities live in payload/shared:

import { generateKeyBetween, generateNKeysBetween } from 'payload/shared'

// A new key between existing keys 'a0' and 'a1'
const newKey = generateKeyBetween('a0', 'a1')

// Insert at the start
const startKey = generateKeyBetween(null, 'a0')

// Append at the end
const endKey = generateKeyBetween('a1', null)

// Generate 5 keys at once between 'a0' and 'a1'
const keys = generateNKeysBetween('a0', 'a1', 5)

The trick is generating a key between two keys, not assigning sequential integers — so reordering doesn't have to rewrite every other row. The same machinery powers the Admin UI's drag-and-drop.

Source: Collections (official docs)

Wrap-up

  • PayloadCMS is a Config-First / code-first CMS where the schema, Admin UI, APIs, and types all grow from a single payload.config.ts
  • The skeleton is required (secret / db) + data model (collections / globals); the rest (admin / editor / plugins …) is dressing
  • Collections = recurring (posts / users / media); Globals = singletons (header / settings)
  • Writing a Field type already grows the Admin UI — no React code required in the common case
  • Ordering uses orderable: true plus the Fractional Indexing utilities in payload/shared

Next

Next, in 03 Data Modeling — Fields / Lexical / Relationships / Versions / Localization, we dive into the world of Fields. From primitives like text / number through Blocks, Lexical Rich Text, Localization, and Versions, we sweep over the data modeling layer.

References