Loading...
Loading...
Use when working with Payload projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
npx skill4agent add samunderwood/agent-skills payload| Task | Solution | Details |
|---|---|---|
| Auto-generate slugs | | FIELDS.md#slug-field-helper |
| Restrict content by user | Access control with query | ACCESS-CONTROL.md#row-level-security-with-complex-queries |
| Local API user ops | | QUERIES.md#access-control-in-local-api |
| Draft/publish workflow | | COLLECTIONS.md#versioning--drafts |
| Computed fields | | FIELDS.md#virtual-fields |
| Conditional fields | | FIELDS.md#conditional-fields |
| Custom field validation | | FIELDS.md#text-field |
| Filter relationship list | | FIELDS.md#relationship |
| Select specific fields | | QUERIES.md#local-api |
| Auto-set author/dates | beforeChange hook | HOOKS.md#collection-hooks |
| Prevent hook loops | | HOOKS.md#hook-context |
| Cascading deletes | beforeDelete hook | HOOKS.md#collection-hooks |
| Geospatial queries | | FIELDS.md#point-geolocation |
| Reverse relationships | | FIELDS.md#join-fields |
| Next.js revalidation | Context control in afterChange | HOOKS.md#nextjs-revalidation-with-context-control |
| Query by relationship | Nested property syntax | QUERIES.md#nested-properties |
| Complex queries | AND/OR logic | QUERIES.md#andor-logic |
| Transactions | Pass | ADAPTERS.md#threading-req-through-operations |
| Background jobs | Jobs queue with tasks | ADVANCED.md#jobs-queue |
| Custom API routes | Collection custom endpoints | ADVANCED.md#custom-endpoints |
| Cloud storage | Storage adapter plugins | ADAPTERS.md#storage-adapters |
| Multi-language | | ADVANCED.md#localization |
| Create plugin | | PLUGIN-DEVELOPMENT.md#plugin-architecture |
| Plugin package setup | Package structure with SWC | PLUGIN-DEVELOPMENT.md#plugin-package-structure |
| Add fields to collection | Map collections, spread fields | PLUGIN-DEVELOPMENT.md#adding-fields-to-collections |
| Plugin hooks | Preserve existing hooks in array | PLUGIN-DEVELOPMENT.md#adding-hooks |
| Check field type | Type guard functions | FIELD-TYPE-GUARDS.md |
npx create-payload-app@latest my-app
cd my-app
pnpm devimport { buildConfig } from "payload";
import { mongooseAdapter } from "@payloadcms/db-mongodb";
import { lexicalEditor } from "@payloadcms/richtext-lexical";
import path from "path";
import { fileURLToPath } from "url";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);
export default buildConfig({
admin: {
user: "users",
importMap: {
baseDir: path.resolve(dirname),
},
},
collections: [Users, Media],
editor: lexicalEditor(),
secret: process.env.PAYLOAD_SECRET,
typescript: {
outputFile: path.resolve(dirname, "payload-types.ts"),
},
db: mongooseAdapter({
url: process.env.DATABASE_URL,
}),
});import type { CollectionConfig } from "payload";
export const Posts: CollectionConfig = {
slug: "posts",
admin: {
useAsTitle: "title",
defaultColumns: ["title", "author", "status", "createdAt"],
},
fields: [
{ name: "title", type: "text", required: true },
{ name: "slug", type: "text", unique: true, index: true },
{ name: "content", type: "richText" },
{ name: "author", type: "relationship", relationTo: "users" },
],
timestamps: true,
};// Text field
{ name: 'title', type: 'text', required: true }
// Relationship
{ name: 'author', type: 'relationship', relationTo: 'users', required: true }
// Rich text
{ name: 'content', type: 'richText', required: true }
// Select
{ name: 'status', type: 'select', options: ['draft', 'published'], defaultValue: 'draft' }
// Upload
{ name: 'image', type: 'upload', relationTo: 'media' }export const Posts: CollectionConfig = {
slug: "posts",
hooks: {
beforeChange: [
async ({ data, operation }) => {
if (operation === "create") {
data.slug = slugify(data.title);
}
return data;
},
],
},
fields: [{ name: "title", type: "text" }],
};import type { Access } from "payload";
import type { User } from "@/payload-types";
// Type-safe access control
export const adminOnly: Access = ({ req }) => {
const user = req.user as User;
return user?.roles?.includes("admin") || false;
};
// Row-level access control
export const ownPostsOnly: Access = ({ req }) => {
const user = req.user as User;
if (!user) return false;
if (user.roles?.includes("admin")) return true;
return {
author: { equals: user.id },
};
};// Local API
const posts = await payload.find({
collection: "posts",
where: {
status: { equals: "published" },
"author.name": { contains: "john" },
},
depth: 2,
limit: 10,
sort: "-createdAt",
});
// Query with populated relationships
const post = await payload.findByID({
collection: "posts",
id: "123",
depth: 2, // Populates relationships (default is 2)
});
// Returns: { author: { id: "user123", name: "John" } }
// Without depth, relationships return IDs only
const post = await payload.findByID({
collection: "posts",
id: "123",
depth: 0,
});
// Returns: { author: "user123" }// In API routes (Next.js)
import { getPayload } from 'payload'
import config from '@payload-config'
export async function GET() {
const payload = await getPayload({ config })
const posts = await payload.find({
collection: 'posts',
})
return Response.json(posts)
}
// In Server Components
import { getPayload } from 'payload'
import config from '@payload-config'
export default async function Page() {
const payload = await getPayload({ config })
const { docs } = await payload.find({ collection: 'posts' })
return <div>{docs.map(post => <h1 key={post.id}>{post.title}</h1>)}</div>
}// ✅ Valid: single string
payload.logger.error("Something went wrong");
// ✅ Valid: object with msg and err
payload.logger.error({ msg: "Failed to process", err: error });
// ❌ Invalid: don't pass error as second argument
payload.logger.error("Failed to process", error);
// ❌ Invalid: use `err` not `error`, use `msg` not `message`
payload.logger.error({ message: "Failed", error: error });// ❌ SECURITY BUG: Passes user but ignores their permissions
await payload.find({
collection: "posts",
user: someUser, // Access control is BYPASSED!
});
// ✅ SECURE: Actually enforces the user's permissions
await payload.find({
collection: "posts",
user: someUser,
overrideAccess: false, // REQUIRED for access control
});overrideAccess: trueoverrideAccess: falsereq// ❌ DATA CORRUPTION RISK: Separate transaction
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: "audit-log",
data: { docId: doc.id },
// Missing req - runs in separate transaction!
});
},
];
}
// ✅ ATOMIC: Same transaction
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.create({
collection: "audit-log",
data: { docId: doc.id },
req, // Maintains atomicity
});
},
];
}// ❌ INFINITE LOOP
hooks: {
afterChange: [
async ({ doc, req }) => {
await req.payload.update({
collection: "posts",
id: doc.id,
data: { views: doc.views + 1 },
req,
}); // Triggers afterChange again!
},
];
}
// ✅ SAFE: Use context flag
hooks: {
afterChange: [
async ({ doc, req, context }) => {
if (context.skipHooks) return;
await req.payload.update({
collection: "posts",
id: doc.id,
data: { views: doc.views + 1 },
context: { skipHooks: true },
req,
});
},
];
}src/
├── app/
│ ├── (frontend)/
│ │ └── page.tsx
│ └── (payload)/
│ └── admin/[[...segments]]/page.tsx
├── collections/
│ ├── Posts.ts
│ ├── Media.ts
│ └── Users.ts
├── globals/
│ └── Header.ts
├── components/
│ └── CustomField.tsx
├── hooks/
│ └── slugify.ts
└── payload.config.ts// payload.config.ts
export default buildConfig({
typescript: {
outputFile: path.resolve(dirname, "payload-types.ts"),
},
// ...
});
// Usage
import type { Post, User } from "@/payload-types";