Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • World
  • Users
  • Groups
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (Darkly)
  • No Skin
Collapse
Brand Logo
  1. Home
  2. NodeBB Plugins
  3. [nodebb-plugin-internalnotes] internal notes and topic assignment

[nodebb-plugin-internalnotes] internal notes and topic assignment

Scheduled Pinned Locked Moved NodeBB Plugins
11 Posts 4 Posters 0 Views
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • BrutalBirdieB BrutalBirdie

    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


    Lint and publish to npm Draft release on tag

    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

    View Demo

    Installation

    cd /path/to/nodebb
    npm install nodebb-plugin-internalnotes
    

    Then 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

    1. Navigate to any topic as a user who has access (admin, or global/category moderator if enabled in settings).
    2. 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

    DownPWD This user is from outside of this forum
    DownPWD This user is from outside of this forum
    DownPW
    wrote on last edited by
    #2

    Cool plugin 🙂
    test it in dev infra ASAP

    BrutalBirdieB 1 Reply Last reply
    0
    • DownPWD DownPW

      Cool plugin 🙂
      test it in dev infra ASAP

      BrutalBirdieB This user is from outside of this forum
      BrutalBirdieB This user is from outside of this forum
      BrutalBirdie
      wrote on last edited by
      #3

      @DownPW lookging forward to get feedback 🙂

      1 Reply Last reply
      0
      • BrutalBirdieB BrutalBirdie

        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


        Lint and publish to npm Draft release on tag

        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

        View Demo

        Installation

        cd /path/to/nodebb
        npm install nodebb-plugin-internalnotes
        

        Then 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

        1. Navigate to any topic as a user who has access (admin, or global/category moderator if enabled in settings).
        2. 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

        BrutalBirdieB This user is from outside of this forum
        BrutalBirdieB This user is from outside of this forum
        BrutalBirdie
        wrote last edited by
        #4

        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.
        1 Reply Last reply
        0
        • BrutalBirdieB BrutalBirdie

          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


          Lint and publish to npm Draft release on tag

          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

          View Demo

          Installation

          cd /path/to/nodebb
          npm install nodebb-plugin-internalnotes
          

          Then 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

          1. Navigate to any topic as a user who has access (admin, or global/category moderator if enabled in settings).
          2. 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

          crazycellsC This user is from outside of this forum
          crazycellsC This user is from outside of this forum
          crazycells
          wrote last edited by
          #5

          @brutalbirdie what do you use to vibe code? claude code?

          BrutalBirdieB 1 Reply Last reply
          0
          • crazycellsC crazycells

            @brutalbirdie what do you use to vibe code? claude code?

            BrutalBirdieB This user is from outside of this forum
            BrutalBirdieB This user is from outside of this forum
            BrutalBirdie
            wrote last edited by
            #6

            @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 like nodebb-plugin-question-and-answer, nodebb-theme-harmony and 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="😅" />

            crazycellsC 1 Reply Last reply
            0
            • BrutalBirdieB BrutalBirdie

              @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 like nodebb-plugin-question-and-answer, nodebb-theme-harmony and 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="😅" />

              crazycellsC This user is from outside of this forum
              crazycellsC This user is from outside of this forum
              crazycells
              wrote last edited by
              #7

              > @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 like nodebb-plugin-question-and-answer, nodebb-theme-harmony and 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="👍" />

              BrutalBirdieB 1 Reply Last reply
              0
              • crazycellsC crazycells

                > @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 like nodebb-plugin-question-and-answer, nodebb-theme-harmony and 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="👍" />

                BrutalBirdieB This user is from outside of this forum
                BrutalBirdieB This user is from outside of this forum
                BrutalBirdie
                wrote last edited by
                #8

                > @crazycells said:
                >
                > just the opposite... very good

                I 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.

                julianJ 1 Reply Last reply
                0
                • BrutalBirdieB BrutalBirdie

                  > @crazycells said:
                  >
                  > just the opposite... very good

                  I 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.

                  julianJ This user is from outside of this forum
                  julianJ This user is from outside of this forum
                  julian
                  wrote last edited by
                  #9

                  @BrutalBirdie can you ask Claude where our documentation is lacking?

                  I am afraid of the answer 😝

                  1 Reply Last reply
                  0
                  • BrutalBirdieB This user is from outside of this forum
                    BrutalBirdieB This user is from outside of this forum
                    BrutalBirdie
                    wrote last edited by
                    #10

                    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.json keys, 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.routes and routeHelpers.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 the router I receive already scoped to my plugin, or is it the global API router? The client has to call api.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 to static:api.routes may 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 / setupAdminPageRoute signatures: argument order, what middleware array is expected, and what helpers (i.e. formatApiResponse) is for.
                    • State clearly how the client should call these routes (api.get/post/put/del from the api client module) and how that maps to the server path.
                    • Mention the route-ordering gotcha: /:tid/assign and /assignable-users must be registered before /:tid or 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
                    database getObject(s), setObject, sortedSetAdd, getSortedSetRange, getSortedSetRevRange, getSortedSetRevRangeWithScores, sortedSetCard, incrObjectField, deleteObjectFields, deleteAll undocumented
                    user getUsersFields, getUserFields, getSettings, isAdministrator, isGlobalModerator, isModeratorOfAnyCategory, exists undocumented
                    groups search, getMembers, getUserGroups, getGroupFields, exists, isPrivilegeGroup undocumented
                    topics getTopicsByTids, getTopicFields, getFollowers, follow, calculateTopicIndices undocumented
                    privileges topics.filterTids, categories.getModeratorUids undocumented
                    notifications create, push undocumented
                    controllers/helpers formatApiResponse, buildBreadcrumbs undocumented
                    pagination create undocumented
                    translator translate (callback-style) undocumented

                    The database-structure doc describes the on-disk schema, not the db API 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, plus data.uid. (We needed data.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 setting data.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. mutate data.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 import syntax — so the guidance reads as contradictory. We flagged this uncertainty in our own audit and shipped without being sure whether our async/await + template literals in forum scripts would be transpiled or break on older setups.

                    What's missing:

                    • A clear statement: forum scripts use [X module format and Y JS level]; ACP acpScripts/modules use ES modules.
                    • Whether the build step transpiles, and what the minimum supported browser/JS target is.
                    • The modules mapping in plugin.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 the settings client module

                    The 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 of settings.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 settings module (import { save, load } from 'settings') — its API, and how the ACP form field name attributes map to the keys returned by meta.settings.get server-side — is not documented end to end. The "magic" binding between HTML data-*/name attributes and the saved settings object deserves an explicit page.

                    What's missing: a complete ACP settings round-trip example: the .tpl form markup → save/load client wiring → meta.settings.get server read, including the checkbox-is-a-string caveat.


                    6. Widgets: the widget render payload shape is unspecified

                    The 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 &amp;&amp; widget.templateData.template &amp;&amp; widget.templateData.template.name) ||
                        (widget.area &amp;&amp; widget.area.template) || '';
                    const path = (widget.req &amp;&amp; widget.req.path) ? widget.req.path
                        : (widget.area &amp;&amp; widget.area.url) || '';
                    

                    That fallback chain exists purely because we didn't know which of widget.req, widget.area, or widget.templateData is 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 /assigned page that reuses the core recent template:

                    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 nid must be unique and is your dedupe key (we encode IDs into it: internalnotes:note:${tid}:${noteId}).
                    • That bodyShort accepts a translation key with positional args: &lsqb;&lsqb;internalnotes:notif-assigned-group, ${title}, ${group}&rsqb;&rsqb;.
                    • That create can return falsy and you must null-check before push.

                    A single documented example covering these five points would fully cover the feature.


                    9. i18n: the translator.translate callback vs. namespaced-key-with-args details

                    The i18n doc explains the languages/ layout and &lsqb;&lsqb;namespace:key&rsqb;&rsqb; syntax, but two practical details bit us:

                    • Server/client translator.translate('&lsqb;&lsqb;ns:key&rsqb;&rsqb;', cb) is callback-style, so we had to new Promise((resolve) =&gt; translator.translate(key, resolve)) to use it with async/await. Whether a promise-returning form exists isn't documented.
                    • Positional arguments in keys (&lsqb;&lsqb;ns:key, arg1, arg2&rsqb;&rsqb;) and how they map to %1/%2 in 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. Either filter: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.compatibility controls listing, but there's no "what changed for plugin authors between v3 and v4" migration page. We had to assert ^4.0.0 and 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 restart after 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

                    1. A real "Plugin REST API routes" page — static:api.routes, setupApiRoute, the exact /api/v3/plugins/... path, client api.* calls, and the route-ordering footgun. (biggest win)
                    2. 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)
                    3. Payload shapes + rejection conventions on every hook — especially the throw-vs-data.error inconsistency across the three username hooks.
                    4. Client module-system clarity — AMD forum scripts vs. ES-module ACP, transpilation, JS target.
                    5. ACP settings round-trip with the 'on'/'off' string caveat.
                    6. 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.

                    julianJ 1 Reply Last reply
                    0
                    • BrutalBirdieB BrutalBirdie

                      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.json keys, 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.routes and routeHelpers.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 the router I receive already scoped to my plugin, or is it the global API router? The client has to call api.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 to static:api.routes may 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 / setupAdminPageRoute signatures: argument order, what middleware array is expected, and what helpers (i.e. formatApiResponse) is for.
                      • State clearly how the client should call these routes (api.get/post/put/del from the api client module) and how that maps to the server path.
                      • Mention the route-ordering gotcha: /:tid/assign and /assignable-users must be registered before /:tid or 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
                      database getObject(s), setObject, sortedSetAdd, getSortedSetRange, getSortedSetRevRange, getSortedSetRevRangeWithScores, sortedSetCard, incrObjectField, deleteObjectFields, deleteAll undocumented
                      user getUsersFields, getUserFields, getSettings, isAdministrator, isGlobalModerator, isModeratorOfAnyCategory, exists undocumented
                      groups search, getMembers, getUserGroups, getGroupFields, exists, isPrivilegeGroup undocumented
                      topics getTopicsByTids, getTopicFields, getFollowers, follow, calculateTopicIndices undocumented
                      privileges topics.filterTids, categories.getModeratorUids undocumented
                      notifications create, push undocumented
                      controllers/helpers formatApiResponse, buildBreadcrumbs undocumented
                      pagination create undocumented
                      translator translate (callback-style) undocumented

                      The database-structure doc describes the on-disk schema, not the db API 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, plus data.uid. (We needed data.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 setting data.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. mutate data.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 import syntax — so the guidance reads as contradictory. We flagged this uncertainty in our own audit and shipped without being sure whether our async/await + template literals in forum scripts would be transpiled or break on older setups.

                      What's missing:

                      • A clear statement: forum scripts use [X module format and Y JS level]; ACP acpScripts/modules use ES modules.
                      • Whether the build step transpiles, and what the minimum supported browser/JS target is.
                      • The modules mapping in plugin.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 the settings client module

                      The 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 of settings.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 settings module (import { save, load } from 'settings') — its API, and how the ACP form field name attributes map to the keys returned by meta.settings.get server-side — is not documented end to end. The "magic" binding between HTML data-*/name attributes and the saved settings object deserves an explicit page.

                      What's missing: a complete ACP settings round-trip example: the .tpl form markup → save/load client wiring → meta.settings.get server read, including the checkbox-is-a-string caveat.


                      6. Widgets: the widget render payload shape is unspecified

                      The 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 &amp;&amp; widget.templateData.template &amp;&amp; widget.templateData.template.name) ||
                          (widget.area &amp;&amp; widget.area.template) || '';
                      const path = (widget.req &amp;&amp; widget.req.path) ? widget.req.path
                          : (widget.area &amp;&amp; widget.area.url) || '';
                      

                      That fallback chain exists purely because we didn't know which of widget.req, widget.area, or widget.templateData is 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 /assigned page that reuses the core recent template:

                      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 nid must be unique and is your dedupe key (we encode IDs into it: internalnotes:note:${tid}:${noteId}).
                      • That bodyShort accepts a translation key with positional args: &lsqb;&lsqb;internalnotes:notif-assigned-group, ${title}, ${group}&rsqb;&rsqb;.
                      • That create can return falsy and you must null-check before push.

                      A single documented example covering these five points would fully cover the feature.


                      9. i18n: the translator.translate callback vs. namespaced-key-with-args details

                      The i18n doc explains the languages/ layout and &lsqb;&lsqb;namespace:key&rsqb;&rsqb; syntax, but two practical details bit us:

                      • Server/client translator.translate('&lsqb;&lsqb;ns:key&rsqb;&rsqb;', cb) is callback-style, so we had to new Promise((resolve) =&gt; translator.translate(key, resolve)) to use it with async/await. Whether a promise-returning form exists isn't documented.
                      • Positional arguments in keys (&lsqb;&lsqb;ns:key, arg1, arg2&rsqb;&rsqb;) and how they map to %1/%2 in 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. Either filter: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.compatibility controls listing, but there's no "what changed for plugin authors between v3 and v4" migration page. We had to assert ^4.0.0 and 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 restart after 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

                      1. A real "Plugin REST API routes" page — static:api.routes, setupApiRoute, the exact /api/v3/plugins/... path, client api.* calls, and the route-ordering footgun. (biggest win)
                      2. 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)
                      3. Payload shapes + rejection conventions on every hook — especially the throw-vs-data.error inconsistency across the three username hooks.
                      4. Client module-system clarity — AMD forum scripts vs. ES-module ACP, transpilation, JS target.
                      5. ACP settings round-trip with the 'on'/'off' string caveat.
                      6. 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.

                      julianJ This user is from outside of this forum
                      julianJ This user is from outside of this forum
                      julian
                      wrote last edited by
                      #11

                      @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.

                      1 Reply Last reply
                      0

                      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
                      Reply
                      • Reply as topic
                      Log in to reply
                      • Oldest to Newest
                      • Newest to Oldest
                      • Most Votes


                      • Login

                      • Don't have an account? Register

                      • Login or register to search.
                      Powered by NodeBB Contributors
                      • First post
                        Last post
                      0
                      • Categories
                      • Recent
                      • Tags
                      • Popular
                      • World
                      • Users
                      • Groups