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.tsand 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
postsunder/admin(the default pattern is/admin/collections/posts) - REST:
GET /api/posts,POST /api/posts,GET /api/posts/:id, etc. - GraphQL: a
Postsquery and acreatePostmutation - DB: a
poststable 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:
| Group | Properties | Role |
|---|---|---|
| Required | secret / db | JWT signing key / DB adapter |
| Data model | collections / globals | Recurring documents and singletons |
| Admin UI | admin / editor | Admin Panel behavior / rich text editor (Lexical) |
| API | routes / endpoints / graphQL / cors / csrf | URL prefix / custom endpoints / GraphQL config / security |
| Extensions | plugins / hooks / onInit | Plugins / config-wide hooks / startup hooks |
| i18n | localization / i18n | Field-level localization / Admin UI language |
| Other | serverURL / email / upload / typescript / logger / telemetry | Public 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
| Property | Role |
|---|---|
slug (required) | The identifier behind the URL and the table name |
fields (required) | Array of field definitions (covered in 03 Data Modeling) |
auth | Turn this Collection into an auth endpoint (users that can log in) |
upload | Enable uploads (images / PDFs / etc. — i.e. turn it into a Media collection) |
versions | Document versioning / Draft mode |
hooks | beforeChange / afterChange / etc. — inject logic into the lifecycle |
access | Access control written as functions (covered in 04 Security & Lifecycle) |
admin | Customize Admin UI behavior — list columns, sort order, grouping, etc. |
labels | Singular / plural labels like "Post" / "Posts" for the Admin UI |
timestamps | Auto-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 question | Collection / Global |
|---|---|
| Blog posts / products / users / images | Collection |
| Site header / footer menus | Global |
| Terms of service / privacy policy text | Global (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 page | Global (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'→ inputtype: 'textarea'→ textareatype: 'richText'→ Lexical editortype: 'relationship'→ a picker (filter and pick from a related Collection)type: 'array'→ a repeatable UI where you add child rowstype: '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
typealready grows the Admin UI — no React code required in the common case - Ordering uses
orderable: trueplus the Fractional Indexing utilities inpayload/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.