September 30, 2022

Migration Guide for v3

A person placing a pushpin on a board featuring printouts of website screens

v3 Migration Guide

In advance of the release of version 3, we are releasing this guide in order to give third-party developers a chance to bring their plugins and themes up-to-date. In the successive sections below, we will outline the breaking change or new best-practice, and the steps to migrate, along with a live example.

While the update to version 2 was primarily limited to the server-side, there are a significant number of client-side breaking changes (most notably the switch from LESS to SCSS.)

Looking for the v2 migration guide? Click here.

This blog post is the fourth in a series of posts related to the release of NodeBB v3

Please see our other articles related to v3:

Update Less files to SCSS (🚨 breaking change)

Bootstrap 4 swiched from Less to Sass for their source files. As we are updating to Bootstrap 5, we will follow suit and change the underlying CSS preprocessor used in NodeBB to Sass (or SCSS).

Existing plugins and themes with LESS files will not be compiled to CSS. The scss and acpScss properties will be checked instead of less and acpLess.

Additionally, a new required file overrides.scss has been added. It must be present, even if empty.

Live example (nodebb-plugin-markdown)

The markdown theme exposes a Less file for precompilation. The file extension is renamed to .scss, and the plugin.json property less is changed to scss.


git mv public/less public/scss

Note: Your less directory may be named or located differently.


touch public/scss/overrides.scss
git add public/scss/overrides.scss

Note: Your less directory may be named or located differently.


- "less": ["public/less/default.less"],
+ "scss": ["public/scss/default.scss"],

While many features in Less (e.g. nesting of styles, etc.) are also found in Sass, there are some differences. The most prominent ones being:

  • Mixins are now explicitly defined with the @mixin prefix. If a mixin is used in style declarations, they are explicitly referred to via the @include prefix. (source documentation)
  • Variables are now referenced using the $ character instead of @
    • e.g. @brand-primary becomes $brand-primary
  • Many variables available in Bootstrap 3 are no longer available in Bootstrap 5, or were more likely renamed
    • $brand-primary doesn’t actually exist in Bootstrap 5. It was renamed $primary

Update to FontAwesome 6

We’ve updated the icon library from Fontawesome 5 to FontAwesome 6, which comes with its own subtle icon style changes.

It should mean that any new functionality for FontAwesome 6 is available for use in templates, including the new FontAwesome styles (thin, solid, duotone, etc.) and new icon names.

Big thanks to contributor @oplik0 for doing the legwork to upgrade us to FontAwesome 6!

Relax 😎

No changes here.

Update nbbpm.compatibility in third-party themes and plugins (best practice)

In order to ensure that plugins and themes updated for NodeBB v3.x are not accidentally installed in v2.x installations, you should update the nbbpm.compatibility string in your package.json to at least ^3.0.0.

