@focus-reactive/payload-plugin-comments
v1.4.0
Published
[](https://www.npmjs.com/package/@focus-reactive/payload-plugin-comments)
Downloads
837
Readme
@focus-reactive/payload-plugin-comments
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.
Table of Contents
- AI Integration Prompt
- UI Screenshots
- Features
- Prerequisites
- Installation
- Setup
- Configuration
- Translations
- Environment Variables
- Architecture Overview
- Exports Reference
- Available Scripts
- Contributing
- License
AI Integration Prompt
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.UI Screenshots
Global Comments Panel
A header button in the Payload admin opens a drawer listing all comments across every document and collection.
Document Comments Panel
When viewing a document, a side panel shows all comments scoped to that document with filter tabs (Open / Resolved / Mentioned me).
Field Comment Popup
Clicking the comment badge on a field label opens a popup where you can write and post a new comment for that specific field.
Field Label Button — Two States
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) |
| ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| |
|
| |
Features
- Document-level comments — Leave comments on any document in any collection or global
- Field-level comments — Comment directly on individual fields; the field label shows a badge with the comment count
- Global support — Field-level and document-level comments work on Payload Globals, not just collections
- @mention users — Mention other users in comments using
@nameautocomplete - Email notifications — Mentioned users receive email notifications via Resend
- Resolve comments — Mark comments as resolved/unresolved; filter by open, resolved, or mentioned
- Global comments panel — A header button opens a drawer showing all comments across all documents and globals
- Optimistic UI — Comments appear instantly before the server confirms
- Multi-tenancy — Scope comments to tenants via
@payloadcms/plugin-multi-tenant - Locale-aware — Field-level comments are tied to a locale; document-level comments are shown in all locales
- Auto-cleanup — Comments are automatically deleted when their parent document is deleted
- i18n / Translations — All UI strings are translatable; ship your own locale overrides alongside the built-in English defaults
Prerequisites
- Node.js 20 or higher
- A working Payload CMS v3 project with Next.js
- pnpm (recommended)
- A Resend account (required only for mention email notifications)
Installation
pnpm add @focus-reactive/payload-plugin-commentsnpm install @focus-reactive/payload-plugin-comments
# or
yarn add @focus-reactive/payload-plugin-commentsPeer 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.
Setup
1. Add the plugin to your Payload config
// 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
});2. Import the styles
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";3. Regenerate the import map
Payload needs its import map regenerated whenever you add or remove a plugin that registers admin components. Run:
npx payload generate:importmapThis 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:importmapIf you skip this step the plugin's admin components (comment badges, drawer, header button) will not appear in the Payload admin UI.
4. Create and run a migration
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 migrateSkip this step if you use the MongoDB adapter — Payload creates collections automatically.
Configuration
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 |
CollectionEntry
Each 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" },
],
});usernameFieldPath
Specifies 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",
});TenantPluginConfig
Configure 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",
},
});Collection Overrides
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);
},
],
},
},
});Translations
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 });Environment Variables
Required for email notifications
| 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]Architecture Overview
How It Works
Plugin initialization (plugin.ts):
- The plugin receives a
CommentsPluginConfigand returns a standard PayloadPluginfunction. - It creates a
commentscollection (hidden from the admin sidebar by default). - It patches every collection to inject
FieldCommentLabelinto each field's admin label and registers anafterDeletehook that cascade-deletes comments when a document is removed. - It patches every global to inject
FieldCommentLabelinto each field's admin label. - It registers two admin providers (
CommentsProviderWrapper,GlobalCommentsLoader) and one admin action (CommentsHeaderButton).
Data loading (GlobalCommentsLoader):
- This server component runs on every admin page load.
- It fetches all comments, document titles, mentionable users, field labels, collection labels, and global labels in parallel.
- Results are passed to
GlobalCommentsHydrator(a client component) which hydrates theCommentsContext.
State management (CommentsProvider):
- Holds
allCommentsin React state with optimistic updates viauseOptimistic. visibleCommentsis derived: filtered to the current document/collection/global/locale based on the Next.jspathname.- Exposes
addComment,removeComment,resolveComment, andsyncCommentsmutations.
Field-level comments (FieldCommentLabel):
- The plugin overrides the
Labelcomponent for every named field in every configured collection and global. - The label reads comments from context and filters by field path, showing a badge with the count.
- Clicking the badge opens the comments drawer pre-scrolled to that field's comment group.
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) |
Exports Reference
| Import path | Exports |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| @focus-reactive/payload-plugin-comments | commentsPlugin, CommentsPluginConfig (type), CommentsTranslations (type), setPayloadConfig |
| @focus-reactive/payload-plugin-comments/styles.css | Plugin stylesheet (Tailwind-compiled CSS) |
Available Scripts
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 | Run ESLint on src/ |
| pnpm lint:fix | Run ESLint with auto-fix |
| pnpm format | Format src/ with Prettier |
| pnpm format:check | Check formatting without writing |
Contributing
- Fork the repository and create a feature branch.
- Install dependencies:
pnpm install - Start the build watcher:
pnpm dev - Make your changes in
src/. - Run
pnpm lintandpnpm format:checkbefore submitting. - Open a pull request against
main.
License
MIT © Focus Reactive
