IoT Automation

ChirpStack MQTT Integration for LoRaWAN: Forward Decoded Uplinks to a Cloud Broker

Use ChirpStack's built-in MQTT integration to publish decoded LoRaWAN uplinks, then forward them to a cloud MQTT broker with either a Mosquitto bridge or a small Node.js forwarder.

ChirpStack MQTT integration forwarding decoded LoRaWAN uplinks from a local network server to a cloud MQTT broker
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

The ChirpStack MQTT integration publishes every LoRaWAN event from your own network server to an MQTT broker, which makes it the natural starting point for getting field sensor data into dashboards and automation. In this tutorial you will configure the integration in ChirpStack v4, decode payloads with a device profile codec, and forward decoded uplinks to a cloud broker in two ways: a zero-code Mosquitto bridge and a Node.js forwarder that reshapes the data into your own topic tree.

How ChirpStack Uses MQTT

ChirpStack is an open-source LoRaWAN network server. In a typical installation (for example the official Docker Compose setup), MQTT appears twice:

  1. Gateway backend. Gateway bridges publish raw radio frames to a local Mosquitto broker, and ChirpStack subscribes to them.
  2. MQTT integration. After deduplication, decryption and decoding, ChirpStack publishes application events (uplinks, joins, acknowledgements, errors) to MQTT topics on the same local broker.

The integration events are what you want. They live on a broker inside the LoRaWAN server's network, which is usually not reachable from dashboards, phones or other sites. Forwarding them to a cloud broker solves that without exposing the network server itself.

 Sensors ~~LoRa~~> Gateway --> local Mosquitto <--> ChirpStack
                                    |
                     application/.../event/up
                                    |
                  Mosquitto bridge or Node.js forwarder
                                    |   (TLS)
                                    v
                           Cloud MQTT broker --> Node-RED, dashboards, alerts

Step 1: Enable the ChirpStack MQTT Integration

In chirpstack.toml, the integration section looks like this in a standard install:

[integration]
  enabled = ["mqtt"]

  [integration.mqtt]
    server = "tcp://mosquitto:1883/"
    json = true

With json = true, events are JSON rather than Protobuf, which is far easier to work with. The default event topic is:

application/<application_id>/device/<dev_eui>/event/<event>

where <event> is up, join, ack, txack, log, status or location. Restart ChirpStack after changing the file, then watch the local broker on the server:

mosquitto_sub -h localhost -p 1883 -t 'application/+/device/+/event/up' -v

Step 2: Decode Payloads with a Codec

Raw LoRaWAN payloads are bytes, delivered base64-encoded in the data field. Add a JavaScript codec to the device profile (Device profiles > your profile > Codec, type JavaScript) so ChirpStack adds a decoded object field to every uplink:

function decodeUplink(input) {
  var b = input.bytes;
  if (input.fPort !== 1 || b.length < 6) {
    return { errors: ["unexpected fPort or length"] };
  }
  var temp = (b[2] << 8) | b[3];
  if (temp & 0x8000) temp -= 0x10000;
  return {
    data: {
      moisture_pct: ((b[0] << 8) | b[1]) / 10,
      soil_temp_c: temp / 10,
      battery_v: ((b[4] << 8) | b[5]) / 1000
    }
  };
}

An uplink event then contains, among other fields, deviceInfo.deviceName, deviceInfo.devEui, fCnt, fPort, time, object (your decoded values) and rxInfo (one entry per gateway with rssi and snr).

Option A: Forward with a Mosquitto Bridge

If ChirpStack already runs with Mosquitto, a bridge is the quickest route: a few lines of configuration and Mosquitto forwards matching topics to the cloud broker, reconnecting automatically. Create /etc/mosquitto/conf.d/cloud-bridge.conf (or the equivalent in your Docker volume):

connection connectmq
address broker.connectmq.com:1883
remote_clientid cs-bridge-c3h7k5
cleansession true
try_private false
bridge_protocol_version mqttv311
notifications true
notification_topic connectmq/demo/c3h7k5/bridge/state

# local:  application/<app>/device/<eui>/event/up
# remote: connectmq/demo/c3h7k5/<app>/device/<eui>/event/up
topic +/device/+/event/up out 1 application/ connectmq/demo/c3h7k5/

