ChirpStack already terminates the LoRaWAN session and, with a codec on the device profile, hands you decoded JSON. Node-RED is the low-code glue layer after that: it lets you wire an uplink to a database insert, a dashboard gauge, an alert or a downlink without writing a service. The connection between the two is plain MQTT, so there is no special coupling and no vendor lock-in.
Node-RED + ChirpStack: Build LoRaWAN Flows over MQTT
Wire ChirpStack into Node-RED over MQTT: subscribe to the uplink topic, read msg.payload.object in a function node, then route to a DB, dashboard or downlink.
The data flow
Node-RED turns ChirpStack uplinks into low-code flows: subscribe to the MQTT event topic, read the decoded values from msg.payload.object in a function node, then fan out to a database, a dashboard or a downlink. This guide shows the full path with a small importable flow.
Sensor / controller
Measures or controls in the field and sends LoRaWAN uplinks.
LoRaWAN gateway
Receives the radio packets and forwards them to the server.
ChirpStack
Network server: manages sessions and decodes the payload.
ThingsBoard / Grafana
Dashboards, alarms, rules and reports.
What Node-RED adds on top of ChirpStack
Step 1: Publish events from ChirpStack
In the ChirpStack application under Integrations, enable the MQTT integration with the marshaler set to JSON. Every uplink is then published to:
application/{ApplicationID}/device/{DevEUI}/event/up
The message body is the same v4 event you would forward anywhere else. With a working codec on the device profile, the decoded values sit in the object field:
{
"deviceInfo": {
"applicationId": "00372e31-0620-4671-8270-237632a4e227",
"applicationName": "cold-chain",
"deviceProfileName": "EM300-TH",
"deviceName": "coldroom-01",
"devEui": "24e124136b502217"
},
"fPort": 85,
"fCnt": 128,
"data": "AXVkA2etAARoSw==",
"object": { "battery": 100, "temperature": 17.3, "humidity": 57.5 },
"rxInfo": [{ "gatewayId": "24e124fffef54092", "rssi": -51, "snr": 13.8 }],
"txInfo": { "frequency": 868100000 }
}
Step 2: Subscribe with an mqtt in node
Add an mqtt in node, point its broker config at the ChirpStack MQTT broker and subscribe to:
application/+/device/+/event/up
The two + wildcards match any ApplicationID and any DevEUI, so a single node receives every device. Set the node's Output to a parsed JSON object so the next node gets msg.payload as an object rather than a string.
Step 3: Read the decoded values in a function node
Wire the mqtt in node into a function node. The decoded sensor values are in msg.payload.object; the device identity is in msg.payload.deviceInfo. Shape the message for whatever comes next:
// mqtt in -> function: ChirpStack v4 uplink already parsed to an object
var p = msg.payload;
var o = p.object || {}; // decoded values from the codec
// Stop early if no codec ran (only raw Base64 in p.data)
if (!p.object) {
node.warn("No decoded object - is a codec set on the device profile?");
return null;
}
msg.devEui = p.deviceInfo.devEui; // stable, unique key
msg.topic = p.deviceInfo.deviceName; // human label for dashboards
msg.payload = {
devEui: p.deviceInfo.devEui,
name: p.deviceInfo.deviceName,
profile: p.deviceInfo.deviceProfileName,
ts: new Date(p.time || Date.now()).toISOString(),
temperature: o.temperature,
humidity: o.humidity,
battery: o.battery,
rssi: (p.rxInfo && p.rxInfo[0]) ? p.rxInfo[0].rssi : null
};
return msg;
From here you fan out: an influxdb out or postgres node for storage, a ui_gauge node for a dashboard, or a switch node that fires an alert when temperature crosses a threshold.
A minimal importable flow
This is a self-contained flow with the mqtt in node, the function above and a debug node. Paste it into Menu, Import in Node-RED and set your broker credentials:
[
{
"id": "mqtt_up",
"type": "mqtt in",
"name": "ChirpStack up",
"topic": "application/+/device/+/event/up",
"qos": "0",
"datatype": "json",
"broker": "cs_broker",
"wires": [["fn_shape"]]
},
{
"id": "fn_shape",
"type": "function",
"name": "shape uplink",
"func": "var p=msg.payload;var o=p.object||{};if(!p.object){node.warn('no codec object');return null;}msg.devEui=p.deviceInfo.devEui;msg.payload={devEui:p.deviceInfo.devEui,name:p.deviceInfo.deviceName,temperature:o.temperature,humidity:o.humidity,battery:o.battery};return msg;",
"outputs": 1,
"wires": [["dbg_out"]]
},
{
"id": "dbg_out",
"type": "debug",
"name": "decoded",
"active": true,
"complete": "payload",
"wires": []
},
{
"id": "cs_broker",
"type": "mqtt-broker",
"name": "ChirpStack broker",
"broker": "localhost",
"port": "1883"
}
]
Sending a downlink back to the device
To control a device (a relay, a valve, a thermostat) publish a JSON command to the downlink topic with an mqtt out node. Build the command in a function node:
// function -> mqtt out: enqueue a ChirpStack downlink
var appId = "00372e31-0620-4671-8270-237632a4e227";
var devEui = "24e124136b502217";
msg.topic = "application/" + appId + "/device/" + devEui + "/command/down";
msg.payload = {
devEui: devEui, // must match the DevEUI in the topic
confirmed: false,
fPort: 85, // must be greater than 0
data: "AQ==" // Base64 command; ChirpStack encrypts before sending
};
return msg;
Set the mqtt out node's output to JSON (or JSON.stringify the payload yourself). If a codec with an encodeDownlink function sits on the device profile, you can send an object field with readable values instead of data. For Class A devices the command is delivered in the RX window after the next uplink; for Class C devices almost immediately.
Pitfalls from the field
- Custom topic template: The
application/...event and command topics come from theintegration.mqttblock inchirpstack.toml. The default has no region prefix, but a customized deployment can change the template, so if your subscription sees no traffic, check the configuredevent_topicagainst what you subscribed to. (Theeu868/...prefix you may have seen elsewhere applies to the separate Gateway Bridgegateway/...topics, not these application topics.) - No codec, no object: If
msg.payload.objectis empty you only havemsg.payload.data(Base64). Put a codec on the device profile, see the ChirpStack payload decoder guide. - Datatype on mqtt in: Leave it on a parsed JSON object, otherwise
msg.payloadarrives as a string and.objectis undefined. - fPort greater than 0: Downlinks with fPort 0 are rejected.
- DevEUI as the key: Key your storage and dashboards on
devEui, not the display name, so renaming a device does not split its history.
When Node-RED is and is not the right layer
Node-RED is ideal for prototyping, light routing and bridging odd protocols. For long-term storage and proper dashboards, send the shaped data on to a time-series store and Grafana, or to ThingsBoard, see ChirpStack to ThingsBoard. We run the broker, the flows and the backups for you as Node-RED managed hosting alongside ChirpStack hosting, so the flows you build stay yours.
Frequently asked questions
Let's discuss your infrastructure. Digital and on-site.
Whether it's IoT platform development, hardware selection, managed hosting for ChirpStack, ThingsBoard, Grafana or NetBird VPN, or migration from a self-hosted setup - we'll find the right solution for your use case. Book a free 30-minute consultation, no commitment required.
Your contact
Timo Wevelsiep
Founder, merkaio
15 minutes, no commitment, directly with Timo.
merkaio is an independent integrator and is not affiliated with Milesight.