NodeBB Development
Stay tuned here to hear more about new releases and features of NodeBB!
This is a forum category containing topical discussion. You can start new discussions by mentioning this category.
We just released version 4.1.0 of NodeBB. Here are the latest features you can now take advantage of!
As an aside, for the first time in a long while, both @baris and I are working on the same codebase again. Up until version 4 was released, I'd been working on the activitypub
branch and periodically merging in the latest changes from develop
. It's nice to be home! :house_with_garden:
Improved federation of Group actors :left_speech_bubble:
We've improved the activity synchronization for followed group actors. Local updates/likes/etc. are now federated outward by the category in addition to those from remote users. Thanks to @rimu@piefed.social and @freamon who worked with me on debugging this one.
Mentions and Emoji now federating out in source.content :wave:
Emoji and mentions have been sent out to followers since v4, but that wasn't reflected in the raw markdown content that we also send along. That has been remediated now, and this change improves nodebb-to-nodebb federation.
Video
object type now parseable :tv:
NodeBB is now able to ingest Peertube Video
objects, and render then in a topic just like other pieces of content. Thanks @deadsuperhero@forum.wedistribute.org for prodding me to get this sorted out!
Today's the day! :tada:
After nearly a full year in development, NodeBB v4.0.0 has landed, bringing federation between NodeBB instances (and a connection to the wider fediverse of social media) to forum software.
Fedi-what?
Fediverse! Here's a TechCrunch primer about it, but at the end of the day, it doesn't really matter. All you need to know is that NodeBB plugs in to a wider social network so that you don't have to cultivate an audience, they're already there.
The genesis
It was back in mid-2023 when I had the initial idea of interconnecting NodeBB forums. Back then, I had far smaller ambitions... I wanted a singular NodeBB to be able to communicate with other forums running NodeBB. To do that, we'd need to build out a centralized service to act as a bridge between instances, and corresponding slim clients on individual installs to consume the relayed data. At the time, concept like decentralization were not even part of my thought process.
It was during this period when I was doing my research that I stumbled on Mastodon, and later, ActivityPub, the protocol that powers it all. Since then, it's been one wild ride getting NodeBB to speak the same language.
Funding
Soon after dipping my toes into all that Mastodon had to offer, I discovered the NLNet Foundation, and their corresponding fund — NGI Zero Core. With the promise of funding, NodeBB could fully commit to implementing the protocol in short order, instead of piece by piece over time. We sent in an application and were delighted to be approved for the August 2023 call.
Their funding was instrumental in providing the financial stability to experiment with ActivityPub and to participate in developer circles, such as the SWICG, FediForum, and much more.
The fund continues to operate, perhaps you could benefit, or donate to the cause. It has certainly made a difference to NodeBB.
Federate, or not, it's your choice
NodeBB v4 comes shipped with the capability to interact with other NodeBB forums and any other ActivityPub-speaking software, right out of the box. We opted to make this a core feature instead of a plugin, since there were many changes made to core to support even the concept of accepting content from outside itself.
To that end, any users upgrading from v3.x will automatically have federation disabled, in order to reduce surprise. Any new forums will federate automatically.
You can turn federation on and off (and adjust some other fun toggles) directly from ACP > Settings > Federation (ActivityPub)
.
Even after turning federation on, how you use it shapes how well connected you will be. There is no centralized authority artificially boosting your content, so the name of the game is establishing two-way follow relationships to other sites.
The ActivityPub Equalizer
We're not alone in this journey to interoperate with other decentralized services. We're not even the only forum software to attempt to do so.
- Discourse has a working plugin.
- Ghost is building out in the open.
I specifically highlight these two because they both started in the early 2010s, same as NodeBB. It's always been a bit of an informal competition between us, and we always checked in on what the others were doing (growth-wise, pricing-wise, etc.) Truth be told, I don't think the ghost team ever really noticed NodeBB, but I digress...
The funny thing about ActivityPub is that at the end of the day, the overarching goal of seamless communication breaks down any barriers between competing organizations.
NodeBB and Discourse have been vying for the exact same market share (forums, community-building, self-started or enterprise) for over 10 years, and it was only after ActivityPub came around that the dev teams even started talking to one another.
Funny how that works.
So how does it all work?
Our documentation portal has been updated with the latest information about the ActivityPub functionality in v4.
If you have any questions about how it works or how to configure some aspect of it, please don't hesitate to reach out in the corresponding v4 support thread.
If you run NodeBB, the quickest way to see this in action is to upgrade to v4, and then paste this post's URL into your search bar. It should show up automatically, and you should be able to read and reply to it, directly from your own forum. Neat!
Our relationship with push notifications has been rather circuitous...
- First Andrew (@psychobunny) tried plain desktop notifications via the Notification API (that's the desktop notifications plugin), but notification delivery stopped when you closed your browser.
- Then I wrote the PushBullet plugin, but they went from free to paid and that went out the window
- Around that time I wrote the Firebase plugin, which was a dead end because there was no iOS support.
- I then wrote the ntfy plugin, which works really well, but does rely on a freemium third-party service and app (written by @binwiederhier@discuss.ntfy.sh)
Thanks to some recent discussion from @crazycells and @bh4-tech, I learned that the Push API had reached general availability across most modern browsers. In fact, this actually happened over a year ago, so compatibility should be even better.
So I set about working on yet another push notifications plugin, hopefully for the last time, and this time using the native Push API on the client-side.
To enable them, simply navigate over to the "Push Notifications" menu item in your user profile, and flip the switch.
So, let's dogfood! I de-activated the ntfy plugin on this site and activated the web-push plugin so you can try it out right now.
The plugin is in the proof-of-concept phase, so there might (read: most definitely will be) issues. Please let me know any you find here.

This will be a post about the various caches in NodeBB and how they work.
What is caching?
There are 4 different caches, I will go over each one and explain how they work and how they help make nodebb faster. But before that let's remember what a cache is and how they help:
>In computing, a cache is a hardware or software component that stores data so that future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation or a copy of data stored elsewhere. A cache hit occurs when the requested data can be found in a cache, while a cache miss occurs when it cannot. Cache hits are served by reading data from the cache, which is faster than recomputing a result or reading from a slower data store; thus, the more requests that can be served from the cache, the faster the system performs.
>To be cost-effective and to enable efficient use of data, caches must be relatively small. Nevertheless, caches have proven themselves in many areas of computing, because typical computer applications access data with a high degree of locality of reference. Such access patterns exhibit temporal locality, where data is requested that has been recently requested already, and spatial locality, where data is requested that is stored physically close to data that has already been requested.
Source: https://en.wikipedia.org/wiki/Cache_(computing)
Cache Module used in NodeBB
All the caches in nodebb use the nodejs module https://www.npmjs.com/package/lru-cache, A cache object that deletes the least-recently-used items.
It let's you cache things and drop things out of the cache if they are not used frequently.
Let's go chronologically and see what caches there are and why they were added.
Post Cache
(added in 2015)
Out of all the caches this one was the most obvious. When a user types a post the content of that post is stored as is in the database. In NodeBB's case this is markdown but other formats can be used as well. Storing the text as the user typed it makes it easy to implement things like editing, since we can just display the same content in the composer. But when we need to display the post as part of the webpage we need to convert it into html. This process happens in :
Posts.parsePost = async function (postData) {
if (!postData) {
return postData;
}
postData.content = String(postData.content || '');
const cache = require('./cache');
const pid = String(postData.pid);
const cachedContent = cache.get(pid);
if (postData.pid && cachedContent !== undefined) {
postData.content = cachedContent;
return postData;
}
const data = await plugins.hooks.fire('filter:parse.post', { postData: postData });
data.postData.content = translator.escape(data.postData.content);
if (data.postData.pid) {
cache.set(pid, data.postData.content);
}
return data.postData;
};
To turn the post content into html the hook filter:parse.post
is used by nodebb-plugin-markdown
https://github.com/NodeBB/nodebb-plugin-markdown/blob/master/index.js#L152-L158. This is done entirely in the nodebb process and blocks the cpu since it is not an async IO operation. When displaying a topic we do this for 20 posts(1 page) and if 20 users are loading the same topic we are doing 400 parses to generate the same output. If the posts are longer the process takes more time as well.
So it was a no-brainer to cache the parsed content since it rarely changes and we can just display the cached html content. As you can see from the first piece of code if the post content is cached filter:parse.post
isn't fired and we don't spend time in the plugins.
This cache makes topic pages faster and reduces the cpu usage of the nodebb process.
Group Cache
(added in 2016)
Next up is the group cache, this cache is used in the groups module and caches all group membership checks.
Groups.isMember = async function (uid, groupName) {
if (!uid || parseInt(uid, 10) <= 0 || !groupName) {
return false;
}
const cacheKey = `${uid}:${groupName}`;
let isMember = Groups.cache.get(cacheKey);
if (isMember !== undefined) {
return isMember;
}
isMember = await db.isSortedSetMember(`group:${groupName}:members`, uid);
Groups.cache.set(cacheKey, isMember);
return isMember;
};
When you load any page in NodeBB there is almost always a privilege check to see if you can see certain content or perform certain actions. Can this user see all categories? Can this user post a reply to this topic? All of these questions are answered by checking if the user is part of a specific group.
After the initial setup of the forum group membership rarely changes so it was a perfect candidate for caching.
Unlike the post cache which lowers the cpu usage of the nodebb process, this cache reduces the calls made to the database. Instead of making one or more database calls on every navigation we just make them once and store the results.
Object Cache
(added in 2017)
The reason why this cache is named Object cache
is because it caches the results of db.getObject(s)
calls.
module.getObjectsFields = async function (keys, fields) {
if (!Array.isArray(keys) || !keys.length) {
return [];
}
const cachedData = {};
const unCachedKeys = cache.getUnCachedKeys(keys, cachedData);
let data = [];
if (unCachedKeys.length >= 1) {
data = await module.client.collection('objects').find(
{ _key: unCachedKeys.length === 1 ? unCachedKeys[0] : { $in: unCachedKeys } },
{ projection: { _id: 0 } }
).toArray();
data = data.map(helpers.deserializeData);
}
const map = helpers.toMap(data);
unCachedKeys.forEach((key) => {
cachedData[key] = map[key] || null;
cache.set(key, cachedData[key]);
});
if (!Array.isArray(fields) || !fields.length) {
return keys.map(key => (cachedData[key] ? { ...cachedData[key] } : null));
}
return keys.map((key) => {
const item = cachedData[key] || {};
const result = {};
fields.forEach((field) => {
result[field] = item[field] !== undefined ? item[field] : null;
});
return result;
});
};
Every time we load a page we make calls to the database to load the post/topic/user/category objects. These all use the call db.getObjects
. Since these objects rarely change and are requested frequently it makes sense to cache these as well.
This lowers the load on the database significantly. Once a topic has been accessed most of the data required will be in the cache for anyone else loading the same topic.
Local Cache
(added in 2018)
The last cache is a bit different than the others as it was added to cache different types of things and can be used from plugins as well. I will give two examples of how this is used in nodebb to make pages faster.
The first one is the list of categories. Whenever someone loads the /categories page we load a list of category ids. Since the list of categories rarely changes we cache it in the local cache.
Categories.getAllCidsFromSet = async function (key) {
let cids = cache.get(key);
if (cids) {
return cids.slice();
}
cids = await db.getSortedSetRange(key, 0, -1);
cids = cids.map(cid => parseInt(cid, 10));
cache.set(key, cids);
return cids.slice();
};
Another use of this cache is for plugin settings. Whenever a plugin loads settings with await meta.settings.get('mypluginid');
the result is loaded and cached.
Settings.get = async function (hash) {
const cached = cache.get(`settings:${hash}`);
if (cached) {
return _.cloneDeep(cached);
}
const [data, sortedLists] = await Promise.all([
db.getObject(`settings:${hash}`),
db.getSetMembers(`settings:${hash}:sorted-lists`),
]);
const values = data || {};
await Promise.all(sortedLists.map(async (list) => {
const members = await db.getSortedSetRange(`settings:${hash}:sorted-list:${list}`, 0, -1);
const keys = members.map(order => `settings:${hash}:sorted-list:${list}:${order}`);
values[list] = [];
const objects = await db.getObjects(keys);
objects.forEach((obj) => {
values[list].push(obj);
});
}));
const result = await plugins.hooks.fire('filter:settings.get', { plugin: hash, values: values });
cache.set(`settings:${hash}`, result.values);
return _.cloneDeep(result.values);
};
Out of all the caches used this one will have the highest hit rate as you can see from the screenshot. Because the things stored in this cache pretty much never change during regular operation, once the list of categories is loaded it will always be served from the cache unless you add or reorder categories.
ACP Page
(/admin/advanced/cache)
On the ACP cache page you can enable/disable the cache, see the hit rate for each cache and even download the contents of the cache as json.
You can also empty the cache here, this is sometimes useful if you have to make changes to the database directly via CLI, for example if you make a change in the database to a topic title and if that topic is cached you won't see the changes until you clear the object cache
.
Hope this answers any questions about the caches in NodeBB! :runner:

I'm looking to use nodebb to run debates and will eventually require integration with our customer database - mysql. This would also allow us to develop integration with our favoured cms. I know the word "mysql" is anathema round here but is this possible? It would surely open nodebb up to a much larger market.
We've had some really great contributions to our language files since we started supporting translations in NodeBB core, although keeping up with the changes has been quite hard. Specifically:
- There was never really any way to check if any changes were out of date, except trying out every page of NodeBB in a new language and looking for incorrect language tags
- When a language tag was missing, the system ended up showing the unattractive "code" behind the tag, instead of a sensible fallback like English
To that end, we've registered NodeBB as a project on Transifex! Using a web site like this will allow us to see at a glance how complete each language is, and allow translators to quickly see which strings need translating, and which do not.
(clap)