13 KiB
| title | tags | topic | sources | created | updated | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| PayloadCMS — Plugins (Overview + Official) |
|
payloadcms |
|
2026-05-15 | 2026-05-15 |
PayloadCMS — Plugins (Overview + Official)
Overview
- Plugins are functions:
(incomingConfig: Config) => Config - Added to
plugins: []array in wiki/payloadcms/configuration - Execute after config validation, before default-option merging and sanitization
- Can add collections, globals, fields, wiki/payloadcms/hooks, endpoints, admin views
- Official plugins live in the Payload monorepo packages directory
- Community plugins: search
payload-plugintopic on GitHub
Example Use Cases
- Sync collection data to HubSpot / CRM on change
- Password-protect specific documents
- Add full e-commerce backend
- Add custom reporting views to admin panel
- Encrypt specific collections
- Integrate upload collections with S3 / Cloudinary
- Add custom GraphQL queries or endpoints
Example Plugin — addLastModified
Adds a lastModifiedBy relationship field to every collection:
import { Config, Plugin } from 'payload'
export const addLastModified: Plugin = (incomingConfig: Config): Config => {
const authEnabledCollections = incomingConfig.collections.filter(
(collection) => Boolean(collection.auth),
)
return {
...incomingConfig,
collections: incomingConfig.collections.map((collection) => ({
...collection,
fields: [
...collection.fields,
{
name: 'lastModifiedBy',
type: 'relationship',
relationTo: authEnabledCollections.map(({ slug }) => slug),
hooks: {
beforeChange: [
({ req }) => ({
value: req?.user?.id,
relationTo: req?.user?.collection,
}),
],
},
admin: { position: 'sidebar', readOnly: true },
},
],
})),
}
}
Register it:
import { buildConfig } from 'payload'
import { addLastModified } from './addLastModified'
export default buildConfig({
plugins: [addLastModified],
})
See wiki/payloadcms/plugins-api for the full
definePluginadvanced API.
Plugin Quick Reference
| Plugin | Package | Purpose |
|---|---|---|
| SEO | @payloadcms/plugin-seo |
Meta title/description/image fields + preview |
| Search | @payloadcms/plugin-search |
Fast indexed search collection |
| Form Builder | @payloadcms/plugin-form-builder |
Dynamic forms with email + payments |
| Redirects | @payloadcms/plugin-redirects |
Managed redirect rules |
| Nested Docs | @payloadcms/plugin-nested-docs |
Parent/child hierarchy + breadcrumbs |
| Multi-Tenant | @payloadcms/plugin-multi-tenant |
Multi-tenancy with tenant scoping |
| Stripe | @payloadcms/plugin-stripe |
Stripe payments + two-way sync |
| Sentry | @payloadcms/plugin-sentry |
Error tracking + performance monitoring |
| Import/Export | @payloadcms/plugin-import-export |
CSV/JSON import and export |
| MCP | @payloadcms/plugin-mcp |
Model Context Protocol server for AI |
SEO Plugin
pnpm add @payloadcms/plugin-seo
- Adds
metagroup (title,description,image) to enabled collections/globals - Auto-generate buttons call your custom
generateTitle/generateDescription/generateImagefunctions - Live preview renders a SERP snippet with character counters
- Supports
tabbedUIto place meta in its own tab - Custom fields can be injected via
fields: ({ defaultFields }) => [...]
import { seoPlugin } from '@payloadcms/plugin-seo'
seoPlugin({
collections: ['pages', 'posts'],
uploadsCollection: 'media',
generateTitle: ({ doc }) => `MySite — ${doc.title}`,
generateDescription: ({ doc }) => doc.excerpt,
generateURL: ({ doc, collectionSlug }) =>
`https://example.com/${collectionSlug}/${doc.slug}`,
})
Key options: collections, globals, uploadsCollection, tabbedUI, generateTitle, generateDescription, generateImage, generateURL, interfaceName, fields
Search Plugin
pnpm add @payloadcms/plugin-search
- Creates an indexed
searchcollection — sync-critical fields only (title, slug, excerpt) - Auto-creates, updates, deletes search records via
beforeChange/afterDeletewiki/payloadcms/hooks - First-party alternative to Algolia/Elasticsearch
priorityfield enables custom result ranking per collection or document- "Reindex" button in admin list view for re-syncing existing documents
import { searchPlugin } from '@payloadcms/plugin-search'
searchPlugin({
collections: ['pages', 'posts'],
defaultPriorities: { pages: 10, posts: 20 },
beforeSync: ({ originalDoc, searchDoc }) => ({
...searchDoc,
excerpt: originalDoc?.excerpt ?? '',
}),
})
Key options: collections, defaultPriorities, searchOverrides, beforeSync, syncDrafts, deleteDrafts, skipSync, reindexBatchSize, localize
Form Builder Plugin
pnpm add @payloadcms/plugin-form-builder
- Adds
formsandform-submissionscollections - Admin builds form schema on-the-fly; frontend maps over blocks to render UI
- Supports email notifications with dynamic
{{field_name}}interpolation - Payment field integrates with any payment processor via
handlePaymentcallback - Upload field requires
uploadCollectionsin config; supports multipart or pre-upload flows
import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'
formBuilderPlugin({
fields: { text: true, email: true, select: true, payment: false },
redirectRelationships: ['pages'],
defaultToEmail: 'team@example.com',
})
Field types available: text, textarea, select, radio, email, state, country, checkbox, number, message, date, payment, upload
Gotcha: forms collection is publicly readable by default — override access control if you have authenticated front-end users.
Redirects Plugin
pnpm add @payloadcms/plugin-redirects
- Adds
redirectscollection withfrom(text) andto(URL or document relationship) fields - Plugin only stores rules — you implement redirect logic in your frontend (Next.js middleware, Express, etc.)
- Supports optional
redirectTypes(['301', '302']) andoverridesfor custom fields
import { redirectsPlugin } from '@payloadcms/plugin-redirects'
redirectsPlugin({
collections: ['pages'],
redirectTypes: ['301', '302'],
})
Nested Docs Plugin
pnpm add @payloadcms/plugin-nested-docs
- Adds
parentrelationship field (self-referencing within the collection) - Adds
breadcrumbsarray field populated up the full ancestor tree - Cascades changes recursively — editing a grandparent updates all descendants
- Supports localization automatically if
localizationis set in wiki/payloadcms/configuration
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
nestedDocsPlugin({
collections: ['pages'],
generateLabel: (_, doc) => String(doc.title),
generateURL: (docs) =>
docs.reduce((url, doc) => `${url}/${String(doc.slug)}`, ''),
})
Key options: collections, generateLabel, generateURL, parentFieldSlug, breadcrumbsFieldSlug
Multi-Tenant Plugin
pnpm add @payloadcms/plugin-multi-tenant
- Adds
tenantrelationship field to all specified collections - Adds tenant selector to admin panel; list views auto-filter by selected tenant
- Supports
isGlobal: truefor one-doc-per-tenant pattern userHasAccessToAllTenantsenables super-admin bypass- Exports
useTenantSelectionReact hook for custom components
import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant'
multiTenantPlugin<Config>({
collections: {
pages: {},
navigation: { isGlobal: true },
},
tenantsSlug: 'tenants',
})
Gotcha: By default, deleting a tenant removes all its documents. Set cleanupAfterTenantDelete: false to disable. Add strong access control on the tenants collection.
Stripe Plugin
pnpm add @payloadcms/plugin-stripe
- Proxies Stripe REST API through Payload access control at
/api/stripe/rest - Handles Stripe webhooks at
/api/stripe/webhooks syncconfig sets up two-way sync: creates hooks + webhook handlers automatically- Adds
stripeIDandskipSyncfields to synced collections
import { stripePlugin } from '@payloadcms/plugin-stripe'
stripePlugin({
stripeSecretKey: process.env.STRIPE_SECRET_KEY,
stripeWebhooksEndpointSecret: process.env.STRIPE_WEBHOOKS_ENDPOINT_SECRET,
webhooks: {
'customer.subscription.updated': ({ event, stripe }) => { /* ... */ },
},
sync: [
{
collection: 'customers',
stripeResourceType: 'customers',
stripeResourceTypeSingular: 'customer',
fields: [{ fieldPath: 'name', stripeProperty: 'name' }],
},
],
})
Gotcha: REST proxy (rest: true) exposes full Stripe API to authenticated users — disable in production, use stripeProxy server-side instead. For Vercel serverless, install @vercel/functions for webhook waitUntil support.
Sentry Plugin
pnpm add @payloadcms/plugin-sentry
- Requires Sentry Next.js setup first:
npx @sentry/wizard@latest -i nextjs - Passes Sentry instance directly into plugin; no DSN config needed in plugin options
- Captures errors ≥ 500 by default; add codes with
captureErrors: [400, 403] - Supports
contextcallback for enriching events with locale, user, etc. - For Postgres query traces, inject Sentry-patched
pgdriver intopostgresAdapter
import * as Sentry from '@sentry/nextjs'
import { sentryPlugin } from '@payloadcms/plugin-sentry'
sentryPlugin({
Sentry,
options: {
captureErrors: [400, 403],
context: ({ defaultContext, req }) => ({
...defaultContext,
tags: { locale: req.locale },
}),
},
})
Import/Export Plugin
pnpm add @payloadcms/plugin-import-export
- Adds
exportsandimportsupload collections (hidden from nav by default) - Export formats: CSV (underscore-notation for nested fields) and JSON
- Uses Payload wiki/payloadcms/jobs-queue by default — requires
jobs.autoRunconfig - Import modes:
create,update,upsert(withmatchFieldfor non-ID matching) - Per-batch hooks (
before/after) for data transformation and logging - Field-level
custom['plugin-import-export']config for disabled fields andbeforeExport/beforeImporthooks
import { importExportPlugin } from '@payloadcms/plugin-import-export'
importExportPlugin({
collections: [
{ slug: 'users' },
{
slug: 'pages',
export: { format: 'csv', disableDownload: false },
import: { defaultVersionStatus: 'draft' },
},
{ slug: 'posts', export: false },
],
exportLimit: 10000,
})
Gotcha: Users with read access to the exports upload collection can download data bypassing normal access control. Always override overrideExportCollection to add access restrictions for sensitive collections. Add jobs.autoRun or set disableJobsQueue: true per collection for synchronous mode.
MCP Plugin
pnpm add @payloadcms/plugin-mcp
- Adds a Model Context Protocol server at
/api/mcp - Adds an
API Keyscollection for managing per-key capabilities - Exposes Payload collections and globals as MCP tools (
find,create,update,delete) - Two-step access: (1) enable in config, (2) allow in admin API key
- Supports custom
tools,prompts, andresourceswith Zod parameter schemas - Use
selectparameter to limit returned fields and reduce token usage - Use
overrideResponseto sanitize sensitive data before it reaches the model
import { mcpPlugin } from '@payloadcms/plugin-mcp'
mcpPlugin({
collections: {
posts: {
enabled: { find: true, create: true, update: true, delete: false },
description: 'Published blog posts with title, body, and tags.',
},
},
globals: {
'site-settings': { enabled: { find: true, update: true } },
},
})
Connect Claude Code:
claude mcp add --transport http Payload http://127.0.0.1:3000/api/mcp \
--header "Authorization: Bearer MCP-USER-API-KEY"
Gotcha: Models receive full documents by default — use select or overrideResponse to strip sensitive/large fields. Only enable the operations a model actually needs. Detect MCP context in hooks via req.payloadAPI === 'MCP'.
Gotchas
- Plugins run after config validation but before sanitization — spread existing arrays/objects, never replace
formscollection access is public by default — override for authenticated apps- Stripe REST proxy must be disabled in production
- Multi-tenant: tenant deletion cascades to documents by default
- Import/Export: jobs runner required unless
disableJobsQueue: true - MCP: two-step access (config + API key) — enabling in config alone is not enough
- Sentry: complete Next.js Sentry wizard setup before adding the plugin