GuidesPayload CMSCollectionsReferenceTypeScript

Payload CMS Collections: The Complete Field Reference

Every field type, access control pattern, and hook in Payload CMS 3 with real-world examples and TypeScript types.

March 31, 2026

payload cms collections field reference

Payload CMS 3.0 collections are the building blocks of your data model. Each collection becomes a database table, a REST endpoint, a GraphQL type, and an admin UI panel — all from a single configuration object. This reference covers the field types and patterns you'll use most.

Collection Structure

import type { CollectionConfig } from 'payload'

export const Posts: CollectionConfig = {
  slug: 'posts',              // URL segment, DB table name, API endpoint
  admin: {
    useAsTitle: 'title',      // which field shows in the admin list
    defaultColumns: ['title', 'status', 'publishedAt'],
  },
  access: {
    read: () => true,         // public read
    create: isAdmin,          // admin-only write
    update: isAdmin,
    delete: isAdmin,
  },
  hooks: {
    beforeChange: [],
    afterChange: [],
  },
  fields: [],
}

Field Types

text

{
  name: 'title',
  type: 'text',
  required: true,
  unique: true,           // adds DB unique constraint
  index: true,            // adds DB index
  minLength: 3,
  maxLength: 100,
  admin: {
    description: 'Display name shown in admin',
    placeholder: 'Enter title...',
  },
}

textarea

{
  name: 'description',
  type: 'textarea',
  required: true,
  maxLength: 160,         // useful for SEO meta descriptions
}

richText

import { lexicalEditor } from '@payloadcms/richtext-lexical'

{
  name: 'content',
  type: 'richText',
  required: true,
  editor: lexicalEditor({
    features: ({ rootFeatures }) => [
      ...rootFeatures,
      HeadingFeature({ enabledHeadingSizes: ['h2', 'h3'] }),
      BlocksFeature({ blocks: [Callout, CodeBlock] }),
    ],
  }),
}

select

{
  name: 'status',
  type: 'select',
  required: true,
  defaultValue: 'draft',
  options: [
    { label: 'Draft', value: 'draft' },
    { label: 'Published', value: 'published' },
  ],
  admin: { position: 'sidebar' },
}

relationship

The most important field type for multi-collection schemas:

{
  name: 'tenant',
  type: 'relationship',
  relationTo: 'tenants',   // slug of another collection
  required: true,
  index: true,             // ALWAYS index relationship fields used in queries
  hasMany: false,
}

// Many-to-many (hasMany: true)
{
  name: 'categories',
  type: 'relationship',
  relationTo: 'categories',
  hasMany: true,
}

// Polymorphic (multiple collections)
{
  name: 'relatedContent',
  type: 'relationship',
  relationTo: ['posts', 'pages'],
  hasMany: true,
}

Always index relationship fields you query by. Without an index, fetching all posts for a tenant scans every row. With an index, it's a fast lookup.

upload

{
  name: 'featuredImage',
  type: 'upload',
  relationTo: 'media',    // must be a collection with `upload: true`
}

The Media collection that backs this:

export const Media: CollectionConfig = {
  slug: 'media',
  upload: {
    staticDir: 'media',
    mimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
    imageSizes: [
      { name: 'thumbnail', width: 400 },
      { name: 'card', width: 800 },
      { name: 'hero', width: 1600 },
    ],
  },
  fields: [
    { name: 'alt', type: 'text', required: true },
  ],
}

array

For repeatable groups of fields:

{
  name: 'tags',
  type: 'array',
  fields: [
    { name: 'tag', type: 'text', required: true },
  ],
  minRows: 0,
  maxRows: 10,
}

// Usage in a post creation script:
tags: [{ tag: 'TypeScript' }, { tag: 'Next.js' }]

group

For non-repeating nested fields (no separate DB table):

{
  name: 'theme',
  type: 'group',
  fields: [
    { name: 'primary', type: 'text' },
    { name: 'secondary', type: 'text' },
    { name: 'accent', type: 'text' },
  ],
}

// In code: tenant.theme.primary

date

{
  name: 'publishedAt',
  type: 'date',
  admin: {
    date: {
      pickerAppearance: 'dayAndTime',
    },
  },
}

code

For storing code strings with syntax highlighting in the admin:

{
  name: 'mdxContent',
  type: 'code',
  admin: {
    language: 'mdx',
    description: 'MDX content for frontend rendering',
  },
}

Access Control

Access functions receive the request and return a boolean (or a query constraint for filtered reads):

const isAdmin = ({ req }: { req: PayloadRequest }) =>
  Boolean(req.user)

// Filtered read — public can only see published posts
const readPublished = ({ req }: { req: PayloadRequest }) => {
  if (req.user) return true  // admins see everything
  return {
    status: { equals: 'published' },
  }
}

export const Posts: CollectionConfig = {
  access: {
    read: readPublished,
    create: isAdmin,
    update: isAdmin,
    delete: isAdmin,
  },
}

Hooks

Run code before or after collection operations:

{
  hooks: {
    beforeChange: [
      ({ data, operation }) => {
        // Auto-set publishedAt when status changes to published
        if (operation === 'update' && data.status === 'published' && !data.publishedAt) {
          return { ...data, publishedAt: new Date().toISOString() }
        }
        return data
      },
    ],
    afterChange: [
      async ({ doc }) => {
        // Trigger revalidation of the Next.js frontend cache
        if (doc.status === 'published') {
          await revalidatePost(doc.slug)
        }
      },
    ],
  },
}

Regenerating Types

After any schema change, regenerate TypeScript types:

cd apps/cms
pnpm generate:types

This updates src/payload-types.ts. Import Post, Tenant, Media etc. from there for fully-typed access to your collection shapes.

Key Takeaways

  • Index every relationship field you query by — this is the single biggest performance lever
  • group is for nested fields (no DB table); array is for repeatable rows (separate DB table)
  • Access functions return true/false for full access or a query constraint for filtered reads
  • beforeChange hooks can modify the document before it's saved; afterChange hooks are for side effects
  • Always run pnpm generate:types after schema changes to keep TypeScript types in sync