MapexOS MapexOS docs
Architecture / MQTT Broker

MQTT Broker

The MQTT Broker is how devices that speak MQTT get into MapexOS. It is not a MapexOS microservice — it is Eclipse Mosquitto 2.0 carrying a single in-house Go plugin (cgo) that turns the MQTT edge into a source of structured NATS events. Mosquitto handles the MQTT protocol; the plugin handles MapexOS: it authenticates every connection locally, authorizes every topic, and forwards every authorized message onto NATS — where JS Execution and the Assets health monitor pick it up.

It is the broker side of the rule you saw in Assets: Assets authors the credentials; the edge decides the connection. This is that edge.

Packaged as the Docker image mapexos/mapex-broker-mqtt (operators pull, never build) · Mosquitto 2.0.x + a cgo plugin (libmosquitto + OpenSSL) · listeners 1883 (TCP) / 8883 (mTLS, auto-detected) · Pebble (L1) + MinIO (L2) + NATS Core (no JetStream produced here).


One plugin, four hooks

The plugin registers exactly four Mosquitto callbacks — the whole MapexOS behavior hangs off these:

Hook What the plugin does
BASIC_AUTH Authenticate the connection against the Assets auth projection.
ACL_CHECK Authorize every PUBLISH / SUBSCRIBE by topic.
MESSAGE Forward every authorized publish onto NATS.
DISCONNECT Emit a presence advisory.

Mosquitto 2.0.x exposes no CONNECT hook, so a device coming online is derived from the auth-success edge — the broker never needs a separate connect signal.


Zero-HTTP auth on the warm path

Every connection is authenticated locally — there is no per-connection call back to Assets. The plugin resolves the device's credentials through a three-tier, self-healing cache:

 CONNECT → L1 Pebble (embedded KV on disk, survives restarts)
              ↓ miss
           L2 MinIO   (mapex-asset-auth/{assetUUID}.json — the projection Assets writes)
              ↓ miss
           L3 HTTP    (GET /internal/asset_auth/:assetUUID → Assets)

Every L2 or L3 hit warms L1, so the warm path is pure local disk. A full miss denies (default-deny); a total store outage fails closed. L1 carries a TTL safety net (default 30 min), and a FANOUT consumer drops stale L1 entries the instant Assets changes a device — so the broker authenticates fast and never on stale credentials.

The credentials themselves come straight from the Assets auth projection's mqtt block. The broker enforces the two mutually-exclusive modes Assets declares:

  • password — a local bcrypt compare against passwordHash (run on the broker thread, no HTTP),
  • cert — equality of the client certificate's serial against currentCertSerial.

A password-mode asset presenting a cert is denied, and vice-versa; an unknown auth type is denied.


A device can only touch its own topics

Authorization is a pure-Go, allocation-free check on a strict contract. The MQTT username is the bare assetUUID (globally unique), and a device may use exactly two topic shapes:

Topic Direction
events/{assetUUID}/{eventType} the device publishes telemetry
commands/{assetUUID}/{commandType} the device subscribes to commands

The decisive rule: the assetUUID token in the topic must equal the username. One device physically cannot publish to or read another device's topics — cross-asset access is denied at the broker, in sub-microsecond string comparison, before anything reaches NATS.


Liveness over correctness — the async publisher

An authorized publish must never let a slow backend stall a device. So the plugin hands each message to a bounded async publisher: a non-blocking enqueue onto a fixed-size channel, drained by a worker pool that publishes to NATS. If the queue is full, the message is dropped and counted — never blocked, never retried inline. A slow or unreachable NATS degrades into counted drops, while MQTT clients keep connecting and publishing unaffected. Every payload is copied across the cgo boundary before it reaches a worker, so the broker's own buffers are never touched off-thread.


What it emits — raw bytes, structured subjects

The broker does not decode or transform payloads — it forwards the raw MQTT bytes and lets JS Execution normalize them. It publishes two NATS streams (Core, fire-and-forget):

Signal Subject Consumed by
Ingress mqtt.data.{orgId}.{assetUUID} JS Execution (subscribes mqtt.data.>)
Presence mqtt.presence.advisory (connect / disconnect) the Assets health monitor

Before publishing, ingress applies safety invariants — it drops subject tokens that are illegal on NATS and payloads larger than ~900 KiB, each as a counted warning rather than a malformed event.

Consumes: fanout.asset.invalidate (from Assets) to evict stale L1 entries. The broker runs no HTTP server of its own — it is a plugin, not a service.


Where to go next

To dive into… Go to
Who authors the credentials it reads Assets
What normalizes the bytes it forwards JS Execution
The LoRaWAN counterpart at the edge LNS