The topic line takes a pattern, a direction, a QoS and two prefixes. Mosquitto strips the local prefix application/ and adds the remote prefix, which keeps the remote topic at eight levels, the maximum allowed on the public test broker. Replace c3h7k5 with your own random string; the public area is shared and readable by everyone. try_private false is recommended when the remote broker is not Mosquitto.

Restart Mosquitto and subscribe from anywhere:

mosquitto_sub -h broker.connectmq.com -p 1883 -i watch-c3h7k5 \
  -t 'connectmq/demo/c3h7k5/#' -v

For production, switch to TLS and the credentials issued with your private broker:

connection connectmq
address broker.connectmq.com:8883
bridge_cafile /etc/ssl/certs/ca-certificates.crt
remote_clientid YOUR_CLIENT_ID
remote_username YOUR_USERNAME
remote_password YOUR_PASSWORD
cleansession true
try_private false
notifications true
notification_topic farm1/lorawan/bridge/state
topic +/device/+/event/up out 1 application/ farm1/lorawan/

The limitation of a bridge is that payloads are forwarded unchanged. ChirpStack events are verbose, with base64 data, transmit info and full gateway metadata, and topics use application UUIDs and DevEUIs instead of readable names.

Option B: A Node.js ChirpStack MQTT Forwarder

When consumers should see compact, named data, a small forwarder is better. This one uses MQTT.js v5, subscribes to uplinks on the local broker and republishes a clean record per device. Install with npm install mqtt.

// forwarder.mjs
import mqtt from "mqtt";

const LOCAL_URL = process.env.LOCAL_URL ?? "mqtt://localhost:1883";
const OUT_URL = process.env.OUT_URL ?? "mqtt://broker.connectmq.com:1883";
const OUT_PREFIX = process.env.OUT_PREFIX ?? "connectmq/demo/c3h7k5/farm";

const out = mqtt.connect(OUT_URL, {
  clientId: process.env.OUT_CLIENT_ID ?? "cs-forwarder-c3h7k5",
  username: process.env.OUT_USER,
  password: process.env.OUT_PASS,
  protocolVersion: 5,
  will: { topic: `${OUT_PREFIX}/forwarder/status`, payload: "offline", qos: 1, retain: true },
});
out.on("connect", () => {
  console.log("cloud broker connected");
  out.publish(`${OUT_PREFIX}/forwarder/status`, "online", { qos: 1, retain: true });
});
out.on("error", (err) => console.error("cloud:", err.message));

const local = mqtt.connect(LOCAL_URL, { clientId: "cs-forwarder-local-c3h7k5" });
local.on("connect", () => {
  local.subscribe("application/+/device/+/event/up", { qos: 1 });
  console.log("local broker connected");
});

local.on("message", (topic, payload) => {
  let ev;
  try {
    ev = JSON.parse(payload.toString());
  } catch {
    return console.warn("non-JSON event on", topic);
  }
  if (!ev.object) {
    return console.warn(`${ev.deviceInfo?.deviceName}: no decoded object, check the codec`);
  }
  const rx = ev.rxInfo ?? [];
  const best = rx.reduce((a, g) => ((g.rssi ?? -999) > (a.rssi ?? -999) ? g : a), {});
  const name = ev.deviceInfo.deviceName.replace(/[^A-Za-z0-9_-]/g, "_");
  const record = {
    ts: ev.time,
    dev_eui: ev.deviceInfo.devEui,
    ...ev.object,
    f_cnt: ev.fCnt,
    rssi: best.rssi ?? null,
    snr: best.snr ?? null,
    gateways: rx.length,
  };
  out.publish(`${OUT_PREFIX}/${name}/telemetry`, JSON.stringify(record), { qos: 1, retain: true });
  console.log(name, record);
});

Run it next to ChirpStack:

node forwarder.mjs
# production
OUT_URL=mqtts://broker.connectmq.com:8883 OUT_USER=YOUR_USERNAME \
OUT_PASS=YOUR_PASSWORD OUT_CLIENT_ID=YOUR_CLIENT_ID OUT_PREFIX=farm1/north \
node forwarder.mjs

