nualt

payload plugin comments

by nualt
Publishing workflow
v3
^3.90.0
MIT

Overview

Summary generated from the README

Adds inline comment threads to Payload's Lexical editor. Editors select a passage, comment in an admin sidebar, reply, resolve, reopen or delete threads. It plugs in through a Lexical feature plus a plugin that stores messages in a hidden collection. Requires Payload 3.90 or later and React 19, and works with SQLite, PostgreSQL and MongoDB.

Features

  • Select text in a Lexical editor and start a comment thread
  • Sidebar panel to reply, resolve, reopen and delete threads
  • Hidden comments collection with author-based update and delete permissions
  • Optional canModerate callback to let others moderate comments
  • Cascade deletion of replies with a permission pre-check
  • English and French UI that follows the admin language, with overridable strings

Use cases

  • Collect editorial feedback on specific passages of a rich text field
  • Review drafts with a team by resolving and reopening comment threads

From the author

Inline comment threads on Payload rich text (Lexical), in English and French.

Installation

npm i @nualt/payload-plugin-comments

0

Stars

0

Forks

0

Open Issues

Today

Last Updated

0

Weekly Downloads

v0.1.0

npm Version

205.4 KB

Unpacked Size

0

Dependencies

GitHub stars

Trends appear after 7 days of data. Daily snapshots have not started yet.

npm weekly downloads

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

61/100Good

Composite score based on GitHub activity, stars, npm downloads, publish recency, dependency count, and package size.

Badge for your README

