Builds full-stack applications with Payload CMS, the Next.js-native headless CMS. Use when creating content-driven apps with TypeScript, code-first configuration, and full control over your backend.
Builds full-stack apps with Payload CMS using code-first configuration and TypeScript. Use when creating content-driven Next.js applications that require a self-hosted headless CMS with automatic REST/GraphQL APIs.
/plugin marketplace add mgd34msu/goodvibes-plugin/plugin install goodvibes@goodvibes-marketThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Open-source, Next.js-native headless CMS with full TypeScript support. Code-first configuration, self-hosted, with REST and GraphQL APIs.
npx create-payload-app@latest my-app
cd my-app
npm run dev
Opens admin at http://localhost:3000/admin.
my-app/
app/ # Next.js app directory
(payload)/ # Payload admin routes
collections/ # Content type definitions
payload.config.ts # Main configuration
payload-types.ts # Generated types
// payload.config.ts
import { buildConfig } from 'payload';
import { mongooseAdapter } from '@payloadcms/db-mongodb';
// or: import { postgresAdapter } from '@payloadcms/db-postgres';
import { Posts } from './collections/Posts';
import { Users } from './collections/Users';
import { Media } from './collections/Media';
export default buildConfig({
admin: {
user: Users.slug,
},
collections: [Users, Posts, Media],
db: mongooseAdapter({
url: process.env.MONGODB_URI!,
}),
typescript: {
outputFile: 'payload-types.ts',
},
secret: process.env.PAYLOAD_SECRET!,
});
// collections/Posts.ts
import { CollectionConfig } from 'payload';
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'status', 'publishedAt'],
},
access: {
read: () => true, // Public read
create: ({ req }) => !!req.user, // Authenticated only
update: ({ req }) => !!req.user,
delete: ({ req }) => !!req.user,
},
versions: {
drafts: true,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'slug',
type: 'text',
unique: true,
admin: {
position: 'sidebar',
},
hooks: {
beforeValidate: [
({ value, data }) => value || data?.title?.toLowerCase().replace(/\s+/g, '-'),
],
},
},
{
name: 'author',
type: 'relationship',
relationTo: 'users',
},
{
name: 'publishedAt',
type: 'date',
admin: {
position: 'sidebar',
},
},
{
name: 'status',
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
defaultValue: 'draft',
admin: {
position: 'sidebar',
},
},
{
name: 'content',
type: 'richText',
},
{
name: 'featuredImage',
type: 'upload',
relationTo: 'media',
},
{
name: 'categories',
type: 'relationship',
relationTo: 'categories',
hasMany: true,
},
],
};
// Text
{ name: 'title', type: 'text', required: true }
// Textarea
{ name: 'excerpt', type: 'textarea' }
// Rich Text (Lexical)
{ name: 'content', type: 'richText' }
// Number
{ name: 'price', type: 'number', min: 0 }
// Email
{ name: 'email', type: 'email' }
// Date
{ name: 'publishedAt', type: 'date' }
// Checkbox
{ name: 'featured', type: 'checkbox', defaultValue: false }
// Select
{
name: 'status',
type: 'select',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
}
// Radio
{
name: 'type',
type: 'radio',
options: ['video', 'article', 'podcast'],
}
// Relationship
{
name: 'author',
type: 'relationship',
relationTo: 'users',
}
// Upload
{
name: 'image',
type: 'upload',
relationTo: 'media',
}
// Array (repeatable)
{
name: 'gallery',
type: 'array',
fields: [
{ name: 'image', type: 'upload', relationTo: 'media' },
{ name: 'caption', type: 'text' },
],
}
// Group (nested object)
{
name: 'meta',
type: 'group',
fields: [
{ name: 'title', type: 'text' },
{ name: 'description', type: 'textarea' },
],
}
// Blocks (flexible content)
{
name: 'layout',
type: 'blocks',
blocks: [HeroBlock, ContentBlock, CTABlock],
}
// blocks/Hero.ts
import { Block } from 'payload';
export const HeroBlock: Block = {
slug: 'hero',
labels: {
singular: 'Hero',
plural: 'Heroes',
},
fields: [
{ name: 'heading', type: 'text', required: true },
{ name: 'subheading', type: 'text' },
{ name: 'image', type: 'upload', relationTo: 'media' },
{
name: 'cta',
type: 'group',
fields: [
{ name: 'label', type: 'text' },
{ name: 'link', type: 'text' },
],
},
],
};
export const Posts: CollectionConfig = {
slug: 'posts',
access: {
// Function-based access
read: ({ req }) => {
// Published posts are public
if (!req.user) {
return { status: { equals: 'published' } };
}
// Logged in users see all
return true;
},
create: ({ req }) => !!req.user,
update: ({ req }) => {
if (!req.user) return false;
// Admins can update all
if (req.user.role === 'admin') return true;
// Authors can only update own posts
return {
author: { equals: req.user.id },
};
},
delete: ({ req }) => req.user?.role === 'admin',
},
};
export const Posts: CollectionConfig = {
slug: 'posts',
hooks: {
beforeChange: [
async ({ data, req, operation }) => {
if (operation === 'create') {
data.author = req.user?.id;
}
return data;
},
],
afterChange: [
async ({ doc, operation }) => {
if (operation === 'create') {
// Send notification, revalidate cache, etc.
await revalidatePath('/posts');
}
},
],
beforeRead: [
async ({ doc, req }) => {
// Transform document before returning
return doc;
},
],
},
fields: [/* ... */],
};
Auto-generated at /api/{collection}.
// Get all posts
const response = await fetch('/api/posts');
const { docs, totalDocs, page, limit } = await response.json();
// Get single post
const post = await fetch('/api/posts/123').then(r => r.json());
// Query with parameters
const params = new URLSearchParams({
where: JSON.stringify({
status: { equals: 'published' },
publishedAt: { less_than: new Date().toISOString() },
}),
sort: '-publishedAt',
limit: '10',
page: '1',
depth: '2',
});
const filtered = await fetch(`/api/posts?${params}`).then(r => r.json());
// Equals
{ field: { equals: 'value' } }
// Not equals
{ field: { not_equals: 'value' } }
// Greater/less than
{ field: { greater_than: 100 } }
{ field: { less_than: 100 } }
{ field: { greater_than_equal: 100 } }
{ field: { less_than_equal: 100 } }
// Contains (string)
{ field: { contains: 'text' } }
// In array
{ field: { in: ['a', 'b', 'c'] } }
// Not in array
{ field: { not_in: ['x', 'y'] } }
// Exists
{ field: { exists: true } }
// Logical operators
{
or: [
{ status: { equals: 'published' } },
{ featured: { equals: true } },
],
}
{
and: [
{ status: { equals: 'published' } },
{ category: { equals: 'tech' } },
],
}
Available at /api/graphql.
query {
Posts(
where: { status: { equals: published } }
sort: "-publishedAt"
limit: 10
) {
docs {
id
title
slug
author {
name
}
}
totalDocs
}
}
Use in Server Components, API routes, or hooks.
import { getPayload } from 'payload';
import config from '@payload-config';
const payload = await getPayload({ config });
// Find many
const posts = await payload.find({
collection: 'posts',
where: {
status: { equals: 'published' },
},
sort: '-publishedAt',
limit: 10,
depth: 2,
});
// Find one
const post = await payload.findByID({
collection: 'posts',
id: '123',
depth: 2,
});
// Create
const newPost = await payload.create({
collection: 'posts',
data: {
title: 'New Post',
content: '...',
},
});
// Update
const updated = await payload.update({
collection: 'posts',
id: '123',
data: {
title: 'Updated Title',
},
});
// Delete
await payload.delete({
collection: 'posts',
id: '123',
});
Types are auto-generated to payload-types.ts.
import { Post, User } from './payload-types';
// Fully typed
const posts: Post[] = await payload.find({
collection: 'posts',
});
posts.forEach((post: Post) => {
console.log(post.title); // TypeScript knows this exists
});
// app/posts/page.tsx
import { getPayload } from 'payload';
import config from '@payload-config';
export default async function PostsPage() {
const payload = await getPayload({ config });
const { docs: posts } = await payload.find({
collection: 'posts',
where: { status: { equals: 'published' } },
sort: '-publishedAt',
});
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<a href={`/posts/${post.slug}`}>{post.title}</a>
</li>
))}
</ul>
);
}
// app/posts/[slug]/page.tsx
import { getPayload } from 'payload';
import config from '@payload-config';
import { notFound } from 'next/navigation';
export async function generateStaticParams() {
const payload = await getPayload({ config });
const { docs } = await payload.find({ collection: 'posts' });
return docs.map((post) => ({ slug: post.slug }));
}
export default async function PostPage({ params }: { params: { slug: string } }) {
const payload = await getPayload({ config });
const { docs } = await payload.find({
collection: 'posts',
where: { slug: { equals: params.slug } },
depth: 2,
});
if (!docs[0]) notFound();
const post = docs[0];
return (
<article>
<h1>{post.title}</h1>
{/* Render content */}
</article>
);
}
// collections/Media.ts
import { CollectionConfig } from 'payload';
export const Media: CollectionConfig = {
slug: 'media',
upload: {
staticDir: 'public/media',
imageSizes: [
{
name: 'thumbnail',
width: 400,
height: 300,
position: 'centre',
},
{
name: 'card',
width: 768,
height: 1024,
position: 'centre',
},
],
mimeTypes: ['image/*'],
},
fields: [
{ name: 'alt', type: 'text', required: true },
{ name: 'caption', type: 'text' },
],
};
// globals/Settings.ts
import { GlobalConfig } from 'payload';
export const Settings: GlobalConfig = {
slug: 'settings',
access: {
read: () => true,
},
fields: [
{ name: 'siteName', type: 'text', required: true },
{ name: 'siteDescription', type: 'textarea' },
{
name: 'navigation',
type: 'array',
fields: [
{ name: 'label', type: 'text' },
{ name: 'link', type: 'text' },
],
},
],
};
// Usage
const settings = await payload.findGlobal({ slug: 'settings' });
// Login
const user = await payload.login({
collection: 'users',
data: {
email: 'user@example.com',
password: 'password',
},
});
// Current user (in hooks/access control)
const user = req.user;
// Logout
await payload.logout({
collection: 'users',
});
# Build
npm run build
# Start production
npm run start
Deploy to:
npm run generate:typesThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.