[nodebb-plugin-internalnotes] internal notes and topic assignment
-
Disclaimer | 100% transparency
This plugin is vibe coded! So if there are issues that should be resolved, please let me know.
I tried to ensure that the AI followed to the guidelines and made it write a report in NODEBB_STANDARDS_AUDIT.md
nodebb-plugin-internalnotes
A NodeBB plugin that adds internal staff notes and topic assignment to forum topics. By default only administrators can see and manage notes and assignments; you can optionally allow global moderators and/or category moderators in the plugin settings. They are completely invisible to everyone else.
Version: 1.1.0 · NodeBB: 4.x (tested on 4.8.1)
Features
- Internal Notes — Add, view, and delete private notes on any topic. Notes are stored per-topic and include the author and timestamp.
- Topic Assignment (User or Group) — Assign a topic to a specific user or an entire group. All members of an assigned group receive a notification.
- "Assign to myself" — The first option in the assignment modal lets the current user instantly assign the topic to themselves.
- Permission-based visibility — Notes, assignment badges, and the thread tool buttons are completely invisible to regular users. By default only admins can see them; you can enable global moderators and/or category moderators in the plugin settings. No DOM elements are rendered for unprivileged users.
- Right sidebar placement — On topic pages, "Internal Notes" and "Assign Topic" buttons are shown in the far-right sidebar (
component="sidebar/right"). A widget is also available for themes that use a different layout. - Admin settings page — Configure who can access notes: allow global moderators and/or category moderators (ACP > Plugins > Internal Notes & Assignments).
Demo - Version 1.0.2

Installation
cd /path/to/nodebb npm install nodebb-plugin-internalnotesThen activate the plugin from the Admin Control Panel > Extend > Plugins.
Where the buttons appear: On topic pages, the Internal Notes and Assign Topic buttons are automatically placed in the far-right sidebar (the thin vertical bar on the right edge of the page). No widget setup is required. If your theme does not have this component, you can add the Internal Notes & Assign Topic widget to the Global Sidebar in ACP > Appearance > Widgets as a fallback.
Configuration
Navigate to ACP > Plugins > Internal Notes & Assignments to configure:
- Allow global moderators — Enable to let global moderators view and manage internal notes and assignments (default: off; only admins have access).
- Allow category moderators — Enable to let category moderators view and manage internal notes in their categories (default: off).
Usage
- Navigate to any topic as a user who has access (admin, or global/category moderator if enabled in settings).
- In the far-right sidebar (the vertical bar on the right edge of the page), click Internal Notes to open the notes side panel, or Assign Topic to assign the topic.
Notes panel
- View all existing notes for the topic
- Add new notes (supports Ctrl+Enter to submit)
- Delete notes
- See current assignee (user or group) and unassign
Assignment modal
- Assign to myself — one-click self-assignment (first option)
- User tab — search and select any user by username
- Group tab — search and select any group by name
Compatibility
NodeBB v4.x. Tested on NodeBB 4.8.1.
The right-sidebar button placement (injection into
component="sidebar/right") is only tested with the default theme nodebb-theme-harmony v2.1.36. Other themes may need the Internal Notes & Assign Topic widget in ACP > Appearance > Widgets.For developers
- DEVELOPMENT.md — Local setup, linting, and publishing to npm.
- TECHNICAL.md — Database keys and API endpoints.
License
MIT
Cool plugin

test it in dev infra ASAP -
@DownPW lookging forward to get feedback

-
Disclaimer | 100% transparency
This plugin is vibe coded! So if there are issues that should be resolved, please let me know.
I tried to ensure that the AI followed to the guidelines and made it write a report in NODEBB_STANDARDS_AUDIT.md
nodebb-plugin-internalnotes
A NodeBB plugin that adds internal staff notes and topic assignment to forum topics. By default only administrators can see and manage notes and assignments; you can optionally allow global moderators and/or category moderators in the plugin settings. They are completely invisible to everyone else.
Version: 1.1.0 · NodeBB: 4.x (tested on 4.8.1)
Features
- Internal Notes — Add, view, and delete private notes on any topic. Notes are stored per-topic and include the author and timestamp.
- Topic Assignment (User or Group) — Assign a topic to a specific user or an entire group. All members of an assigned group receive a notification.
- "Assign to myself" — The first option in the assignment modal lets the current user instantly assign the topic to themselves.
- Permission-based visibility — Notes, assignment badges, and the thread tool buttons are completely invisible to regular users. By default only admins can see them; you can enable global moderators and/or category moderators in the plugin settings. No DOM elements are rendered for unprivileged users.
- Right sidebar placement — On topic pages, "Internal Notes" and "Assign Topic" buttons are shown in the far-right sidebar (
component="sidebar/right"). A widget is also available for themes that use a different layout. - Admin settings page — Configure who can access notes: allow global moderators and/or category moderators (ACP > Plugins > Internal Notes & Assignments).
Demo - Version 1.0.2