Payload Directory health badge
Markdown
[![Payload Directory](https://payloaddirectory.dev/badge/1428525268-root.svg)](https://payloaddirectory.dev/plugins/1428525268-root)
HTML
<a href="https://payloaddirectory.dev/plugins/1428525268-root"><img src="https://payloaddirectory.dev/badge/1428525268-root.svg" alt="Payload Directory"></a>
Updated Oct 22, 2026Created Oct 21, 2026Published Oct 22, 2026

README

@nualt/payload-plugin-comments

npm version CI

Inline comment threads for Payload 3.90 and Lexical. Select a passage, comment in the admin sidebar, reply, resolve, reopen or delete a thread. Includes English and French; the panel follows the admin language. The plugin does not change your public frontend.

Tested with SQLite, PostgreSQL and MongoDB (replica set).

A highlighted passage in the Lexical editor and its comment thread in the admin sidebar

Compatibility

Package / runtimeSupported versions
This plugin0.x targets Payload 3
Payload, its Lexical editor, UI and translations^3.90.0
React and React DOM^19
Node.js>=20.18.1
pnpm (development and packaging)^9 || ^10 || ^11; CI uses pnpm 11 and Node 22
Databases testedSQLite, PostgreSQL 16, MongoDB 7 replica set

Payload 4 is not supported yet. Releases follow Semantic Versioning; each plugin major version documents its Payload compatibility. See the CHANGELOG for release details.

Installation

pnpm add @nualt/payload-plugin-comments

The host must provide compatible versions of payload, @payloadcms/richtext-lexical, @payloadcms/ui (^3.90.0), react and react-dom (^19). Translation types also require @payloadcms/translations (^3.90.0).

import { commentsFeature, commentsPlugin } from '@nualt/payload-plugin-comments'
import { lexicalEditor, InlineToolbarFeature } from '@payloadcms/richtext-lexical'
import { en } from '@payloadcms/translations/languages/en'
import { fr } from '@payloadcms/translations/languages/fr'
import { buildConfig } from 'payload'

export default buildConfig({
  // Keep your database, secret, auth collection and other existing configuration.
  i18n: { supportedLanguages: { en, fr }, fallbackLanguage: 'en' },
  collections: [{
    slug: 'posts',
    fields: [
      { name: 'title', type: 'text', required: true },
      {
        name: 'content',
        type: 'richText',
        editor: lexicalEditor({
          features: ({ defaultFeatures }) => [
            ...defaultFeatures,
            InlineToolbarFeature(),
            commentsFeature(),
          ],
        }),
      },
    ],
  }],
  plugins: [commentsPlugin({
    collections: ['posts'],
    usersCollection: 'users',
    slug: 'comments',
  })],
})

Regenerate the host import map and types after installing. commentsFeature() must be added to each rich text editor you want to annotate. The plugin adds only the sidebar UI field to the listed collections. Enable an inline or fixed Lexical toolbar so that the comment button is available.

Save a new document before commenting. Saving a comment stores its message separately; save the document to persist its text anchors. Draft messages are kept in browser session storage under payload-plugin-comments:drafts:.

Options and permissions

OptionDefaultPurpose
collectionsRequiredCollectionSlug[] receiving the sidebar.
usersCollectionusersAuth collection referenced by author.
slugcommentsHidden collection storing messages.
canModerateNobody({ req }) => boolean | Promise<boolean> grants updates/deletes on other authors' messages.
disabledfalseHides the sidebar and editor commenting UI; retains the comments collection, stored fields and MarkNode support.

By default, any authenticated user can read comments. Only users authenticated through the configured usersCollection can create comments. Only the author can update/delete their messages, including resolving their own threads. The author is always forced to the authenticated user on create and cannot be changed on update. Deleting a root deletes all its replies only if the requesting user can delete each one; otherwise, the entire operation is refused before deleting any message. canModerate is evaluated on the server; the UI gets its result from the collection's /moderation endpoint. No role or user name field is required; display names fall back to email and then “User” / “Utilisateur”.

Permission refusals leave the entire thread intact on all three adapters. With the default transactions on PostgreSQL and MongoDB (replica set), integration tests also verify that an unexpected failure after two reply deletions rolls back the whole cascade, preserving all messages and parent links. MongoDB requires a replica set for these transactions. Without database transactions, an unexpected failure during a cascade (other than a permission refusal caught by the pre-check) can still leave a partial deletion. SQLite transactions remain disabled in the example and default test suite; enable them with sqliteAdapter({ client: { url: '...' }, transactionOptions: {} }) when rollback is required.

These permissions are independent of the host document's access rules: this plugin does not provide tenant or per-document isolation. If your application requires those, adapt collection access before exposing comments to its users. Local API calls follow Payload's usual overrideAccess behavior; use overrideAccess: false and user when enforcing user permissions. The cascade pre-check always enforces the requesting user's delete permissions on replies, including when the root is deleted with overrideAccess: true. Creation also requires a user from usersCollection for author attribution. Ownership checks compare both the auth collection and ID; an explicit canModerate callback may grant moderation to other authenticated identities.

documentCollection and documentId are indexed text fields. The parent relation points to the configured comments slug. Changing slugs after storing comments requires your own data migration. The plugin supports string and numeric document/user IDs. The admin uses serverURL and routes.api, including a customized API route.

Translations

All plugin UI strings use the plugin-comments namespace. Host overrides take precedence:

i18n: {
  supportedLanguages: { en, fr },
  translations: {
    en: { 'plugin-comments': { comments: 'Discussion' } },
    fr: { 'plugin-comments': { comments: 'Discussion' } },
  },
}

CommentsTranslations and CommentsTranslationKeys are exported for typed custom components. Collection and field labels provide English/French objects. Relative dates use the current admin language.

Public rich text rendering

Keep commentsFeature() registered even when commenting is disabled so stored mark nodes remain readable. Public rendering should unwrap each mark and render only its children. For Payload's React renderer:

import type { JSXConvertersFunction } from '@payloadcms/richtext-lexical/react'
import { RichText } from '@payloadcms/richtext-lexical/react'

const converters: JSXConvertersFunction = ({ defaultConverters }) => ({
  ...defaultConverters,
  mark: ({ node, nodesToJSX }) => nodesToJSX({ nodes: node.children }),
})

// <RichText data={document.content} converters={converters} />

Do not fetch comment messages on the public site. Other renderers need the same transparent treatment of mark nodes. This plugin supplies no frontend component or migration from existing comment collections.

Local development

pnpm generate:importmap
pnpm generate:types
pnpm dev
pnpm build
pnpm lint
pnpm test:int
pnpm test:int:mongodb
pnpm test:e2e

The example at http://localhost:3000/admin defaults to SQLite (dev/dev.db) and seeds [email protected] / test. Use these credentials only for local development. Set DATABASE_ADAPTER=sqlite|postgres|mongodb to choose an adapter; PostgreSQL and MongoDB outside tests require DATABASE_URI. Both admin languages are enabled.

pnpm test:int always uses in-memory SQLite with no database daemon. pnpm test:int:mongodb starts and stops a single-member MongoMemoryReplSet, using an isolated database and a binary cache under node_modules/.cache/; the first run downloads MongoDB. PostgreSQL tests require a fresh disposable database. For example:

docker run -d --rm --name ppc-pg -e POSTGRES_PASSWORD=postgres -p 127.0.0.1:55432:5432 postgres:16-alpine
DATABASE_URI=postgres://postgres:[email protected]:55432/postgres pnpm test:int:postgres
docker stop ppc-pg

The suite exercises ownership, moderation, replies, cascading deletion, disabled configuration and Lexical mark serialization. The mid-cascade rollback test runs on PostgreSQL and MongoDB and is skipped on the default SQLite configuration, which has no transactions.

Source exports are used during development; publishing uses dist/. Build before packing or publishing. No publication is performed by these scripts.

Inherited limits

The panel loads up to 200 messages per document. Reopening a resolved thread restores its highlight at the first matching text node, using the stored quote (up to 120 characters). Repeated passages or formatting split across nodes may need manual correction. These behaviors are retained from the original feature; pagination and more precise anchors are outside this port.

License

MIT.

Similar plugins