Skip to content

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.

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:

StepWhat happens
Config fileDefines fields, access control, and hooks
RegisterAdded to payload.config.ts so Payload knows it exists
Generate typesgenerate:types updates payload-types.ts with TypeScript interfaces
Frontend routeA 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.

  1. 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'),
    ],
    };

    slug is the collection’s identifier — it becomes the API endpoint (/api/events) and the database table name. useAsTitle tells the admin panel which field to use as the document label in list views.

  2. 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.

  3. Generate types

    Terminal window
    ddev pnpm generate:types

    This updates src/payload-types.ts with a TypeScript interface for your new collection. Run it after every config change — stale types will cause TypeScript errors across the frontend.

  4. Create the frontend route

    Create src/app/(frontend)/events/[slug]/page.tsx with generateMetadata() and data fetching.

See the SEO Guide for complete instructions on adding SEO fields, sitemap entries, and metadata generation.

src/collections/events.ts
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.

src/collections/events.ts
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.

src/collections/events.ts
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.

src/collections/events.ts
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);
}
},
],
},
};