Installation
cd /path/to/nodebb npm install nodebb-plugin-internalnotesThen activate the plugin from the Admin Control Panel > Extend > Plugins.
Where the buttons appear: On topic pages, the Internal Notes and Assign Topic buttons are automatically placed in the far-right sidebar (the thin vertical bar on the right edge of the page). No widget setup is required. If your theme does not have this component, you can add the Internal Notes & Assign Topic widget to the Global Sidebar in ACP > Appearance > Widgets as a fallback.
Configuration
Navigate to ACP > Plugins > Internal Notes & Assignments to configure:
- Allow global moderators — Enable to let global moderators view and manage internal notes and assignments (default: off; only admins have access).
- Allow category moderators — Enable to let category moderators view and manage internal notes in their categories (default: off).
Usage
- Navigate to any topic as a user who has access (admin, or global/category moderator if enabled in settings).
- In the far-right sidebar (the vertical bar on the right edge of the page), click Internal Notes to open the notes side panel, or Assign Topic to assign the topic.
Notes panel
- View all existing notes for the topic
- Add new notes (supports Ctrl+Enter to submit)
- Delete notes
- See current assignee (user or group) and unassign
Assignment modal
- Assign to myself — one-click self-assignment (first option)
- User tab — search and select any user by username
- Group tab — search and select any group by name
Compatibility
NodeBB v4.x. Tested on NodeBB 4.8.1.
The right-sidebar button placement (injection into
component="sidebar/right") is only tested with the default theme nodebb-theme-harmony v2.1.36. Other themes may need the Internal Notes & Assign Topic widget in ACP > Appearance > Widgets.For developers
- DEVELOPMENT.md — Local setup, linting, and publishing to npm.
- TECHNICAL.md — Database keys and API endpoints.
License
MIT
Update https://github.com/BrutalBirdie/nodebb-plugin-internalnotes/releases/tag/1.1.1
Fixed:
- NodeBB v4.12.0 compatibility — broken avatar images in the Assign Topic modal and assignee badge. NodeBB 4.12.0 now HTML-escapes user.picture server-side (turning / into / etc.), and the plugin was escaping it a second time before injecting it into <img src="..." />. The double-encoding broke the URL. The picture value is now used as-is when interpolated into attribute values.
-
Disclaimer | 100% transparency
This plugin is vibe coded! So if there are issues that should be resolved, please let me know.
I tried to ensure that the AI followed to the guidelines and made it write a report in NODEBB_STANDARDS_AUDIT.md
nodebb-plugin-internalnotes
A NodeBB plugin that adds internal staff notes and topic assignment to forum topics. By default only administrators can see and manage notes and assignments; you can optionally allow global moderators and/or category moderators in the plugin settings. They are completely invisible to everyone else.
Version: 1.1.0 · NodeBB: 4.x (tested on 4.8.1)
Features
- Internal Notes — Add, view, and delete private notes on any topic. Notes are stored per-topic and include the author and timestamp.
- Topic Assignment (User or Group) — Assign a topic to a specific user or an entire group. All members of an assigned group receive a notification.
- "Assign to myself" — The first option in the assignment modal lets the current user instantly assign the topic to themselves.
- Permission-based visibility — Notes, assignment badges, and the thread tool buttons are completely invisible to regular users. By default only admins can see them; you can enable global moderators and/or category moderators in the plugin settings. No DOM elements are rendered for unprivileged users.
- Right sidebar placement — On topic pages, "Internal Notes" and "Assign Topic" buttons are shown in the far-right sidebar (
component="sidebar/right"). A widget is also available for themes that use a different layout. - Admin settings page — Configure who can access notes: allow global moderators and/or category moderators (ACP > Plugins > Internal Notes & Assignments).
Demo - Version 1.0.2