If your plugin does not contain any SCSS, or if the styles are backwards compatible, you can simply append || ^3.0.0 to the compatibility string.


  "nbbpm": {
-    "compatibility": "^1.17.4 || ^2.0.0"
+    "compatibility": "^3.0.0"

Rewrite templates to use Bootstrap 5 classes (🚨 breaking change)

The most major changes to Bootstrap came from the jump from Bootstrap 3 to Bootstrap 4. Bootstrap 5 has comparatively fewer changes, although there are still enough to warrant their own migration guides.

Migration Guides

While the above are complete migration guides from one framework to another, there are a couple of patterns that are used heavily in NodeBB, and many parts of Bootstrap that NodeBB doesn’t use at all.

For convenience, we’ve compiled a list of template changes that we’ve run into most often when upgrading themes and plugins to support Bootstrap 5 → Check it out here.

Replace any broken widget containers (🚨 breaking change)

Related to the above change, the “panel” and “well” containers have been updated.


If you have any widgets using those two container types, you will need to enter the widget configuration page (ACP > Extend > Widgets), and reset the container to the new type by dragging and dropping the container into the affected widget.

  • Instead of the “panel” container, use the “card” container
  • The “Well” container still exists, but you will need to overwrite the old container HTML with the new one.

Renamed methods in lru-cache dependency (🚨 breaking change)

NodeBB exposes a least-recently-used cache in src/cache.js for use in core and by plugins. That library uses the underlying dependency lru-cache, which was upgraded to version 7.
Some methods have been renamed, and some properties have been deprecated/removed.


  • .del() is no longer available, it is now .delete()
  • .reset() is no longer available, it is now .clear()


The following options will now emit warnings.

  • stale is now allowStale
  • maxAge is now ttl
  • length is now sizeCalculation

Changes to panel offset calculation (🚨 breaking change)

The panel-offset CSS variable was calculated from the main NodeBB navbar’s vertical position, but also included its bottom margin.

This behaviour has now changed to not include the bottom margin, as well as to be calculated more simply, so as to be easier for themes to override if needed.

Live Example

If your theme uses var(--panel-offset), you may have to adjust its value. In practice, you should probably just only need to use var(--panel-offset) with no additional offsets at all, now.

This Persona theme commit contains fixes to adapt to this change.

Changes to alert.tpl (🚨 breaking change)

The alert.tpl template has been moved to the partials/ directory.

This change probably will not apply if your theme does not extend or override the alert template.

Live Example

The persona theme contained alert.tpl, and it was moved to the new location.

Changes to partials/users_list_menu.tpl (🚨 breaking change)

The partials/users_list_menu.tpl template has been updated to specify a new component attribute for the list element. This change probably will not apply if your theme does not extend or override the alert template.


The persona theme contains the partial. If your custom theme also contains partials/users_list_menu.tpl, update it to contain the new component attribute.

Changes to registerComplete.tpl (🚨 breaking change)

The registerComplete.tpl template has been updated so that the CSRF token is no longer passed in via query string, but via hidden input element instead. This change probably will not apply if your theme does not extend or override this template.


The persona theme contains the template. If your custom theme also contains registerComplete.tpl, update it to contain the new component attribute. As of v3, this template is now provided by NodeBB core, and not by the theme. It could be that you don’t need this template customized at all.

Removal of deprecated user export routes and methods (🚨 breaking change)

The following routes have been removed:
  • /api/user/{userslug}/export/posts
  • /api/user/{userslug}/export/uploads
  • /api/user/{userslug}/export/profile
  • /api/user/uid/{userslug}/export/{type}
    • (where type is one of posts uploads or profile)
The following methods have been removed:
  • user.exportProfile
  • user.exportPosts
  • user.exportUploads
  • user.emailConfirm


If you make calls to any of those routes listed on the left, you will want to update the endpoint URL. Note that the response bodies have not changed, but merely their endpoints. Email confirmation via user.emailConfirm has been removed in favour of redirecting the user to the email change flow (/me/edit/email).

Passport v0.6 changes (🚨 breaking change)

An underlying dependency called passport was updated to v0.6.x, and includes some breaking changes, the most important of which is that sessions are now automatically re-rolled on authentication.

This was duplicate functionality, as NodeBB had already implemented its own session re-rolling mechanism, but in the interest of keeping things simple, we removed our implementation in favour of the code now built-into passport@0.6


  • If your plugin calls req.login, req.logout, or passport.authenticate, any saved session info will be removed. Pass in keepSessionInfo: true to retain that information.
  • You probably also don’t need to call req.login directly. It is likely more thorough/complete for you to require src/controllers/authentication and call await nbbAuthController.doLogin(req, uid); instead.

For more context on what these breaking changes were inherited from, please see the changelog for v0.6 of passport

Live Example

The two-factor authentication plugin calls passport.authenticate in order to validate the user response against the TOTP challenge. We did not want their session re-rolled here.

Changes to avatar generation (🚨 breaking change)

Avatars are generated via the buildAvatar helper, which consumes a user object and returns either the user’s uploaded avatar in an img tag, or a “user icon” via the span tag.

The .avatar class ensured that irrespective of whether an image was shown or a user icon, that they were both rendered identically.

As of v3, the buildAvatar helper will now render both the picture and the user icon, and hide the second. This will allow for the user icon to function as a fallback if the picture does not load (due to the picture being unreachable, or due to CORS misconfigurations, etc.)

Furthermore, the following class names have been removed:

  • avatar-xs
  • avatar-sm
  • avatar-sm2x
  • avatar-md
  • avatar-lg
  • avatar-xl

Instead, when using the buildAvatar helper, pass in the desired avatar size in quotes (see right.)

Live Example

The Persona theme used the buildAvatar helper to render avatars in its templates. We updated the call to that helper to pass in the desired sizes (e.g. "32px") instead of the old sizes (e.g. “sm”, “lg”, etc.)

You can theoretically use any unit you like (pixels, ems, rems, viewport widths/heights), although we’ve only thoroughly tested with pixel units.


-   {buildAvatar(user, "sm", true)}
+   {buildAvatar(user, "24px", true)} 

Removed client-side methods (🚨 breaking change)

The following deprecated methods were removed from the app object on the client side:

  • app.enableTopicSearch()
  • app.handleSearch()
  • app.prepareSearch()


Require the search module and use the following methods instead:
  • search.enableQuickSearch(options);
  • search.init(options);
  • search.showAndFocusInput();


-   app.enableTopicSearch(options);
+   require(['search'], (search) => {
+       search.enableQuickSearch(options);
+   });

Removal of common templates from themes (🚨 breaking change)

The following templates have been moved from themes to core, so they can be more easily maintained.


If your theme contains these templates, and they have not been modified, you can remove them from your theme.

If they have been modified, move them to the new locations relative to the templates directory:

cd your-plugin/templates
git mv partials/change_picture_modal.tpl modals/change-picture.tpl
Old Template New Template
(core) src/views/admin/partials/temporary-ban.tpl
(core) src/views/admin/partials/temporary-mute.tpl

Removal of some stylesheets to core

In conjunction with the above (removal of common templates from themes), we also moved the following stylesheet from the Persona theme to core.

  • bottom-sheet.scss (formerly bottom-sheet.less)

This stylesheet is now in effect for all themes, although bottom-sheet.scss is opt-in — that is — the style is only applied to dropdowns if the parent container has the bottom-sheet class. Iin reality, there should be no unexpected style changes.

Relax 😎

No changes here.

mobile formatting option no longer allowed for composer toolbar items (🚨 breaking change)

We had a leftover deprecation that was meant to be removed in v1.16.0, which was finally removed for v3.0.

When defining a composer toolbar item, mobile was an accepted option. This has now been removed in favour of using the more fine-grained visibility property.


Instead of setting mobile to a boolean value, set the visibility property as follows:

  "visibility": {
    mobile: true,
    desktop: false,

The values are not exclusive, they can both be true or false (although if you set them both to false, the item just won’t show up at all.)

Changes to chat client-side API/hooks (🚨 breaking change)

  • The .close() method now takes a composer uuid instead of the jQuery element of the composer
  • The action:chat.sent hook now fires only after the message has actually been sent, instead of immediately before
    • Additionally, it will now only fire if the chat message was successfully sent.


 require(['chat'], (chat) => {
-    const chatModal = $('.chat-modal[data-uuid="' + uuid + '"]');
-    chat.close(chatModal);
+    chat.close(uuid);

colorpicker module removed from admin panel (🚨 breaking change)

The module was no longer used used (as of late 2021), and has been removed in order to reduce the javascript payload.


Plugin developers are encouraged to use the native HTML5 color picker instead.

Material Design Lite vendor library removed (🚨 breaking change)

This library has been removed to reduce the admin CSS payload size, and to reduce maintenance burden. The only affected elements are either checkboxes or the custom floating save button.

For checkboxes, use the checkbox styles provided by Bootstrap5 instead.

For the floating save button, import the new partial served by core: admin/partials/save_button.tpl

Live Example

The mentions plugin has been updated to utilise the new save button. The save button code is replaced with the import statement.


<button id="save" class="floating-button mdl-button mdl-js-button mdl-button--fab mdl-js-ripple-effect mdl-button--colored">
    <i class="material-icons">save</i>
<!-- IMPORT admin/partials/save_button.tpl -->


<div class="checkbox">
	<label for="myInput" class="mdl-switch mdl-js-switch mdl-js-ripple-effect">
		<input id="myInput" class="mdl-switch__input" type="checkbox" />
		<span class="mdl-switch__label">Label</span>
<div class="form-check">
	<label for="myInput" class="form-check-label">Label</label>
	<input id="myInput" class="form-check-input" type="checkbox" />

Middleware Changes (🚨 breaking change)

The following middlewares have been removed:

  • middleware.renderHeader
  • middleware.renderAdminHeader

Relax 😎

No changes here. These were public methods used internally, that are now internal-only.

CSRF protection library change (🚨 breaking change)

The CSRF protection library has been changed to csrf-sync. The method used to retrieve a new csrf token has changed.


If you call req.csrfToken() in your plugin to generate a new CSRF token, you will need to update that code:
const { generateToken } = require('../middleware/csrf');

const token = generateToken(req);
Pass true as the second parameter if you wish to force a new csrf token to be generated.

Flags Page Changes (🚨 breaking change)

The flag details template has been updated, and corresponding front-end logic for adding and editing flag notes has been changed.


If you have a customized flags/detail.tpl, update to the latest version of the file from the appropriate parent theme. The note editing is done via modal now. Instead of data-action="appendNote" and data-action="prepareEdit", just use data-action="addEditNote"

API changes

  1. Requests to API endpoints that are rejected due to unauthorized access (i.e. requesting authenticated endpoint while not logged in, or requesting admin endpoint after the relogin timeout has passed) now return a standardised API response instead of an empty object literal.
  2. Unauthorized requests via the client-side API module will now automatically prompt the user to (re-)login, instead of throwing an error.
  3. The api.delete() method has been removed, use api.del() instead.


If you were inspecting the payload for validity, you should instead look at the response code (as that remains unchanged; 401 Unauthorized).

If you continue to analyze the response body, failing requests now look something like this:

    "status": {
        "code": "not-authorised",
        "message": "A valid login session was not found. Please log in and try again."
    "response": {}

Base theme changes

The default theme is no longer assumed to be providing their templates to the currently enabled theme.

This change eliminates longstanding unexpected behaviour where the default theme’s templates were always applied even if the current theme is standalone (that is, it does not define a base/parent theme).


If you have a theme that relies on another theme, but you don’t have it defined — you should update your theme to explicitly define it in your theme’s theme.json now.
   "baseTheme": "nodebb-theme-persona"


© 2014 – 2023 NodeBB, Inc. — Made in Canada.