Device names are sanitised before going into the topic, since characters like +, # and / would break the hierarchy. Records are retained so dashboards immediately see each sensor's latest reading, even though LoRaWAN devices may report only every 15 minutes.

Choosing Between the Two

Criterion Mosquitto bridge Node.js forwarder
Setup effort A config file A small script and a service
Payload Full ChirpStack event Compact record you define
Topic names Application ID and DevEUI Readable device names, your hierarchy
Moving parts None beyond Mosquitto One extra process to monitor
Downlinks Add an in topic line Add a subscription and publish

Many installations start with the bridge to get data flowing and move to a forwarder once consumers multiply. The The Things Network MQTT tutorial uses the same forwarder idea in Python, which is useful if your farm mixes community and private LoRaWAN networks.

To queue a downlink, publish to the device's command topic on the local broker:

mosquitto_pub -h localhost -p 1883 \
  -t 'application/YOUR_APP_ID/device/0004a30b001c1234/command/down' \
  -m '{"devEui":"0004a30b001c1234","confirmed":false,"fPort":2,"data":"Hg=="}'

data is base64 (Hg== is the single byte 30, for example a reporting interval in minutes). Class A devices receive it after their next uplink. Only forward downlink commands from the cloud if you trust every client that can publish to them, and validate them in the forwarder.

Troubleshooting

  1. Nothing on event/up. Check the device has joined (look for event/join), that the integration is enabled, and that json = true if you expect JSON.
  2. object missing. The codec is absent, failed, or returned errors; event/log messages show codec errors.
  3. Bridge connects then drops. Duplicate remote_clientid, wrong credentials or a missing CA file for port 8883. Mosquitto's log shows the reason.
  4. Messages rejected on the public broker. The topic exceeds eight levels or the client exceeds ten messages per second; use shorter prefixes and do not forward log events there.

Monitoring the Forwarding Path

A LoRaWAN pipeline has several places to fail silently: a gateway loses power, ChirpStack stops decoding after a codec edit, or the forwarder dies. Watch each layer separately. ChirpStack's web interface shows gateway last-seen times and per-device frames. The forwarder's retained status topic, backed by its last will, shows whether the forwarder itself is alive, and the bridge's notification topic does the same for Option A. Finally, the age of each device's retained telemetry message tells you whether data is still flowing end to end.

On the cloud side, the ConnectMQ live topic explorer is the quickest way to confirm that records are arriving with the expected shape after a codec change, and a threshold alert rule on a field such as battery_v gives early warning before a sensor goes quiet.

Production Notes

Keep the network server and gateway broker private, and make the forwarder or bridge the only path out. A fully managed MQTT service then becomes the shared hub for LoRaWAN data, Wi-Fi devices and controllers alike, and the MQTT broker in the cloud article compares hosting choices. For what to do with the data, the smart agriculture IoT guide and the automatic irrigation system build show practical uses, and the IoT automation guide covers the wider picture.

Forward ChirpStack Data to ConnectMQ

ConnectMQ gives your LoRaWAN data a private, TLS-secured MQTT broker with a live topic explorer, a dashboard and threshold alert rules for values like low battery or frost. Create a free Serverless broker at app.connectmq.com, no credit card required, and point your bridge or forwarder at it.

Frequently Asked Questions

application/<application_id>/device/<dev_eui>/event/up by default, with JSON payloads when json = true is set in the MQTT integration.

Can ChirpStack publish directly to a cloud broker?

You can point the integration's server at another broker, but keeping it local and forwarding selected topics gives you control over what leaves the network.

Why are ChirpStack payloads base64?

The data field carries raw LoRaWAN bytes encoded as base64. Add a codec to the device profile to get decoded values in the object field.

Does the Mosquitto bridge buffer messages when the internet is down?

With cleansession false and QoS 1, Mosquitto can queue messages for the bridge while the link is down, within its configured queue limits. With cleansession true, messages published during the outage are not forwarded.

Related articles

Zigbee2MQTT Setup Guide: From Coordinator to Dashboard

Install Zigbee2MQTT with Docker, configure the coordinator and MQTT connection, pair and rename devices, control them over MQTT and view their states on a dashboard, testing first on a free public broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.