Installation
cd /path/to/nodebb npm install nodebb-plugin-internalnotesThen activate the plugin from the Admin Control Panel > Extend > Plugins.
Where the buttons appear: On topic pages, the Internal Notes and Assign Topic buttons are automatically placed in the far-right sidebar (the thin vertical bar on the right edge of the page). No widget setup is required. If your theme does not have this component, you can add the Internal Notes & Assign Topic widget to the Global Sidebar in ACP > Appearance > Widgets as a fallback.
Configuration
Navigate to ACP > Plugins > Internal Notes & Assignments to configure:
- Allow global moderators — Enable to let global moderators view and manage internal notes and assignments (default: off; only admins have access).
- Allow category moderators — Enable to let category moderators view and manage internal notes in their categories (default: off).
Usage
- Navigate to any topic as a user who has access (admin, or global/category moderator if enabled in settings).
- In the far-right sidebar (the vertical bar on the right edge of the page), click Internal Notes to open the notes side panel, or Assign Topic to assign the topic.
Notes panel
- View all existing notes for the topic
- Add new notes (supports Ctrl+Enter to submit)
- Delete notes
- See current assignee (user or group) and unassign
Assignment modal
- Assign to myself — one-click self-assignment (first option)
- User tab — search and select any user by username
- Group tab — search and select any group by name
Compatibility
NodeBB v4.x. Tested on NodeBB 4.8.1.
The right-sidebar button placement (injection into
component="sidebar/right") is only tested with the default theme nodebb-theme-harmony v2.1.36. Other themes may need the Internal Notes & Assign Topic widget in ACP > Appearance > Widgets.For developers
- DEVELOPMENT.md — Local setup, linting, and publishing to npm.
- TECHNICAL.md — Database keys and API endpoints.
License
MIT
@brutalbirdie what do you use to vibe code? claude code?
-
@brutalbirdie what do you use to vibe code? claude code?
@crazycells
Currently, I am using Claude Code.
For vibe coding the NodeBB plugin I have created a pre-flight instruction for Claude to ingest the docs https://docs.nodebb.org/development/ and other, also ingest known good plugins likenodebb-plugin-question-and-answer,nodebb-theme-harmonyand others to have a reference for creating code.Why are you asking, is the code this bad? <img class="not-responsive emoji" src="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f605.png?v=14b798222a5" title="
" /> -
@crazycells
Currently, I am using Claude Code.
For vibe coding the NodeBB plugin I have created a pre-flight instruction for Claude to ingest the docs https://docs.nodebb.org/development/ and other, also ingest known good plugins likenodebb-plugin-question-and-answer,nodebb-theme-harmonyand others to have a reference for creating code.Why are you asking, is the code this bad? <img class="not-responsive emoji" src="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f605.png?v=14b798222a5" title="
" />> @BrutalBirdie said:
>
> @crazycells
> Currently, I am using Claude Code.
> For vibe coding the NodeBB plugin I have created a pre-flight instruction for Claude to ingest the docs https://docs.nodebb.org/development/ and other, also ingest known good plugins likenodebb-plugin-question-and-answer,nodebb-theme-harmonyand others to have a reference for creating code.
>
> Why are you asking, is the code this bad? <img class="not-responsive emoji" src="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f605.png?v=14b798222a5" title="
" />nope, just the opposite... very good
I liked it, thanks for the efforts. <img class="not-responsive emoji" src="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f44d.png?v=14b798222a5" title="
" /> -
> @BrutalBirdie said:
>
> @crazycells
> Currently, I am using Claude Code.
> For vibe coding the NodeBB plugin I have created a pre-flight instruction for Claude to ingest the docs https://docs.nodebb.org/development/ and other, also ingest known good plugins likenodebb-plugin-question-and-answer,nodebb-theme-harmonyand others to have a reference for creating code.
>
> Why are you asking, is the code this bad? <img class="not-responsive emoji" src="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f605.png?v=14b798222a5" title="
" />nope, just the opposite... very good
I liked it, thanks for the efforts. <img class="not-responsive emoji" src="https://community.nodebb.org/assets/plugins/nodebb-plugin-emoji/emoji/android/1f44d.png?v=14b798222a5" title="
" />> @crazycells said:
>
> just the opposite... very goodI tried to ensure that Claude sticks to the guidelines. Thanks for feedback.
That has great value for me, so I know my preflight works correctly. -
> @crazycells said:
>
> just the opposite... very goodI tried to ensure that Claude sticks to the guidelines. Thanks for feedback.
That has great value for me, so I know my preflight works correctly.@BrutalBirdie can you ask Claude where our documentation is lacking?
I am afraid of the answer

