MQTT in Node-RED, and the line between what shipped and what you add
MQTT looks like one thing — a topic box and a payload box — but a working mqtt in or mqtt out node rests on a broker connection, a protocol version, a session, TLS, credentials, QoS and the Retain flag, and every one of those has its own rules. This part also does something the earlier chapters have not: it opens the add-on itself and inventories what is already installed before you add anything. Twenty-five packages ship inside the Node-RED add-on's image, a Palette Manager install runs new code with the same permissions as Node-RED itself, and one popular extension — FlowFuse Dashboard 2 — is not in the box at all. Nothing here connects to a real broker or installs a real package. It is the planning you do with the wires still unplugged.
userProperties or a Response Topic only exist once you pick 5MQTT node, bundled package, added package, optional extension — keep these apart
The urge with MQTT is to open the mqtt out node, type a topic, and move on. Before you do, it is worth knowing which of four different layers you are actually touching, because they carry completely different amounts of risk. The Node-RED Home Assistant add-on this guide follows is version 22.0.1, and it embeds Node-RED 5.0.2 and the HA WebSocket integration at 0.80.3.
| Layer | What it is, in this add-on | What you should worry about |
|---|---|---|
| MQTT core nodes | Node-RED 5.0.2's own mqtt in, mqtt out, and mqtt-broker | Broker ACLs, topics, QoS, retained messages, sessions, TLS, credentials, message size — all still yours to set |
| Add-on bundled dependencies | Exactly 25 entries, pinned in node-red/package.json | Not every entry is a node you can drag onto the canvas — some are runtime, theme, or wrapper support code |
| Custom packages | Whatever you add through Palette Manager, or the add-on's npm_packages / system_packages options | Supply-chain risk, native code, version drift, startup failure, and reconstructing it all after a restore |
| FlowFuse Dashboard 2 | @flowfuse/[email protected], optional and not bundled | Adds an HTTP route, a Socket.IO channel, and browser input — its own authentication boundary is yours to manage separately |
It is the difference between the wiring already in the wall, the appliances that came with the flat, the toaster you bought yourself and plugged into a spare socket, and the new circuit you asked an electrician to add. The MQTT core nodes are the wiring — they work as delivered, because Node-RED itself put them there. The 25 bundled dependencies are the appliances that came with the flat: already plugged in, already working, and you did not choose any of them. Palette Manager and npm_packages are the toaster you add yourself, running off the same electrical supply as everything else. FlowFuse Dashboard 2 is the new circuit — nobody wired it in for you, and until you commission it, it is not part of the flat at all.
The add-on also carries two grants worth knowing about before you go further: host_network: true and uart: true, plus media:rw and share:rw storage access. None of these mean MQTT, a serial node, or a file node is actually in use — they mean the container can reach further than a sandboxed add-on normally would, if a flow inside it asks to. Treat every grant as a ceiling on what is possible, not a report on what is happening.
flowchart TD A["Node-RED core
mqtt in, mqtt out, mqtt-broker"] --> B["Ships inside Node-RED 5.0.2 itself.
Nothing to install, nothing to audit"] C["The add-on's 25 bundled dependencies
pinned in package.json"] --> D["Already present the moment
the add-on starts. You chose none of them"] E["Palette Manager, or npm_packages
in the add-on's own options"] --> F["Third-party code you add on purpose.
Runs with the same permissions
as Node-RED itself"] G["FlowFuse Dashboard 2
@flowfuse/node-red-dashboard"] --> H["Optional. Not one of the 25.
You have to add it yourself, and secure it yourself"]
npm_packages, and FlowFuse Dashboard 2 last. Every arrow here is solid, and none of the four boxes on the left connects to any other layer's box. That separation is the whole point of this section: a question about one layer is rarely a question about another.Ticking a box rarely does everything you assume it does
Every MQTT setting on a Node-RED node answers one narrow question. The broker connection settles the transport, the protocol version, and the session. MQTT In settles the subscription filter, the subscription QoS, and the shape of the data coming out. MQTT Out settles the publish topic, its QoS, its Retain setting, and any MQTT 5 properties riding along. Ticking the TLS checkbox does not create a topic ACL. Choosing QoS 2 does not make whatever happens downstream in your flow run “exactly once.” The table below is the seven ideas worth knowing apart, with the misreading that trips people up sitting right next to the real meaning.
| Item | What it precisely means | The common misreading |
|---|---|---|
| Topic | A subscription may use + or # filters; a publish topic cannot contain either | Subscribing to a whole # tree does not mean everything under it may be processed or disclosed |
| QoS 0 | At-most-once delivery; a message may simply be lost | Low overhead does not mean errors and disconnects can be ignored |
| QoS 1 | At-least-once delivery; a duplicate is possible | A broker acknowledgment does not mean the side effect it triggers only ran once |
| QoS 2 | An exactly-once handshake at the protocol layer | It does not guarantee exactly-once behavior end to end — not in your flow, not in the HA service call, not on the device |
| Retain | The broker keeps the last retained message on that topic and hands it to whoever subscribes next | It is not a history log — a wrongly retained command keeps affecting every new subscriber until it is cleared |
| Session | Clean session / clean start, client ID, session expiry, and the broker's own state together decide behavior | It is not Node-RED's flow or global context, and it does not guarantee nothing is ever lost |
| TLS / credentials | TLS protects the transport and the server's identity; a username and password authenticate you to the broker | Neither one replaces a per-client publish or subscribe ACL on the broker itself |
QoS 2 is a courier who gets a signature before leaving your doorstep. That signature proves one thing only: the parcel changed hands between the courier and whoever answered the door. It says nothing about what that person does with the parcel afterward — whether they open it once, twice, or hand it straight to somebody else. QoS 2 is the same shape of promise. It guarantees the packet crossed from broker to client exactly once. Whether the light.turn_on call your flow makes after receiving it also runs exactly once — through a Node-RED restart, a retry, or the same message looping back in — is a guarantee you have to build yourself, not one MQTT hands you for free.
Protocol version matters more than it looks. MQTT 5 adds properties and subscription flags that simply do not exist under 3.1 or 3.1.1: a Response Topic, correlation data, content type, message expiry, user properties, No Local, Retain As Published, Retain Handling, and session expiry. Pick MQTT 3.1 or 3.1.1 and none of that is available to you — not because Node-RED hid it, but because the protocol version you chose does not carry it. Pin the version your broker actually supports before you configure a node around it; do not let a client guess.
Six checks you can finish without a broker in sight
Everything in this section happens on paper, or in a text file — not against a running broker. The first four checks apply to anyone touching MQTT at all. The last two only apply if you are also auditing packages or planning a Dashboard change; skip them if MQTT is all you came for.
-
Step 1
Write a topic contract
For each direction data flows, write down a placeholder topic, the publisher, the subscriber, the payload's shape, its maximum byte size, how often it is allowed to fire, its QoS, its Retain setting, and how duplicates are handled. Do not put a real broker address, a real client ID, or real credentials into this document.
-
Step 2
Decide the protocol and the session behavior
Write down MQTT 3.1, 3.1.1, or 5 explicitly. Then decide clean session or clean start, the client ID, automatic unsubscription, and — only under MQTT 5 — session expiry. Leave blank whatever the protocol version you chose does not support.
-
Step 3
Read the example flow, offline
Open
14-mqtt-in-out.jsonwithout importing it live. Confirm both the MQTT In and MQTT Out nodes carryd:true, that the broker configuration hasautoConnect:false, and that the host, client ID, and topics are all complete placeholders with no credentials anywhere in the file. -
Step 4
Check TLS and ACLs on paper
In your written specification, require a controlled TLS configuration, server certificate verification, separate credentials per client, and least-privilege publish/subscribe ACLs per topic. Never write a secret into a broker URL, a Function node, an exported flow, or Debug output.
-
Step 5 — conditional
Inventory where every package came from
Only if you are auditing the bundled nodes or evaluating a new install: compare against the 25-entry matrix later in this part. Record the exact npm package and pinned version, its maintenance status, its license, its transitive dependencies, its permissions, and how you would remove it. MQTT-only readers can skip this step entirely.
-
Step 6 — conditional
Write down failure and recovery, for packages or Dashboard work
Only if packages or FlowFuse Dashboard are involved: document what a package startup failure looks like, what a routing or Socket.IO problem looks like, and how you would recover. In the MQTT topic contract, also record what broker unreachability, a certificate error, an ACL denial, a duplicate, a stale retained message, or a session recovery would look like. Keep the relevant nodes disabled and do no I/O while you write this.
The literal contract from the source material looks like this — a template to fill in, not a live configuration:
broker: PLACEHOLDER_MQTT_BROKER_HOSTclient id: PLACEHOLDER_MQTT_CLIENT_IDsubscribe: PLACEHOLDER_MQTT_INPUT_TOPICpublish: PLACEHOLDER_MQTT_OUTPUT_TOPICprotocol: PLACEHOLDER_MQTT_VERSIONmax payload: PLACEHOLDER_LIMITcredentials: store only in the controlled credentials storenetwork action: do not executeThe check you run before calling this part done
| Artifact | What it should contain | Stop if |
|---|---|---|
| Topic contract | Placeholder topic, publisher/subscriber, payload shape/bytes/frequency, QoS, Retain setting, ACLs, duplicate handling | A real broker, client, topic, or secret has crept in, or the payload and ACL boundaries are still undefined |
| Protocol/session decision | Pinned MQTT version, clean start/session behavior, client-ID rule, expiry, TLS server verification, and where credentials live | The design leans on broker auto-detection, shares a client ID, disables certificate verification, or uses a field the chosen protocol does not support |
| Disabled-state check | MQTT In/Out carry d:true, the broker has autoConnect:false, and no install, connection, Dashboard, file, or serial action has run | Any executable node is enabled, connects at startup, or acceptance depends on reaching a real broker |
The exact fields on Node-RED 5.0.2's two MQTT nodes
MQTT In has five things you decide: which broker, what topic or action, what QoS, which MQTT 5 flags, and what shape the output takes.
| Field | Options on 5.0.2 | Where the risk sits |
|---|---|---|
| Broker | A required reference to an mqtt-broker configuration node | Select only an approved configuration — do not accept an override to a different broker |
| Action / Topic | A Static topic has no input wire; a Dynamic topic gives the node one input | Prefer a fixed subscription filter. Allow a dynamic subscription only from controlled internal messages, never raw external input |
| QoS | 0, 1, or 2 | This is the requested maximum. You still handle duplicates and any downgrade the broker applies |
| MQTT 5 flags | nl (No Local), rap (Retain As Published), rh (Retain Handling, 0/1/2) | Shown, and effective, only against an MQTT 5 broker. Set each value on purpose rather than leaving a default |
| Output | auto-detect, legacy auto, buffer, UTF-8 string, parsed JSON, or Base64 | Auto-detected output still needs validating. With a known schema, prefer a fixed output type over auto-detect |
What comes out of MQTT In always includes msg.topic, msg.payload, msg.qos, and msg.retain. An MQTT 5 packet can also carry responseTopic, correlationData, contentType, messageExpiryInterval, payloadFormatIndicator, reasonString, and userProperties.
responseTopic, userProperties, or any other property straight into choosing a Home Assistant Action, a file path, or an HTTP URL. It arrived on the wire, which means anyone who can publish to that topic can shape it.MQTT Out: whichever value is set on the node wins
MQTT Out has Broker, Topic, QoS, and Retain fields. If Topic is left blank on the node, it falls back to msg.topic. If QoS or Retain is left blank, they fall back to msg.qos or msg.retain. But a value fixed directly on the node always takes precedence over the same value carried on the message — and that flexibility cuts both ways: an untrusted message can change what gets published if you leave the node's fields empty.
msg.topic is ignored completely, even if a Function node upstream carefully built one.msg.topic decides where the message goes — which means anything able to shape msg.topic can redirect the publish.flowchart TD
A["A message reaches MQTT Out"] --> B{"Is Topic filled in
on the node itself?"}
B -->|"yes"| C["The node's Topic wins.
msg.topic is ignored"]
B -->|"no"| D["msg.topic is used instead"]
A --> E{"Are QoS or Retain set
on the node itself?"}
E -->|"yes"| F["The node's values win.
msg.qos / msg.retain ignored"]
E -->|"no"| G["msg.qos or msg.retain
is used instead"]
MQTT 5 also lets MQTT Out carry User Properties, a Response Topic, Correlation Data, Content Type, and Message Expiry. A Response Topic is protocol metadata, nothing more — it does not, by itself, create a secure request/response mechanism. If you want a reply pattern out of it, the ACL on the reply topic, the uniqueness of the correlation ID, a timeout, duplicate handling, and what happens to a late reply are all design decisions you still have to make yourself. And whichever node you are setting it on: a publish topic can never contain the + or # wildcards, even though a subscription topic can use either freely.
flowchart TD A["A topic string you are
about to type"] --> B{"Does it contain
a + or a # ?"} B -->|"no"| C["Fine either way.
home/kitchen/temp works on
MQTT In or MQTT Out"] B -->|"yes"| D{"Which node are
you setting it on?"} D -->|"MQTT In (subscribe)"| E["Accepted. home/+/temp or
home/# are valid filters"] D -->|"MQTT Out (publish)"| F["Rejected. A publish
topic names one exact topic"]
Connection, session, security, messages, and the MQTT 5 extras — five separate groups
The mqtt-broker configuration node is where the transport, the protocol version, and the session actually get decided. Node-RED 5.0.2 groups its settings into five parts.
| Group | 5.0.2 fields | The principle to hold to |
|---|---|---|
| Connection | Name, Broker, Port, Auto-connect, Use TLS / TLS config, Protocol Version (3/4/5), Client ID, Keepalive | Put a host or a controlled scheme in Broker; pin the port; verify the server certificate under TLS; give every persistent session a unique client ID |
| Session | Clean session (v3/v4) or Clean start (v5), Auto unsubscribe; v5 adds Session Expiry and User Properties | A non-clean session needs a client ID. Put a limit and a cleanup policy on offline queues and stale subscriptions |
| Security | Username and Password credentials | Store these in Node-RED's credentials store — never in a broker URL or a flow's JSON — and rely on the broker's own ACLs to restrict topics in both directions |
| Messages | Birth, Close, and Will — each with its own Topic, Payload, QoS, and Retain | Only configure these for an explicit lifecycle need. Avoid a retained command here, and keep the payload fixed and free of secrets |
| MQTT 5 message properties | Content Type, User Properties, Response Topic, Correlation Data, Expiry; Will also gets a Delay | Only meaningful under v5. Constrain byte size, type, and expiry, and never let external input choose a control topic |
Protocol Version is a plain number on the node: 3 means MQTT 3.1 compatibility, 4 means MQTT 3.1.1, and 5 means MQTT 5. If a node you imported from an older flow still reads 3 or 4 and you need the MQTT 5 properties from the previous section, that number is the first thing to change — and the first thing to confirm your broker actually supports.
Ticking Use TLS is sealing the envelope so nobody along the way can read what is inside. Turning on server certificate verification is checking the name on the door before you push that sealed envelope through the slot. A sealed envelope pushed through the wrong door is still private mail — it has just been handed to a stranger instead of the person you meant.
verifyservercert to false. If the TLS configuration you point at does not itself supply rejectUnauthorized, the runtime falls back to that old verifyservercert value. When you migrate an older configuration, move it onto an approved TLS configuration and confirm the value it ends up with is rejectUnauthorized=true. Turning the TLS toggle on tells you the transport is encrypted — it does not by itself prove the broker's identity was checked.When Auto-connect is turned off, Node-RED 5.0.2 still allows a controlled action message to connect or disconnect the broker on command. That capability is not meant as an entry point for outside traffic: never wire an external payload from MQTT In or HTTP In straight into broker control. And because the add-on's host_network: true grant can put more of your local network within reach of the container, a broker allowlist and real ACLs matter more here than they would in a fully sandboxed setup.
14-mqtt-in-out.json, both the MQTT In and MQTT Out nodes are fixed at d:true — disabled. A configuration node has no d field of its own, so it is autoConnect:false on the broker configuration that keeps it from ever opening a connection. The broker field on each MQTT node only references a configuration ID inside that same file; the actual host, client ID, subscribe topic, and publish topic are every one of them placeholders.The 25 pinned dependencies, and what running any of them actually means
The add-on's node-red/package.json pins exactly 25 direct dependencies: 19 give you Palette or configuration nodes, 1 is the Node-RED runtime itself, 1 is an editor theme, and 4 are wrapper or runtime support libraries that never show up as a node you can drag onto a canvas. Preinstalled means available — it does not mean any flow has actually run the package. If MQTT is all you came for, you can skip straight past this table to the next section.
| Package & pinned version | Role | What it is for, and the main risk |
|---|---|---|
[email protected] | Wrapper support library, not a Palette node | Hashes passwords for HTTP Basic Auth. Never expose the plaintext or the hash input, and do not mistake it for an available node |
[email protected] | Wrapper support library, not a Palette node | Parses YAML. Still constrain the byte size, structure, and types of any untrusted YAML you feed it |
[email protected] | The runtime baseline, not an extra Palette node | Provides the editor, the runtime, and the core nodes. Do not confuse this version number with the add-on's own 22.0.1 |
[email protected] | Palette node | Schedules can repeat or fire downstream actions at the wrong time because of time zones, DST, or a restart |
[email protected] | Palette / configuration nodes | Connects to and controls Cast devices. Pin the device, the media URL, and the network scope |
[email protected] | Palette node | Validate increment, decrement, reset input and restart state, so an incorrect threshold cannot trigger an action |
[email protected] | Palette / configuration nodes | Can read Home Assistant state and take actions in it. Protect tokens and environment IDs, and constrain side effects node by node |
[email protected] | Palette / configuration nodes | Queries and writes a database. Pin the query and write shapes, the credentials, and the retention policy |
[email protected] | Palette node | Handle out-of-order, missing, and duplicate data, plus post-deploy state, or it will report an incorrect duration |
[email protected] | Palette / configuration nodes | Can read and write OT or physical devices. Pin the host, unit, address, and function, and keep every write disabled until reviewed |
[email protected] | Palette node | Pin the time zone, locale, and input format, or you get parsing ambiguity or DST errors |
[email protected] | Palette node | Validate the persisted states and transitions. After a recovery, confirm the state before letting it trigger an action |
[email protected] | Palette node | Location and time-zone data can be sensitive; restarts, DST, and polar-region dates can cause missed or duplicate events |
[email protected] | Palette node | Handle time zones, DST, and midnight crossings explicitly so a message does not land on the wrong side-effect branch |
[email protected] | Palette node | Base64 is encoding, not encryption — its output can still contain secrets |
[email protected] | Palette / configuration nodes | Sending or receiving email creates network and data-exfiltration risk. Pin recipients, attachments, and credentials |
[email protected] | Palette node | Fetches an external feed. Pin the HTTPS URL, the response size limit, and the timeout, and treat the content as untrusted |
[email protected] | Palette node | Can probe out across the host network. Pin the host and the frequency, and reject a dynamic destination |
[email protected] | Palette node | Not cryptographic randomness. Never use it for a token, a password, a nonce, or an authorization decision |
[email protected] | Palette / configuration nodes | Can read and write physical devices through the UART grant. Pin the device path, the baud rate, and the command, and keep writes disabled until reviewed |
[email protected] | Palette node | Validate the numeric type and cap the sample window, or you get unbounded state or an incorrect smoothing result |
[email protected] | Palette node | Coordinates reveal sensitive location data. Handle time zones, DST, and dates without a valid sunrise or sunset |
@node-red-contrib-themes/[email protected] | Editor theme, not a runtime Palette node | Changes appearance only — it is neither a security control nor a flow feature |
[email protected] | Wrapper support library, not a Palette node | Processes data one line at a time. Still constrain the size and error handling of large or untrusted files |
[email protected] | Stack-trace support library, not a Palette node | Being on this list does not prove a node is registered anywhere. Stacks and logs can still expose paths and sensitive data |
Read that table by function, not just by name. node-red-contrib-cast, node-red-contrib-influxdb, node-red-contrib-modbus, node-red-node-email, node-red-node-feedparser, node-red-node-ping, and node-red-node-serialport can all perform network, database, or physical I/O. node-red-contrib-bigtimer, node-red-contrib-sunevents, and node-red-node-suncalc can generate messages on their own, based on time, with nobody clicking anything. And even the ones that look like pure transformation — node-red-contrib-counter, node-red-contrib-moment, node-red-node-smooth — can hand a wrong result to whatever acts on it downstream. Being bundled is not a reason to skip validating any of them.
Adding a node through Palette Manager is not like adding an icon to a phone's home screen, sandboxed away from everything else. It is closer to handing a new hire the same keycard you carry yourself — the one that opens every door in the building — because that is exactly how much access the Node-RED process has, and the new package's server-side JavaScript now runs inside that same process.
On every startup, the add-on installs system_packages with the Alpine package manager first, then changes into /opt and runs npm install --omit=dev --omit=optional for anything listed in npm_packages, then finally runs eval on each line of init_commands. Any single failure in that chain aborts the whole startup.
flowchart LR A["system_packages
installed with the Alpine
package manager"] --> B["npm_packages
cd /opt, then
npm install --omit=dev --omit=optional"] B --> C["init_commands
each line run with eval"] C --> D["Node-RED starts"] A -.->|"fails"| X["Startup aborts"] B -.->|"fails"| X C -.->|"fails"| X
| Method | Persistence / startup behavior | The control that matters |
|---|---|---|
| Image-bundled pins | Provided by the add-on image itself, all 25 versions pinned | Compare the package matrix and each node's behavior whenever you upgrade the add-on |
| Palette Manager | Manages extra nodes inside the Node-RED user directory; the add-on fixes nodesDir=/config/nodes | Pin a version and a source before installing; confirm it can be reconstructed after a backup and restore |
npm_packages | The declared option persists, but installation runs again every time the add-on starts | Use the full package name plus an exact version. A dead registry or an install error blocks startup |
system_packages | Updates the index, then installs each item, on every startup | Native execution surface, architecture, repository availability, and startup time all become concerns |
init_commands | Runs shell eval line by line after everything else installs | It is not a package manager. Never put a secret in it, never use it to dodge pinning, and do not run it while you are still at the planning stage |
Do not record just a package name with a loose version range. At minimum keep the exact version, the registry identity, the source repository, the license, the maintainer, the publish date, any known advisories, Node and Node-RED compatibility, a summary of the transitive tree, and a rollback version. Pinning your direct dependency does not freeze every transitive one underneath it. And because the add-on's backups explicitly exclude node_modules, a restore does not reproduce the installed tree byte for byte — your own notes, plus an available registry, are what let you rebuild the same dependencies afterward. Do not manage the same package through both Palette Manager and npm_packages at once; that is how ownership and version both go ambiguous.
File nodes and the storage grants
Node-RED's core also ships File, File In, and Watch. The add-on's media:rw and share:rw grants give the container potential read/write reach — they do not, on their own, authorize any flow to touch any particular path. If a file name is ever built from MQTT, Dashboard, or HTTP input, that opens the door to path traversal, overwriting, or exfiltration. Pin the base directory, allowlist the file names you accept, cap the byte size and the frequency, and keep every write disabled until you have reviewed it. The same logic applies to serial nodes: uart: true is a grant, not approval to run an arbitrary device command.
Twenty-seven registrations, and a boundary you have to draw yourself
The 25-entry list in the previous section does not include @flowfuse/node-red-dashboard. The pinned version, 1.30.2, declares it needs Node >=14 and Node-RED >=3.0.0. Node-RED 5.0.2 sits inside both of those ranges, but that is a declared compatibility range, not a guarantee — you still test it against this specific add-on before relying on it. Installing it registers 5 configuration nodes and 22 ui-* widgets, 27 registrations in total, and it brings its own server code, its own browser code, an HTTP route, and a Socket.IO channel along with it.
| Boundary | How 1.30.2 behaves | What to verify yourself |
|---|---|---|
| Package identity | @flowfuse/[email protected] | Use the scoped package at the exact version — do not accidentally pick the unscoped legacy one instead |
| Route | The ui-base path defaults to /dashboard, mounted under Node-RED's own httpNodeRoot | The add-on's own root is /endpoint. Confirm the real external path, any proxy rewrite, and any conflict, in an isolated test first |
| PWA manifest | /dashboard/manifest.webmanifest is a hard-coded route; a custom ui-base.path does not move it, and it sits outside the Dashboard middleware that a custom path would otherwise apply | If you use a custom path, test this fixed route's external path, its authentication, and its response separately |
| HTTP authentication | Dashboard has its own optional HTTP middleware, separate from the add-on's editor login, its Ingress, and any direct endpoint | Confirm the initial HTML, setup, assets, and page routes all sit behind the same authentication and authorization policy |
| Socket.IO | The socket path combines httpNodeRoot, the Dashboard path, and socket.io; ioMiddleware can be configured separately | Test the handshake, same-origin behavior, session authentication, message-size limits, proxy upgrade and streaming, and reconnection |
| Browser input | Buttons, forms, templates, and other widgets can send data back into a flow | Treat every widget input as untrusted — enforce a schema, an authorization check, a rate limit, and isolation from the side effect it triggers |
The add-on's direct /endpoint/ NGINX path does not pass through the editor's Supervisor auth_request. Ingress only accepts the Supervisor's own ingress source, and that on its own does not prove every Dashboard viewer has authorization for every widget. Check directly whether http_node Basic Auth is actually protecting your route, and the Socket.IO session, over the real proxy path you use — not just the editor. With a custom Dashboard path, test that custom route and the hard-coded /dashboard/manifest.webmanifest separately, because a login requirement on the editor does not automatically extend to the Dashboard page, its manifest, its assets, or its real-time channel.
npm_packages entry. Work through the pinning, the supply-chain check, the routing check, and the recovery plan above before you add it to a real add-on.If you already run the legacy Dashboard
The old, unscoped node-red-dashboard is built on Angular v1 and is considered legacy; nothing here teaches a fresh install of it. Its interim scoped name, @flowforge/node-red-dashboard, is deprecated and no longer updated. FlowFuse Dashboard 2 is a rewrite, not a drop-in replacement — do not assume existing ui_* nodes, themes, layouts, custom templates, browser scripts, URLs, authentication, or client state carry over automatically.
Inventory the existing flow's pages, groups, widgets, template code, routes, authentication, CSS, browser dependencies, and message contracts first. Rebuild it page by page in an isolated copy, and compare the two side by side. If you run the legacy and new Dashboards at the same time during migration, you also have to prevent conflicts in routes, Socket.IO, credentials, and duplicated side effects. Do not remove the old package first, and do not point a new widget at a production action until the comparison is finished.
The failures that actually happen here
| Symptom | Likely cause | What to do |
|---|---|---|
| An MQTT node stays disconnected | Something earlier in the chain is wrong — host, DNS, port, protocol version, TLS chain, hostname, client ID, credentials, or the broker's own ACL | Keep In/Out disabled and check that list in order. Do not start by disabling certificate validation or switching to anonymous access |
| The same message arrives more than once | QoS 1, a reconnect, or a subscriber restart can all cause redelivery | Use a stable message ID or business key for bounded deduplication, with a defined retention period. Do not assume QoS 2 gives end-to-end exactly-once for HA, email, or a database write |
| A new subscription immediately receives an old command | The broker still holds a retained message on that topic | Check the packet's Retain flag and the broker's retained state. Do not use this chapter's example to publish a clearing message — get the broker's own administration to confirm the topic, the permissions, and the scope first |
| Subscriptions misbehave after a non-clean session | The client ID is not unique and stable, or session expiry / auto-unsubscribe / broker queue limits are off, or an old client is still online | Confirm the client ID is unique. Never share one client ID across multiple running instances |
| JSON output reports a parse error | A mismatch between the content type, the encoding, and what the publisher actually sent | Confirm all three. Do not switch unparseable bytes to auto-detect and then trust the result as an object — cap the payload size and route errors to a bounded Catch path |
| The add-on fails to start after a custom package is added | An issue with the exact package, the architecture, registry availability, or the fixed system_packages → npm_packages → init_commands sequence |
Check each in order. Use your recorded rollback version instead of repeatedly trying unpinned ones |
| A package shows up in the inventory, but the node you expected is missing | The package's role was never a Palette node to begin with | bcryptjs, js-yaml, line-by-line, source-map-support, the Node-RED runtime, and the theme collection are not ordinary runtime Palette nodes. Check the package's actual registrations and the startup log next |
| The Dashboard page loads, but nothing updates live | A break somewhere between the external route rewrite, the Socket.IO path, the HTTP/Socket.IO middleware, proxy streaming or upgrade, same-origin policy, or session handling | Check each of those separately. Do not expose the direct endpoint or drop authentication just to debug it |
| A migrated legacy Dashboard flow looks or behaves differently | Dashboard 2 is a rewrite, not a drop-in replacement | Rebuild configuration nodes, widgets, layouts, CSS, templates, and message contracts page by page. Do not freshly install the legacy package or swap node names between the two directly |
The ones that come up again and again
Does QoS 2 guarantee a Home Assistant action only runs once?
The broker's ACL is not approved yet. Can I test with a shared admin account in the meantime?
The example broker configuration has no d:true. Does that mean it connects automatically?
d field that an executable node uses. In the example, both MQTT In and MQTT Out are fixed at d:true, and the broker configuration is fixed at autoConnect:false. The host and client ID are placeholders too, so do not replace them, enable the nodes, or deploy the file as it stands.The add-on preinstalls 25 packages — does the Palette gain 25 sets of nodes?
bcryptjs, js-yaml, line-by-line, and source-map-support among them. Transitive dependencies underneath those 25 are not part of this count at all.Can I put just a package name into npm_packages and let it track the latest version automatically?
Does FlowFuse Dashboard 2 come bundled with the add-on?
@flowfuse/[email protected] is optional and is not one of the 25 bundled direct dependencies. Adding it brings its own route, Socket.IO channel, and browser input along with it, which is why it needs its own supply-chain, authentication, and proxy review, separate from everything already in the box.Can I freshly install the old node-red-dashboard in a new flow?
node-red-dashboard is an Angular v1 legacy package, and the new scoped one is a rewrite rather than a drop-in replacement. For an existing system, inventory it and migrate in stages instead of starting a brand-new legacy deployment.Where to go from here
You now know what shipped, what you would be adding, and where the line between them sits.
Part 12 closes out this series: backing up the add-on and the secrets inside it properly, and the performance and troubleshooting habits — logs, memory, slow flows — that keep a Node-RED instance you actually rely on from surprising you.
Open the full guidePart 11 of the WoowTech Complete Node-RED Guide series on the Apporo blog.
Adapted from the WoowTech Complete Node-RED Guide, produced by WoowTech and released under CC BY 4.0. This adaptation is published by Apporo under the same licence.
Light · Air · Water · Control · apporo