--- title: "PayloadCMS — Plugins (Overview + Official)" tags: [payloadcms, tech-patterns] topic: payloadcms sources: [plugins__overview.md, plugins__seo.md, plugins__search.md, plugins__form-builder.md, plugins__redirects.md, plugins__nested-docs.md, plugins__multi-tenant.md, plugins__stripe.md, plugins__sentry.md, plugins__import-export.md, plugins__mcp.md] created: 2026-05-15 updated: 2026-05-15 --- # PayloadCMS — Plugins (Overview + Official) ## Overview - Plugins are functions: `(incomingConfig: Config) => Config` - Added to `plugins: []` array in [[wiki/payloadcms/configuration|Configuration]] - Execute **after** config validation, **before** default-option merging and sanitization - Can add collections, globals, fields, [[wiki/payloadcms/hooks|Hooks]], endpoints, admin views - Official plugins live in the [Payload monorepo packages directory](https://github.com/payloadcms/payload/tree/main/packages) - Community plugins: search `payload-plugin` topic 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: ```ts 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: ```ts import { buildConfig } from 'payload' import { addLastModified } from './addLastModified' export default buildConfig({ plugins: [addLastModified], }) ``` > See [[wiki/payloadcms/plugins-api|Plugin API]] for the full `definePlugin` advanced 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 ```bash pnpm add @payloadcms/plugin-seo ``` - Adds `meta` group (`title`, `description`, `image`) to enabled collections/globals - Auto-generate buttons call your custom `generateTitle` / `generateDescription` / `generateImage` functions - Live preview renders a SERP snippet with character counters - Supports `tabbedUI` to place meta in its own tab - Custom fields can be injected via `fields: ({ defaultFields }) => [...]` ```ts 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 ```bash pnpm add @payloadcms/plugin-search ``` - Creates an indexed `search` collection — sync-critical fields only (title, slug, excerpt) - Auto-creates, updates, deletes search records via `beforeChange` / `afterDelete` [[wiki/payloadcms/hooks|Hooks]] - First-party alternative to Algolia/Elasticsearch - `priority` field enables custom result ranking per collection or document - "Reindex" button in admin list view for re-syncing existing documents ```ts 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 ```bash pnpm add @payloadcms/plugin-form-builder ``` - Adds `forms` and `form-submissions` collections - 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 `handlePayment` callback - Upload field requires `uploadCollections` in config; supports multipart or pre-upload flows ```ts 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 ```bash pnpm add @payloadcms/plugin-redirects ``` - Adds `redirects` collection with `from` (text) and `to` (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']`) and `overrides` for custom fields ```ts import { redirectsPlugin } from '@payloadcms/plugin-redirects' redirectsPlugin({ collections: ['pages'], redirectTypes: ['301', '302'], }) ``` --- ## Nested Docs Plugin ```bash pnpm add @payloadcms/plugin-nested-docs ``` - Adds `parent` relationship field (self-referencing within the collection) - Adds `breadcrumbs` array field populated up the full ancestor tree - Cascades changes recursively — editing a grandparent updates all descendants - Supports localization automatically if `localization` is set in [[wiki/payloadcms/configuration|Configuration]] ```ts 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 ```bash pnpm add @payloadcms/plugin-multi-tenant ``` - Adds `tenant` relationship field to all specified collections - Adds tenant selector to admin panel; list views auto-filter by selected tenant - Supports `isGlobal: true` for one-doc-per-tenant pattern - `userHasAccessToAllTenants` enables super-admin bypass - Exports `useTenantSelection` React hook for custom components ```ts import { multiTenantPlugin } from '@payloadcms/plugin-multi-tenant' multiTenantPlugin({ 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 ```bash pnpm add @payloadcms/plugin-stripe ``` - Proxies Stripe REST API through Payload access control at `/api/stripe/rest` - Handles Stripe webhooks at `/api/stripe/webhooks` - `sync` config sets up two-way sync: creates hooks + webhook handlers automatically - Adds `stripeID` and `skipSync` fields to synced collections ```ts 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 ```bash 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 `context` callback for enriching events with locale, user, etc. - For Postgres query traces, inject Sentry-patched `pg` driver into `postgresAdapter` ```ts 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 ```bash pnpm add @payloadcms/plugin-import-export ``` - Adds `exports` and `imports` upload collections (hidden from nav by default) - Export formats: CSV (underscore-notation for nested fields) and JSON - Uses Payload [[wiki/payloadcms/jobs-queue|Jobs Queue]] by default — requires `jobs.autoRun` config - Import modes: `create`, `update`, `upsert` (with `matchField` for non-ID matching) - Per-batch hooks (`before` / `after`) for data transformation and logging - Field-level `custom['plugin-import-export']` config for disabled fields and `beforeExport`/`beforeImport` hooks ```ts 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 ```bash pnpm add @payloadcms/plugin-mcp ``` - Adds a Model Context Protocol server at `/api/mcp` - Adds an `API Keys` collection 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`, and `resources` with Zod parameter schemas - Use `select` parameter to limit returned fields and reduce token usage - Use `overrideResponse` to sanitize sensitive data before it reaches the model ```ts 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:** ```bash 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 - `forms` collection 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 ## Related - [[wiki/payloadcms/configuration|Configuration]] - [[wiki/payloadcms/hooks|Hooks]] - [[wiki/payloadcms/collections|Collections]] - [[wiki/payloadcms/jobs-queue|Jobs Queue]] - [[wiki/payloadcms/plugins-api|Plugin API]]