-
As you wish @julian I asked Claude for feedback about the docs of NodeBB based on the two plugins I devloped with it.
Here is the output.
Again, 100% AI generated, I take no responsibility for accuracy.
NodeBB Documentation Feedback
Feedback on docs.nodebb.org/development, grounded in building two real NodeBB v4 plugins:
- nodebb-plugin-internalnotes — staff notes + topic assignment (custom API routes, widgets, notifications, custom page, ACP settings, privilege checks, DB access).
- nodebb-plugin-username-denylist — registration/username filtering (multiple
filter:*hooks, ACP settings).
The docs were good enough to get a plugin structured correctly (the
plugin.jsonkeys, the directory layout, the hook list). Almost all of the friction came one layer deeper: once you have a hook firing or a route registered, what is actually in the payload, and which core helper do I call? That is consistently undocumented and had to be reverse-engineered from core source. The notes below are ordered roughly by how much time each cost.
1. Plugin API routes: the base-path / router-scoping story is the single biggest gap
This cost more time than everything else combined.
When you register API routes via
static:api.routesandrouteHelpers.setupApiRoute(router, 'get', '/internalnotes/:tid', ...), it is not documented what the resulting public URL is. Is it/internalnotes/:tid?/api/v3/plugins/internalnotes/:tid? Is therouterI receive already scoped to my plugin, or is it the global API router? The client has to callapi.get(...)with the correct prefix, and there is no authoritative statement of what that prefix is.We ended up writing a full paragraph of hedging in our own audit doc because we genuinely could not tell from the docs:
> "In NodeBB 3, plugin API routes are typically mounted under
/api/v3/plugins/; the router passed tostatic:api.routesmay be scoped per plugin… Ensure the router you receive is the plugin-scoped one…"What's missing:
- A dedicated page: "Adding REST API routes to a plugin." Document
static:api.routes, the shape of{ router, middleware, helpers }, and the exact resulting public path (with the/api/v3/plugins/...prefix spelled out). - Document
routeHelpers.setupApiRoute/setupPageRoute/setupAdminPageRoutesignatures: argument order, what middleware array is expected, and whathelpers(i.e.formatApiResponse) is for. - State clearly how the client should call these routes (
api.get/post/put/delfrom theapiclient module) and how that maps to the server path. - Mention the route-ordering gotcha:
/:tid/assignand/assignable-usersmust be registered before/:tidor Express param-matching swallows them. This is a real footgun for plugin authors building more than one route.
This one topic deserves its own page with a complete, copy-pasteable round-trip example (server route → client call → response).
2. No API reference for the core modules you load via
require.main.require('./src/...')Every non-trivial plugin is built on
require.main.require('./src/database'),'./src/user','./src/groups','./src/topics','./src/privileges','./src/notifications','./src/controllers/helpers','./src/routes/helpers','./src/pagination','./src/translator','./src/meta'. These are the actual surface area of plugin development — and there is essentially no reference documentation for any of them. You discover method names and signatures by reading core source on GitHub.Concrete methods we had to source-dive to find/confirm:
Module Methods we needed Doc status databasegetObject(s),setObject,sortedSetAdd,getSortedSetRange,getSortedSetRevRange,getSortedSetRevRangeWithScores,sortedSetCard,incrObjectField,deleteObjectFields,deleteAllundocumented usergetUsersFields,getUserFields,getSettings,isAdministrator,isGlobalModerator,isModeratorOfAnyCategory,existsundocumented groupssearch,getMembers,getUserGroups,getGroupFields,exists,isPrivilegeGroupundocumented topicsgetTopicsByTids,getTopicFields,getFollowers,follow,calculateTopicIndicesundocumented privilegestopics.filterTids,categories.getModeratorUidsundocumented notificationscreate,pushundocumented controllers/helpersformatApiResponse,buildBreadcrumbsundocumented paginationcreateundocumented translatortranslate(callback-style)undocumented The
database-structuredoc describes the on-disk schema, not thedbAPI a plugin should call — those are different things, and a plugin author needs the latter far more often.What's missing: a generated or hand-written "Core APIs available to plugins" reference, even a thin one. It doesn't need prose for every method — even a typedoc-style dump of signatures for the ~10 modules above would eliminate the single most common reason to leave the docs and read core source. The uncertainty shows up directly in our code as defensive fallbacks like:
const cids = await categories.getAllCidsFromSet ? await categories.getAllCidsFromSet() : await db.getSortedSetRange('categories:cid', 0, -1);We literally guarded against not knowing whether a method exists, because there was no reference to check.
3. Hook payloads are listed by name, but not by shape
The hooks page is a good index of which hooks exist, but for a filter the only thing that matters is what's in the object and what I'm expected to return, and that's almost never specified.
Examples from our plugins where we had to infer the shape by logging:
filter:topic.get→data.topic, plusdata.uid. (We neededdata.uid; that it's present at all is undocumented.)filter:topics.get→data.topics(array),data.uid. (Different shape from the singular hook — easy to get wrong.)action:topics.purge→{ topics }array. Each element shape?filter:register.check→data.userData.username.filter:username.check→data.username, and you signal rejection by settingdata.error, not by throwing.filter:user.create→data.user.username, and here you throw to reject.
That last pair is the sharpest edge: three username hooks, with two different rejection conventions (throw vs. set
data.error) and no documentation telling you which is which. We only got it right by trial and error, and documented it ourselves in the plugin README so the next person doesn't have to. A short "rejection convention" note per hook would have saved hours.What's missing: for each documented hook, a minimal payload example (
{ topic, uid }), what to return, and — critically — how to signal an error/rejection (throw vs. mutatedata.error), because it is not uniform across hooks.
4. The client-side module system split (AMD vs ES modules) is confusing and underdocumented
A v4 plugin has three client entry points with two different module systems, and the docs don't clearly flag this:
scripts(forum client) → AMD/RequireJS style (define,require).acpScripts+modules(ACP) → ES modules (import { save, load } from 'settings').
Nowhere is it clearly stated which directories get which module system, whether/how client code is transpiled, or what the supported JS feature level is. The style guide says "ES5 for minification," but the ACP path clearly uses ES module
importsyntax — so the guidance reads as contradictory. We flagged this uncertainty in our own audit and shipped without being sure whether ourasync/await+ template literals in forum scripts would be transpiled or break on older setups.What's missing:
- A clear statement: forum
scriptsuse [X module format and Y JS level]; ACPacpScripts/modulesuse ES modules. - Whether the build step transpiles, and what the minimum supported browser/JS target is.
- The
modulesmapping inplugin.json("../admin/plugins/foo.js": "./public/lib/admin.js") is subtle and deserves a worked example — the../admin/...virtual path tripped us up.
5. ACP settings: the
'on'/'off'string gotcha and thesettingsclient moduleThe ACP settings flow works but has undocumented sharp edges:
meta.settings.get('myplugin')returns checkbox values as the strings'on'/'off', not booleans. Our code is full ofsettings.allowGlobalMods === 'on'— which we only learned by inspecting stored values. This is a classic footgun that one sentence in the docs would prevent.- The client
settingsmodule (import { save, load } from 'settings') — its API, and how the ACP form fieldnameattributes map to the keys returned bymeta.settings.getserver-side — is not documented end to end. The "magic" binding between HTMLdata-*/nameattributes and the saved settings object deserves an explicit page.
What's missing: a complete ACP settings round-trip example: the
.tplform markup →save/loadclient wiring →meta.settings.getserver read, including the checkbox-is-a-string caveat.
6. Widgets: the
widgetrender payload shape is unspecifiedThe widgets doc covers registration (
filter:widgets.getWidgets) and the render hook well enough, but the render payload is a grab-bag whose contents aren't documented, so we wrote defensive code probing multiple locations for the same fact:const templateName = (widget.templateData && widget.templateData.template && widget.templateData.template.name) || (widget.area && widget.area.template) || ''; const path = (widget.req && widget.req.path) ? widget.req.path : (widget.area && widget.area.url) || '';That fallback chain exists purely because we didn't know which of
widget.req,widget.area, orwidget.templateDatais guaranteed, or how to reliably answer the very common question "what page/template is this widget rendering on?". A documented widget-context object (what's always present:req,area,templateData,data,uid) and a recommended "detect current template/route" pattern would remove all of that guesswork.
7. Reusing core templates and the custom-page recipe
We added a custom
/assignedpage that reuses the corerecenttemplate:res.render('recent', { topics, title, breadcrumbs, pagination });Getting there required reverse-engineering several things with no doc support:
- That you can
res.render()a core template name from a plugin, and which ones are safe/stable to reuse. - The exact data contract a reused template expects (
topics,breadcrumbs,pagination). - The helper chain to build that data:
helpers.buildBreadcrumbs(...),pagination.create(page, pageCount),topics.calculateTopicIndices(topics, start),user.getSettings(uid).topicsPerPage.
What's missing: a "Build a custom plugin page" recipe covering
setupPageRoute, rendering your own template vs. reusing a core one, and the breadcrumb/pagination helpers. This is an extremely common plugin need (any "list view") and currently requires reading core controllers.
8. Notifications: a worked example is missing
notifications.create(...)+notifications.push(notifObj, uids)is a common plugin need and entirely undocumented. Things we had to infer:- The required/optional fields:
type,bodyShort,nid,from,path, plus arbitrary extras (tid). - That
nidmust be unique and is your dedupe key (we encode IDs into it:internalnotes:note:${tid}:${noteId}). - That
bodyShortaccepts a translation key with positional args:[[internalnotes:notif-assigned-group, ${title}, ${group}]]. - That
createcan return falsy and you must null-check beforepush.
A single documented example covering these five points would fully cover the feature.
9. i18n: the
translator.translatecallback vs. namespaced-key-with-args detailsThe i18n doc explains the
languages/layout and[[namespace:key]]syntax, but two practical details bit us:- Server/client
translator.translate('[[ns:key]]', cb)is callback-style, so we had tonew Promise((resolve) => translator.translate(key, resolve))to use it withasync/await. Whether a promise-returning form exists isn't documented. - Positional arguments in keys (
[[ns:key, arg1, arg2]]) and how they map to%1/%2in the translation string is underspecified.
10. Theme coupling: no documented stable extension point for topic-page UI
To put buttons on the topic page we inject into
component="sidebar/right", which only works with nodebb-theme-harmony — we had to ship a widget as a fallback and document the limitation ourselves. There's no documented, theme-agnostic extension point ("add an action to a topic") that works across themes. Eitherfilter:topic.thread_tools(and how harmony renders it) should be documented as the blessed path, or the docs should be honest that cross-theme topic-page UI requires widgets/per-theme work.
11. v3 → v4 migration and the local dev loop
- Versioning: the ACP module-loading pattern changed between v3 and v4 (the
modules+ ES-module approach is v4-only).nbbpm.compatibilitycontrols listing, but there's no "what changed for plugin authors between v3 and v4" migration page. We had to assert^4.0.0and document why (v3 unsupported) ourselves. - Dev loop: the quickstart shows scaffolding but not the day-to-day reality:
npm link/npm install+./nodebb build+./nodebb restartafter nearly every change, that templates and client scripts require a rebuild, and that ACP changes can need a hard refresh. A short "plugin development workflow" section (and any testing guidance — how to write/run plugin tests) would help a lot.
Summary — highest-impact additions
- A real "Plugin REST API routes" page —
static:api.routes,setupApiRoute, the exact/api/v3/plugins/...path, clientapi.*calls, and the route-ordering footgun. (biggest win) - A "Core APIs for plugins" reference — even an auto-generated signature dump for
database,user,groups,topics,privileges,notifications,controllers/helpers,routes/helpers. (second biggest) - Payload shapes + rejection conventions on every hook — especially the throw-vs-
data.errorinconsistency across the three username hooks. - Client module-system clarity — AMD forum scripts vs. ES-module ACP, transpilation, JS target.
- ACP settings round-trip with the
'on'/'off'string caveat. - Recipes: custom plugin page (reusing core templates + breadcrumb/pagination helpers), notifications, and widget-context detection.
The recurring theme: the docs get you to the boundary of core, then stop. Plugin authors live just across that boundary — in the payloads and the
require.main.require('./src/...')modules — and that's exactly where documentation is thinnest. Closing that one gap (items 1–3) would remove the large majority of "go read core source" detours. -
As you wish @julian I asked Claude for feedback about the docs of NodeBB based on the two plugins I devloped with it.
Here is the output.
Again, 100% AI generated, I take no responsibility for accuracy.
NodeBB Documentation Feedback
Feedback on docs.nodebb.org/development, grounded in building two real NodeBB v4 plugins:
- nodebb-plugin-internalnotes — staff notes + topic assignment (custom API routes, widgets, notifications, custom page, ACP settings, privilege checks, DB access).
- nodebb-plugin-username-denylist — registration/username filtering (multiple
filter:*hooks, ACP settings).
The docs were good enough to get a plugin structured correctly (the
plugin.jsonkeys, the directory layout, the hook list). Almost all of the friction came one layer deeper: once you have a hook firing or a route registered, what is actually in the payload, and which core helper do I call? That is consistently undocumented and had to be reverse-engineered from core source. The notes below are ordered roughly by how much time each cost.
1. Plugin API routes: the base-path / router-scoping story is the single biggest gap
This cost more time than everything else combined.
When you register API routes via
static:api.routesandrouteHelpers.setupApiRoute(router, 'get', '/internalnotes/:tid', ...), it is not documented what the resulting public URL is. Is it/internalnotes/:tid?/api/v3/plugins/internalnotes/:tid? Is therouterI receive already scoped to my plugin, or is it the global API router? The client has to callapi.get(...)with the correct prefix, and there is no authoritative statement of what that prefix is.We ended up writing a full paragraph of hedging in our own audit doc because we genuinely could not tell from the docs:
> "In NodeBB 3, plugin API routes are typically mounted under
/api/v3/plugins/; the router passed tostatic:api.routesmay be scoped per plugin… Ensure the router you receive is the plugin-scoped one…"What's missing:
- A dedicated page: "Adding REST API routes to a plugin." Document
static:api.routes, the shape of{ router, middleware, helpers }, and the exact resulting public path (with the/api/v3/plugins/...prefix spelled out). - Document
routeHelpers.setupApiRoute/setupPageRoute/setupAdminPageRoutesignatures: argument order, what middleware array is expected, and whathelpers(i.e.formatApiResponse) is for. - State clearly how the client should call these routes (
api.get/post/put/delfrom theapiclient module) and how that maps to the server path. - Mention the route-ordering gotcha:
/:tid/assignand/assignable-usersmust be registered before/:tidor Express param-matching swallows them. This is a real footgun for plugin authors building more than one route.
This one topic deserves its own page with a complete, copy-pasteable round-trip example (server route → client call → response).
2. No API reference for the core modules you load via
require.main.require('./src/...')Every non-trivial plugin is built on
require.main.require('./src/database'),'./src/user','./src/groups','./src/topics','./src/privileges','./src/notifications','./src/controllers/helpers','./src/routes/helpers','./src/pagination','./src/translator','./src/meta'. These are the actual surface area of plugin development — and there is essentially no reference documentation for any of them. You discover method names and signatures by reading core source on GitHub.Concrete methods we had to source-dive to find/confirm:
Module Methods we needed Doc status databasegetObject(s),setObject,sortedSetAdd,getSortedSetRange,getSortedSetRevRange,getSortedSetRevRangeWithScores,sortedSetCard,incrObjectField,deleteObjectFields,deleteAllundocumented usergetUsersFields,getUserFields,getSettings,isAdministrator,isGlobalModerator,isModeratorOfAnyCategory,existsundocumented groupssearch,getMembers,getUserGroups,getGroupFields,exists,isPrivilegeGroupundocumented topicsgetTopicsByTids,getTopicFields,getFollowers,follow,calculateTopicIndicesundocumented privilegestopics.filterTids,categories.getModeratorUidsundocumented notificationscreate,pushundocumented controllers/helpersformatApiResponse,buildBreadcrumbsundocumented paginationcreateundocumented translatortranslate(callback-style)undocumented The
database-structuredoc describes the on-disk schema, not thedbAPI a plugin should call — those are different things, and a plugin author needs the latter far more often.What's missing: a generated or hand-written "Core APIs available to plugins" reference, even a thin one. It doesn't need prose for every method — even a typedoc-style dump of signatures for the ~10 modules above would eliminate the single most common reason to leave the docs and read core source. The uncertainty shows up directly in our code as defensive fallbacks like:
const cids = await categories.getAllCidsFromSet ? await categories.getAllCidsFromSet() : await db.getSortedSetRange('categories:cid', 0, -1);We literally guarded against not knowing whether a method exists, because there was no reference to check.
3. Hook payloads are listed by name, but not by shape
The hooks page is a good index of which hooks exist, but for a filter the only thing that matters is what's in the object and what I'm expected to return, and that's almost never specified.
Examples from our plugins where we had to infer the shape by logging:
filter:topic.get→data.topic, plusdata.uid. (We neededdata.uid; that it's present at all is undocumented.)filter:topics.get→data.topics(array),data.uid. (Different shape from the singular hook — easy to get wrong.)action:topics.purge→{ topics }array. Each element shape?filter:register.check→data.userData.username.filter:username.check→data.username, and you signal rejection by settingdata.error, not by throwing.filter:user.create→data.user.username, and here you throw to reject.
That last pair is the sharpest edge: three username hooks, with two different rejection conventions (throw vs. set
data.error) and no documentation telling you which is which. We only got it right by trial and error, and documented it ourselves in the plugin README so the next person doesn't have to. A short "rejection convention" note per hook would have saved hours.What's missing: for each documented hook, a minimal payload example (
{ topic, uid }), what to return, and — critically — how to signal an error/rejection (throw vs. mutatedata.error), because it is not uniform across hooks.
4. The client-side module system split (AMD vs ES modules) is confusing and underdocumented
A v4 plugin has three client entry points with two different module systems, and the docs don't clearly flag this:
scripts(forum client) → AMD/RequireJS style (define,require).acpScripts+modules(ACP) → ES modules (import { save, load } from 'settings').
Nowhere is it clearly stated which directories get which module system, whether/how client code is transpiled, or what the supported JS feature level is. The style guide says "ES5 for minification," but the ACP path clearly uses ES module
importsyntax — so the guidance reads as contradictory. We flagged this uncertainty in our own audit and shipped without being sure whether ourasync/await+ template literals in forum scripts would be transpiled or break on older setups.What's missing:
- A clear statement: forum
scriptsuse [X module format and Y JS level]; ACPacpScripts/modulesuse ES modules. - Whether the build step transpiles, and what the minimum supported browser/JS target is.
- The
modulesmapping inplugin.json("../admin/plugins/foo.js": "./public/lib/admin.js") is subtle and deserves a worked example — the../admin/...virtual path tripped us up.
5. ACP settings: the
'on'/'off'string gotcha and thesettingsclient moduleThe ACP settings flow works but has undocumented sharp edges:
meta.settings.get('myplugin')returns checkbox values as the strings'on'/'off', not booleans. Our code is full ofsettings.allowGlobalMods === 'on'— which we only learned by inspecting stored values. This is a classic footgun that one sentence in the docs would prevent.- The client
settingsmodule (import { save, load } from 'settings') — its API, and how the ACP form fieldnameattributes map to the keys returned bymeta.settings.getserver-side — is not documented end to end. The "magic" binding between HTMLdata-*/nameattributes and the saved settings object deserves an explicit page.
What's missing: a complete ACP settings round-trip example: the
.tplform markup →save/loadclient wiring →meta.settings.getserver read, including the checkbox-is-a-string caveat.
6. Widgets: the
widgetrender payload shape is unspecifiedThe widgets doc covers registration (
filter:widgets.getWidgets) and the render hook well enough, but the render payload is a grab-bag whose contents aren't documented, so we wrote defensive code probing multiple locations for the same fact:const templateName = (widget.templateData && widget.templateData.template && widget.templateData.template.name) || (widget.area && widget.area.template) || ''; const path = (widget.req && widget.req.path) ? widget.req.path : (widget.area && widget.area.url) || '';That fallback chain exists purely because we didn't know which of
widget.req,widget.area, orwidget.templateDatais guaranteed, or how to reliably answer the very common question "what page/template is this widget rendering on?". A documented widget-context object (what's always present:req,area,templateData,data,uid) and a recommended "detect current template/route" pattern would remove all of that guesswork.
7. Reusing core templates and the custom-page recipe
We added a custom
/assignedpage that reuses the corerecenttemplate:res.render('recent', { topics, title, breadcrumbs, pagination });Getting there required reverse-engineering several things with no doc support:
- That you can
res.render()a core template name from a plugin, and which ones are safe/stable to reuse. - The exact data contract a reused template expects (
topics,breadcrumbs,pagination). - The helper chain to build that data:
helpers.buildBreadcrumbs(...),pagination.create(page, pageCount),topics.calculateTopicIndices(topics, start),user.getSettings(uid).topicsPerPage.
What's missing: a "Build a custom plugin page" recipe covering
setupPageRoute, rendering your own template vs. reusing a core one, and the breadcrumb/pagination helpers. This is an extremely common plugin need (any "list view") and currently requires reading core controllers.
8. Notifications: a worked example is missing
notifications.create(...)+notifications.push(notifObj, uids)is a common plugin need and entirely undocumented. Things we had to infer:- The required/optional fields:
type,bodyShort,nid,from,path, plus arbitrary extras (tid). - That
nidmust be unique and is your dedupe key (we encode IDs into it:internalnotes:note:${tid}:${noteId}). - That
bodyShortaccepts a translation key with positional args:[[internalnotes:notif-assigned-group, ${title}, ${group}]]. - That
createcan return falsy and you must null-check beforepush.
A single documented example covering these five points would fully cover the feature.
9. i18n: the
translator.translatecallback vs. namespaced-key-with-args detailsThe i18n doc explains the
languages/layout and[[namespace:key]]syntax, but two practical details bit us:- Server/client
translator.translate('[[ns:key]]', cb)is callback-style, so we had tonew Promise((resolve) => translator.translate(key, resolve))to use it withasync/await. Whether a promise-returning form exists isn't documented. - Positional arguments in keys (
[[ns:key, arg1, arg2]]) and how they map to%1/%2in the translation string is underspecified.
10. Theme coupling: no documented stable extension point for topic-page UI
To put buttons on the topic page we inject into
component="sidebar/right", which only works with nodebb-theme-harmony — we had to ship a widget as a fallback and document the limitation ourselves. There's no documented, theme-agnostic extension point ("add an action to a topic") that works across themes. Eitherfilter:topic.thread_tools(and how harmony renders it) should be documented as the blessed path, or the docs should be honest that cross-theme topic-page UI requires widgets/per-theme work.
11. v3 → v4 migration and the local dev loop
- Versioning: the ACP module-loading pattern changed between v3 and v4 (the
modules+ ES-module approach is v4-only).nbbpm.compatibilitycontrols listing, but there's no "what changed for plugin authors between v3 and v4" migration page. We had to assert^4.0.0and document why (v3 unsupported) ourselves. - Dev loop: the quickstart shows scaffolding but not the day-to-day reality:
npm link/npm install+./nodebb build+./nodebb restartafter nearly every change, that templates and client scripts require a rebuild, and that ACP changes can need a hard refresh. A short "plugin development workflow" section (and any testing guidance — how to write/run plugin tests) would help a lot.
Summary — highest-impact additions
- A real "Plugin REST API routes" page —
static:api.routes,setupApiRoute, the exact/api/v3/plugins/...path, clientapi.*calls, and the route-ordering footgun. (biggest win) - A "Core APIs for plugins" reference — even an auto-generated signature dump for
database,user,groups,topics,privileges,notifications,controllers/helpers,routes/helpers. (second biggest) - Payload shapes + rejection conventions on every hook — especially the throw-vs-
data.errorinconsistency across the three username hooks. - Client module-system clarity — AMD forum scripts vs. ES-module ACP, transpilation, JS target.
- ACP settings round-trip with the
'on'/'off'string caveat. - Recipes: custom plugin page (reusing core templates + breadcrumb/pagination helpers), notifications, and widget-context detection.
The recurring theme: the docs get you to the boundary of core, then stop. Plugin authors live just across that boundary — in the payloads and the
require.main.require('./src/...')modules — and that's exactly where documentation is thinnest. Closing that one gap (items 1–3) would remove the large majority of "go read core source" detours.@BrutalBirdie appreciate that. Most of these gotchas and footguns I've actually run into personally as well.
Much of it is just known to @baris and I, and we don't realize what is missing from those docs that should be explicitly spelled out.
Hello! It looks like you're interested in this conversation, but you don't have an account yet.
Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.
With your input, this post could be even better 💗
Register Login