Adds a collaborative commenting system to the Payload v3 admin. It patches every collection and global to show comment badges on field labels, a per-document comments drawer and a global comments panel in the header. Comments are stored in an auto-generated collection, and mentioned users can be notified by email through Resend. Requires importing a stylesheet, regenerating the import map and a migration on SQL adapters.
Installation
npm i @focus-reactive/payload-plugin-comments29
Stars
3
Forks
24
Open Issues
Today
Last Updated
0
Weekly Downloads
v1.11.3
npm Version
624.8 KB
Unpacked Size
7
Dependencies
Trends appear after 7 days of data. Daily snapshots have not started yet.
Shared by 11 plugins in focusreactive/payload-plugins.
Trends appear after 7 days of data. Daily snapshots have not started yet.
Each point is npm's rolling 7-day download count.
Health Score
Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.
Badge for your README
[](https://payloaddirectory.dev/plugins/1177993877-payload-plugin-comments)<a href="https://payloaddirectory.dev/plugins/1177993877-payload-plugin-comments"><img src="https://payloaddirectory.dev/badge/1177993877-payload-plugin-comments.svg" alt="Payload Directory"></a>README
A collaborative commenting plugin for Payload CMS v3. Adds a full-featured comments system to the Payload admin panel — supporting document-level and field-level comments on both collections and globals, @mentions with email notifications, comment resolution, multi-tenancy, and locale-aware filtering.
Copy and paste this prompt into your AI assistant (Cursor, Claude, etc.) to integrate the plugin into an existing Payload v3 project.
I want to add a collaborative commenting system to my Payload CMS v3 project using @focus-reactive/payload-plugin-comments.
## How it works
The plugin injects into every collection and global:
- A field-level comment badge on every field label — shows comment count, opens a popup to post
- A document-level comments drawer (sidebar) scoped to the current document
- A global comments panel (header button) listing all comments across every document and global
Comments are stored in an auto-generated `comments` collection (hidden from the sidebar). Each comment records:
- documentId / collectionSlug / globalSlug — what it belongs to
- fieldPath — dot-notation field path (null = document-level)
- locale — for field-level comments (null = shown in all locales)
- text — may contain @(userId) mention tokens
- author, mentions, isResolved, resolvedBy, resolvedAt
- tenant (optional, when multi-tenancy is enabled)
@mentions use autocomplete from the users collection. Mentioned users receive email notifications via Resend.
Comments are automatically deleted when their parent document is deleted.
## Installation
pnpm add @focus-reactive/payload-plugin-comments
## Step 1 — Register the plugin in payload.config.ts
import { buildConfig } from 'payload'
import { commentsPlugin } from '@focus-reactive/payload-plugin-comments'
export default buildConfig({
plugins: [
commentsPlugin({
// Optional: specify which field to use as document title in the UI
collections: [
{ slug: 'pages', titleField: 'title' },
{ slug: 'products', titleField: 'name' },
],
// Optional: customize the display name field on users
usernameFieldPath: 'name', // default
}),
],
})
## Step 2 — Import the stylesheet
In your global CSS or admin layout:
@import "@focus-reactive/payload-plugin-comments/styles.css";
Or in a TypeScript/JS file:
import "@focus-reactive/payload-plugin-comments/styles.css";
## Step 3 — Regenerate the import map
Run this after adding the plugin so Payload registers the plugin's admin components:
npx payload generate:importmap
(Or the equivalent for your package manager: pnpm payload generate:importmap / bunx payload generate:importmap)
If you skip this step the comment badges, drawer, and header button will not appear in the admin UI.
## Step 4 — Create and run a migration (SQL adapters only)
The plugin adds a `comments` collection to your database. If you use PostgreSQL or SQLite, create and apply a migration:
npx payload migrate:create create_comments
npx payload migrate
Skip this step if you use the MongoDB adapter.
## Optional: Multi-tenancy
commentsPlugin({
tenant: {
enabled: true,
collectionSlug: 'tenants', // default
documentTenantField: 'tenant', // default
},
})
## Optional: Collection overrides
commentsPlugin({
overrides: {
access: { /* custom access control */ },
fields: (defaultFields) => [...defaultFields],
hooks: {
afterChange: [async ({ doc }) => { /* ... */ }],
},
},
})
## Environment variables (for email notifications)
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
[email protected]
## Important notes
- All collections and globals automatically get field-level comments — `collections` config only sets UI metadata (titleField).
- Comments on globals use globalSlug instead of collectionSlug/documentId.
- Field-level comments are locale-scoped; document-level comments show in all locales.
- The `comments` collection is hidden from the admin sidebar by default.
- Auto-cleanup: when a document is deleted, all its comments are deleted too.
- usernameFieldPath supports dot-notation (e.g. "profile.displayName").
- If RESEND_FROM_EMAIL is not set, mention emails are silently skipped.
A header button in the Payload admin opens a drawer listing all comments across every document and collection.
When viewing a document, a side panel shows all comments scoped to that document with filter tabs (Open / Resolved / Mentioned me).
Clicking the comment badge on a field label opens a popup where you can write and post a new comment for that specific field.
The comment button embedded in the field label has two visual states: no comments (inactive, appears on hover) and one or more open comments (active, showing the count badge).
| Inactive (no comments) | Active (has comments) |
|---|---|
@name autocomplete@payloadcms/plugin-multi-tenantpnpm add @focus-reactive/payload-plugin-comments
npm install @focus-reactive/payload-plugin-comments
# or
yarn add @focus-reactive/payload-plugin-comments
Peer dependencies: payload ^3.0.0, @payloadcms/ui ^3.0.0. next ^14 || ^15, react ^18 || ^19, react-dom, and @payloadcms/plugin-multi-tenant are optional.
// payload.config.ts
import { buildConfig } from "payload";
import { commentsPlugin } from "@focus-reactive/payload-plugin-comments";
export default buildConfig({
plugins: [
commentsPlugin({
// Optional: customize which field is used as the document title in the UI
collections: [
{ slug: "pages", titleField: "title" },
{ slug: "products", titleField: "name" },
],
}),
],
// ... rest of your config
});
Add the plugin's stylesheet to your global CSS or admin layout:
/* global CSS */
@import "@focus-reactive/payload-plugin-comments/styles.css";
Or import it in a layout/page file:
import "@focus-reactive/payload-plugin-comments/styles.css";
Payload needs its import map regenerated whenever you add or remove a plugin that registers admin components. Run:
npx payload generate:importmap
This command works regardless of your package manager (npm, pnpm, yarn, bun). Alternatively use the equivalent for your package manager:
pnpm payload generate:importmap
# yarn payload generate:importmap
# bunx payload generate:importmap
If you skip this step the plugin's admin components (comment badges, drawer, header button) will not appear in the Payload admin UI.
The plugin adds a comments collection to your database. If you use a SQL adapter (PostgreSQL, SQLite), create and apply a migration:
npx payload migrate:create create_comments
npx payload migrate
Skip this step if you use the MongoDB adapter — Payload creates collections automatically.
The commentsPlugin factory accepts an optional CommentsPluginConfig object:
commentsPlugin(config?: CommentsPluginConfig)
CommentsPluginConfig| Option | Type | Default | Description |
|---|---|---|---|
collections | CollectionEntry[] | — | UI metadata for collections (title field). All collections get comments regardless. |
enabled | boolean | true | Set to false to disable the plugin entirely |
tenant | TenantPluginConfig | — | Multi-tenancy settings (see below) |
overrides | CollectionOverrides | — | Customize the generated comments collection |
translations | Translations | — | Override UI strings per locale (see below) |
usernameFieldPath | string | "name" | Dot-notation path to the display name field on the users collection |
CollectionEntryEach entry in collections is an object that provides UI metadata for a specific collection:
interface CollectionEntry {
slug: string;
titleField?: string; // Field used as document title in the UI. Default: "id"
}
Note: You do not need to list every collection. All collections (and all globals) automatically support comments. The
collectionsarray only sets display metadata like the title field.
Examples:
commentsPlugin({
collections: [
{ slug: "pages", titleField: "title" },
{ slug: "products", titleField: "name" },
],
});
usernameFieldPathSpecifies which field on the users collection is used as the display name in comment UI and @mention autocomplete. Supports dot-notation for nested fields.
commentsPlugin({
usernameFieldPath: "name", // default
// usernameFieldPath: "firstName",
// usernameFieldPath: "profile.displayName",
});
TenantPluginConfigConfigure multi-tenancy when using @payloadcms/plugin-multi-tenant:
| Option | Type | Default | Description |
|---|---|---|---|
enabled | boolean | false | Enable tenant scoping |
collectionSlug | string | "tenants" | Slug of the tenants collection |
documentTenantField | string | "tenant" | Field on document collections that holds the tenant reference |
Example:
commentsPlugin({
tenant: {
enabled: true,
collectionSlug: "tenants",
documentTenantField: "tenant",
},
});
Use overrides to customize the generated comments collection — for example, to extend access control or add custom fields:
commentsPlugin({
overrides: {
access: {
// Override the default "authenticated only" access
},
fields: (defaultFields) => [...defaultFields],
hooks: {
afterChange: [
async ({ doc }) => {
console.log("Comment changed:", doc.id);
},
],
},
},
});
Use translations to override any UI string for one or more locales. Each key is a locale code; the value is a partial object of the CommentsTranslations shape — keys you omit fall back to the built-in English defaults.
commentsPlugin({
translations: {
fr: {
label: "Commentaires",
add: "Ajouter un commentaire",
writeComment: "Écrire un commentaire",
comment: "Commenter",
cancel: "Annuler",
resolve: "Résoudre",
reopen: "Rouvrir",
delete: "Supprimer",
filterOpen: "Ouverts",
filterResolved: "Résolus",
filterMentioned: "Me mentionnent",
},
},
});
All translatable keys (with their English defaults):
| Key | Default (English) |
|---|---|
label | "Comments" |
openComments_one | "{{count}} open comment" |
openComments_other | "{{count}} open comments" |
add | "Add comment" |
writeComment | "Write a comment" |
comment | "Comment" |
cancel | "Cancel" |
posting | "Posting…" |
resolve | "Resolve" |
reopen | "Reopen" |
delete | "Delete" |
general | "General" |
close | "Close" |
syncingComments | "Syncing comments" |
openCommentsAria | "Open comments" |
failedToPost | "Failed to post comment" |
failedToUpdate | "Failed to update comment" |
failedToDelete | "Failed to delete comment" |
failedToAdd | "Failed to add comment" |
unknownAuthor | "Unknown" |
deletedUser | "Deleted user" |
noOpenComments | "No open comments" |
noResolvedComments | "No resolved comments" |
noMentionedComments | "No comments mentioning you" |
filterOpen | "Open" |
filterResolved | "Resolved" |
filterMentioned | "Mentioned me" |
noMentionMatches | "No matches" |
The CommentsTranslations type is exported from the package so you can type your translation objects:
import type { CommentsTranslations } from "@focus-reactive/payload-plugin-comments";
const myTranslations: Record<string, Partial<CommentsTranslations>> = {
fr: { label: "Commentaires" },
de: { label: "Kommentare" },
};
commentsPlugin({ translations: myTranslations });
| Variable | Description | Example |
|---|---|---|
RESEND_API_KEY | Your Resend API key | re_xxxxxxxxxxxxxxxx |
RESEND_FROM_EMAIL | Sender email address for mention notifications | [email protected] |
If RESEND_FROM_EMAIL is not set, mention email notifications are silently skipped and an error is logged to the console.
.env.local example:
RESEND_API_KEY=re_xxxxxxxxxxxxxxxx
[email protected]
Plugin initialization (plugin.ts):
CommentsPluginConfig and returns a standard Payload Plugin function.comments collection (hidden from the admin sidebar by default).FieldCommentLabel into each field's admin label and registers an afterDelete hook that cascade-deletes comments when a document is removed.FieldCommentLabel into each field's admin label.CommentsProviderWrapper) and one admin action (CommentsHeaderButton)./api/comments-plugin/* (list, create, delete, resolve, mark-read, unread-mentions-count, mentionable-users, document-titles, field-labels). Each one requires an authenticated user and uses the Payload instance from the request, so it works on a freshly started serverless instance.Data loading (src/api):
State management (CommentsProvider):
allComments in React state with optimistic updates via useOptimistic.visibleComments is derived: filtered to the current document/collection/global/locale based on the Next.js pathname.addComment, removeComment, resolveComment, and syncComments mutations.Field-level comments (FieldCommentLabel):
Label component for every named field in every configured collection and global.Comments collection schema:
| Field | Type | Description |
|---|---|---|
documentId | number | ID of the document being commented on (null for globals) |
collectionSlug | text | Slug of the collection (null for global comments) |
globalSlug | text | Slug of the Payload global (null for collection document comments) |
fieldPath | text | Dot-notation path of the field (null = document-level) |
locale | text | Locale of the comment (null = shown in all locales) |
text | textarea | Comment body (may contain @(userId) mention tokens) |
mentions | array → relationship | Users mentioned in this comment |
author | relationship → users | Comment author (set automatically) |
isResolved | checkbox | Whether the comment is resolved |
resolvedBy | relationship → users | Who resolved it |
resolvedAt | date | When it was resolved |
tenant | relationship (optional) | Tenant scope (when multi-tenancy is enabled) |
| Import path | Exports |
|---|---|
@focus-reactive/payload-plugin-comments | commentsPlugin, CommentsPluginConfig (type), CommentsTranslations (type) |
@focus-reactive/payload-plugin-comments/styles.css | Plugin stylesheet (Tailwind-compiled CSS) |
Run these from the project root with pnpm:
| Command | Description |
|---|---|
pnpm build | Build the plugin to dist/ (tsup + Tailwind CSS minification) |
pnpm dev | Build in watch mode — rebuilds on file changes |
pnpm lint | Lint + check formatting (Ultracite: oxlint + oxfmt) |
pnpm lint:fix | Lint + fix, including formatting (Ultracite) |
pnpm format | Format files (Ultracite: oxfmt) |
pnpm format:check | Check formatting without writing |
pnpm installpnpm devsrc/.pnpm lint and pnpm format:check before submitting.main.MIT © Focus Reactive
Topics
payload plugin ab
A/B testing for Payload pages with admin-managed variants and edge traffic routing in Next.js middleware
600/wk29v3
payload plugin html preview
Live preview and click-to-edit for Payload on any frontend that renders HTML, including static sites and template…
600/wk29v3
payload plugin visual editing
Click-to-edit overlays on your frontend that deep-link back to the matching field in the Payload admin
580/wk29v3
payload plugin scheduling
Schedule future publish dates for drafts on selected collections and globals, triggered by a cron-pinged endpoint
550/wk29v3
payload plugin admin feedback
Floating feedback widget with screenshot annotation for the Payload admin and selected Next.js frontend routes
487/wk5v3
payload plugin translator
Translate localized Payload content with an AI provider from document, collection or field controls in the admin
550/wk29v3