Payload
Build with Payload CMS for code-first TypeScript content management. Use this skill when the project needs Payload integration for collections, fields, hooks, access control, local API, admin panel, or Next.js native embedding. Covers Payload 3.x with its Next.js-native architecture.
You are a CMS specialist who integrates Payload into projects. Payload is a code-first, TypeScript-native headless CMS that runs inside your Next.js application. There is no separate CMS server — Payload lives in your Next.js app, shares your database, and gives you a local API with zero network overhead. ## Key Points - Run `payload generate:types` after schema changes to keep TypeScript types in sync - Use the local API (`getPayload`) instead of REST when code runs in the same process - Use `depth` to control relationship resolution — avoid deep nesting for list views - Use `versions.drafts` for editorial workflows with autosave - Put access control on every collection — default is open to all authenticated users - Use `group` fields for logically related data (SEO, social sharing, metadata) - Use field-level hooks for data normalization (trimming, slugifying, computing) - Use collection hooks for side effects (cache invalidation, notifications, syncing) - Defining collections without access control — every collection needs explicit read/write rules - Using `depth: 0` when you need related data or `depth: 5` when you do not — be intentional - Putting business logic in API route handlers instead of hooks — hooks run for all API surfaces - Not generating types after schema changes — leads to runtime errors that TypeScript would catch ## Quick Example ```bash npx create-payload-app@latest # Or add to existing Next.js project npm install payload @payloadcms/next @payloadcms/db-mongodb @payloadcms/richtext-lexical ``` ```env DATABASE_URI=mongodb://localhost:27017/payload PAYLOAD_SECRET=your-secret-key-min-32-chars NEXT_PUBLIC_SERVER_URL=http://localhost:3000 ```
skilldb get cms-services-skills/payloadFull skill: 378 linesPayload CMS Integration
You are a CMS specialist who integrates Payload into projects. Payload is a code-first, TypeScript-native headless CMS that runs inside your Next.js application. There is no separate CMS server — Payload lives in your Next.js app, shares your database, and gives you a local API with zero network overhead.
Core Philosophy
Code-first, not UI-first
Everything in Payload is defined in TypeScript config files. Collections, fields, hooks, access control, and custom components are all code. The admin panel is auto-generated from your config. This means your CMS is version- controlled, type-safe, and reviewable in pull requests.
Your app, not a separate service
Payload 3.x runs inside Next.js. The admin panel is a set of Next.js routes. The API endpoints are Next.js API routes. The local API calls your database directly with no HTTP overhead. There is no separate process to deploy or manage.
TypeScript all the way
Payload generates TypeScript types from your collections. The local API, hooks, access control functions, and custom components are all fully typed. If your schema changes, TypeScript catches every affected callsite.
Setup
Install
npx create-payload-app@latest
# Or add to existing Next.js project
npm install payload @payloadcms/next @payloadcms/db-mongodb @payloadcms/richtext-lexical
Environment variables
DATABASE_URI=mongodb://localhost:27017/payload
PAYLOAD_SECRET=your-secret-key-min-32-chars
NEXT_PUBLIC_SERVER_URL=http://localhost:3000
Payload config
// payload.config.ts
import { buildConfig } from 'payload';
import { mongooseAdapter } from '@payloadcms/db-mongodb';
import { lexicalEditor } from '@payloadcms/richtext-lexical';
import { Posts } from './collections/Posts';
import { Authors } from './collections/Authors';
import { Media } from './collections/Media';
import { Users } from './collections/Users';
export default buildConfig({
editor: lexicalEditor(),
db: mongooseAdapter({ url: process.env.DATABASE_URI! }),
collections: [Posts, Authors, Media, Users],
secret: process.env.PAYLOAD_SECRET!,
typescript: {
outputFile: 'payload-types.ts', // auto-generated types
},
admin: {
user: Users.slug,
},
});
Key Techniques
Collection definitions
// collections/Posts.ts
import type { CollectionConfig } from 'payload';
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'author', 'status', 'publishedAt'],
},
access: {
read: () => true, // public
create: ({ req: { user } }) => Boolean(user), // authenticated
update: ({ req: { user } }) => Boolean(user),
delete: ({ req: { user } }) => user?.role === 'admin',
},
versions: {
drafts: {
autosave: true,
},
},
fields: [
{
name: 'title',
type: 'text',
required: true,
maxLength: 200,
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
admin: {
position: 'sidebar',
},
hooks: {
beforeValidate: [
({ value, data }) => {
if (!value && data?.title) {
return data.title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
return value;
},
],
},
},
{
name: 'author',
type: 'relationship',
relationTo: 'authors',
required: true,
admin: { position: 'sidebar' },
},
{
name: 'featuredImage',
type: 'upload',
relationTo: 'media',
},
{
name: 'content',
type: 'richText', // Lexical editor
},
{
name: 'excerpt',
type: 'textarea',
maxLength: 500,
},
{
name: 'tags',
type: 'array',
fields: [
{ name: 'tag', type: 'text', required: true },
],
},
{
name: 'seo',
type: 'group',
fields: [
{ name: 'metaTitle', type: 'text', maxLength: 60 },
{ name: 'metaDescription', type: 'textarea', maxLength: 160 },
{ name: 'ogImage', type: 'upload', relationTo: 'media' },
],
},
{
name: 'publishedAt',
type: 'date',
admin: { position: 'sidebar' },
},
{
name: 'status',
type: 'select',
defaultValue: 'draft',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
admin: { position: 'sidebar' },
},
],
};
Media collection with image optimization
// collections/Media.ts
import type { CollectionConfig } from 'payload';
export const Media: CollectionConfig = {
slug: 'media',
upload: {
mimeTypes: ['image/*'],
imageSizes: [
{ name: 'thumbnail', width: 300, height: 200, position: 'centre' },
{ name: 'card', width: 600, height: 400, position: 'centre' },
{ name: 'hero', width: 1200, height: 630, position: 'centre' },
],
adminThumbnail: 'thumbnail',
},
access: {
read: () => true,
},
fields: [
{ name: 'alt', type: 'text', required: true },
{ name: 'caption', type: 'text' },
],
};
Local API (zero network overhead)
// app/blog/page.tsx
import { getPayload } from 'payload';
import config from '@payload-config';
export default async function BlogPage() {
const payload = await getPayload({ config });
const posts = await payload.find({
collection: 'posts',
where: {
status: { equals: 'published' },
publishedAt: { less_than_equal: new Date().toISOString() },
},
sort: '-publishedAt',
limit: 20,
depth: 1, // resolve relationships 1 level deep
});
return (
<ul>
{posts.docs.map((post) => (
<li key={post.id}>
<a href={`/blog/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
);
}
Collection hooks
// collections/Posts.ts (hooks section)
import type { CollectionConfig } from 'payload';
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
beforeChange: [
async ({ data, operation }) => {
// Auto-set publishedAt when status changes to published
if (operation === 'create' || operation === 'update') {
if (data.status === 'published' && !data.publishedAt) {
data.publishedAt = new Date().toISOString();
}
}
return data;
},
],
afterChange: [
async ({ doc, operation, req }) => {
// Revalidate Next.js cache
if (operation === 'create' || operation === 'update') {
const { revalidateTag } = await import('next/cache');
revalidateTag('posts');
revalidateTag(`post-${doc.slug}`);
}
},
],
},
// ... fields
fields: [],
};
Access control
// access/isAdminOrAuthor.ts
import type { Access } from 'payload';
export const isAdminOrAuthor: Access = ({ req: { user } }) => {
if (!user) return false;
if (user.role === 'admin') return true;
// Authors can only see their own posts
return {
author: {
equals: user.id,
},
};
};
// Usage in collection
// access: {
// read: () => true,
// update: isAdminOrAuthor,
// delete: isAdminOrAuthor,
// },
Custom endpoints
// collections/Posts.ts
export const Posts: CollectionConfig = {
slug: 'posts',
endpoints: [
{
path: '/slug/:slug',
method: 'get',
handler: async (req) => {
const slug = req.routeParams?.slug as string;
const payload = req.payload;
const result = await payload.find({
collection: 'posts',
where: { slug: { equals: slug }, status: { equals: 'published' } },
depth: 2,
limit: 1,
});
if (result.docs.length === 0) {
return Response.json({ error: 'Not found' }, { status: 404 });
}
return Response.json(result.docs[0]);
},
},
],
fields: [],
};
Field-level hooks and validation
{
name: 'email',
type: 'email',
required: true,
unique: true,
validate: (value: string) => {
if (value && !value.endsWith('@company.com')) {
return 'Must be a company email address';
}
return true;
},
hooks: {
beforeChange: [
({ value }) => value?.toLowerCase().trim(),
],
},
}
Best Practices
- Run
payload generate:typesafter schema changes to keep TypeScript types in sync - Use the local API (
getPayload) instead of REST when code runs in the same process - Use
depthto control relationship resolution — avoid deep nesting for list views - Use
versions.draftsfor editorial workflows with autosave - Put access control on every collection — default is open to all authenticated users
- Use
groupfields for logically related data (SEO, social sharing, metadata) - Use field-level hooks for data normalization (trimming, slugifying, computing)
- Use collection hooks for side effects (cache invalidation, notifications, syncing)
Anti-Patterns
- Defining collections without access control — every collection needs explicit read/write rules
- Using
depth: 0when you need related data ordepth: 5when you do not — be intentional - Putting business logic in API route handlers instead of hooks — hooks run for all API surfaces
- Not generating types after schema changes — leads to runtime errors that TypeScript would catch
- Running Payload as a separate service from your Next.js app — it is designed to run inside it
- Using the REST API from server components when the local API is available — adds unnecessary latency
- Skipping
unique: trueon slug fields — causes duplicate URL conflicts - Hardcoding user role checks instead of using access control functions — misses API and admin panel access
Install this skill directly: skilldb add cms-services-skills
Related Skills
Prismic
Prismic is a headless CMS focused on content modeling and multi-channel delivery. Use this skill when projects require flexible content structures, rich text editing, internationalization, and a robust GraphQL/REST API for publishing digital experiences across various front-ends.
Sanity
Build with Sanity CMS for structured content management. Use this skill when the project needs a headless CMS with real-time collaboration, GROQ queries, Portable Text rendering, image pipelines, or Next.js integration. Covers schema definitions, content modeling, Studio customization, and webhooks.
Storyblok
Build with Storyblok as a visual headless CMS. Use this skill when the project needs a component-based CMS with a real-time visual editor, nested content blocks, localization, asset management, or framework integration (Next.js, Nuxt, Astro). Covers space setup, content modeling with bloks, the Storyblok Bridge, and webhook-driven revalidation.
Strapi
Build with Strapi for self-hosted headless CMS. Use this skill when the project needs Strapi integration for content types, REST or GraphQL APIs, custom controllers, plugin development, admin panel customization, or authentication. Covers Strapi v5 patterns and deployment.
Tina
Tina is an open-source, Git-backed headless CMS that enables developers to build powerful content-editing experiences directly within their code. Use Tina when you need a local-first CMS for modern web applications, integrating content directly into your Git workflow and leveraging GraphQL for data fetching.
Builder Io
Builder.io is a Visual Headless CMS and API that empowers developers to integrate a drag-and-drop page builder and content management system into any web application. Use this skill when you need a powerful visual editor for marketing and content teams, A/B testing, personalization, and a flexible content API for dynamic web experiences.