Collections
Collections are the core data structures in PayloadCMS — each one represents a content type backed by a database table. This guide walks through creating one from scratch.
How it works
Section titled “How it works”A collection in PayloadCMS is more than a database table. When you define one, Payload creates the table, generates the admin interface, and exposes REST and local API endpoints — all from a single TypeScript config file.
Every collection follows the same four steps:
| Step | What happens |
|---|---|
| Config file | Defines fields, access control, and hooks |
| Register | Added to payload.config.ts so Payload knows it exists |
| Generate types | generate:types updates payload-types.ts with TypeScript interfaces |
| Frontend route | A Next.js route fetches and renders the collection’s documents |
The config is the only place you define the collection. Everything else — database schema, API, admin UI — is derived from it automatically.
Creating a New Collection
Section titled “Creating a New Collection”-
Create the config file
src/collections/events.ts export const events: CollectionConfig = {slug: 'events',admin: {useAsTitle: 'title',},fields: [{ name: 'title', type: 'text', required: true },...slugField('title'),],};slugis the collection’s identifier — it becomes the API endpoint (/api/events) and the database table name.useAsTitletells the admin panel which field to use as the document label in list views. -
Register in payload.config.ts
src/payload.config.ts import { events } from './collections/events';export default buildConfig({collections: [pages, posts, media, users, events],});Payload reads this array on startup. Any collection not listed here will not get a database table, admin UI, or API endpoint.
-
Generate types
Terminal window ddev pnpm generate:typesThis updates
src/payload-types.tswith a TypeScript interface for your new collection. Run it after every config change — stale types will cause TypeScript errors across the frontend. -
Create the frontend route
Create
src/app/(frontend)/events/[slug]/page.tsxwithgenerateMetadata()and data fetching.
Adding SEO
Section titled “Adding SEO”See the SEO Guide for complete instructions on adding SEO fields, sitemap entries, and metadata generation.
Adding Blocks/Layout
Section titled “Adding Blocks/Layout”import { image, text } from '@/blocks';
export const events: CollectionConfig = { fields: [ { name: 'title', type: 'text', required: true }, { name: 'blocks', type: 'blocks', blocks: [image, text], }, ],};A blocks field turns the collection into a page builder. Each entry in the blocks array is a block config — the editor can add them in any order from the admin panel. See the Blocks guide for how to create your own.
Access Control
Section titled “Access Control”import { isAdmin, requirePermission, PERMISSIONS } from '@/roles/permissions';
export const events: CollectionConfig = { access: { create: requirePermission(PERMISSIONS.CREATE_CONTENT), read: isAuthenticated, update: requirePermission(PERMISSIONS.EDIT_ALL_CONTENT), delete: isAdmin, },};Access control functions run on every request. requirePermission checks whether the current user’s role has the given permission constant. isAdmin restricts the operation to admin users only. See Roles & Permissions for the full list of available functions and permissions.
Revalidation
Section titled “Revalidation”import { revalidateCollection } from '@/lib/revalidate';
export const events: CollectionConfig = { hooks: { afterChange: [ ({ doc }) => { if (doc.slug) { revalidateCollection('events', doc.slug); } }, ], },};Next.js caches pages at build time. The afterChange hook fires whenever a document is saved in the admin and tells Next.js to regenerate only the affected page — no full rebuild required.
Full Example
Section titled “Full Example”import { slugField } from '@/fields/slug';import { imageUpload } from '@/fields/upload/image';import { isAuthenticated } from '@/roles/permissions';import { revalidateCollection } from '@/lib/revalidate';
export const events: CollectionConfig = { slug: 'events', admin: { useAsTitle: 'title', }, access: { read: isAuthenticated, }, fields: [ { name: 'title', type: 'text', required: true }, ...slugField('title'), { name: 'excerpt', type: 'textarea' }, imageUpload({ name: 'featuredImage', relationTo: 'media' }), { name: 'date', type: 'date' }, ], hooks: { afterChange: [ ({ doc }) => { if (doc.slug) { revalidateCollection('events', doc.slug); } }, ], },};