AdaTrack v1.3: Native MQTT Support Without the Broker Sprawl

  • April 22, 2026
  • AdaTrack Team
AdaTrack v1.3: Native MQTT Support Without the Broker Sprawl

AdaTrack v1.3: Native MQTT Support Without the Broker Sprawl

Published: | Category: Engineering

UDP is an excellent fit for the majority of IoT telemetry workloads: stateless fire-and-forget transmission, minimal per-packet overhead, and zero connection-management cost on resource-constrained hardware. AdaTrack has processed billions of readings over UDP since launch, and that will not change. But UDP is the wrong tool for a growing class of devices — those that need guaranteed delivery, that sit behind NAT or restrictive firewalls, or that require bidirectional communication with the platform. Until now, connecting those devices required either running an external MQTT broker alongside AdaTrack or accepting that some hardware simply could not participate. AdaTrack v1.3 removes that constraint by embedding a full MQTT broker directly into the platform as a compiled plugin.

Embedded, Not Bolted On

The MQTT broker in v1.3 is not a sidecar container, a managed cloud service, or a subprocess wrapped in a shell script. It is a compiled Go plugin — mqtt_broker — that initialises when the plugin is enabled and tears down when it is disabled, sharing the same process, the same memory space, and the same dependency graph as the rest of the platform.

The implementation is built on mochi-mqtt/server v2, a pure-Go, MIT-licensed MQTT v5 and v3.1.1 compliant broker designed specifically for embedding. It introduces no CGo, no external runtime dependencies, and no additional system services to manage. The broker starts and stops with Start() and Stop() lifecycle calls on the plugin struct — the same interface every AdaTrack plugin implements.

This design is a direct consequence of our modular-monolith architecture. We deliberately avoided the temptation to extract MQTT into a separate microservice — doing so would have introduced serialisation overhead on the hot path, complicated deployment for self-hosted operators, and created a new failure domain to reason about. A single binary that does everything is operationally simpler, and for an IoT platform where uptime directly determines data fidelity, simplicity is a competitive advantage.

Transport Selection is Per-Device

Introducing MQTT does not require migrating an entire fleet. Each device now carries a transport_type field — udp or mqtt — set in the device form and stored in the devices table with a DEFAULT 'udp' constraint. Existing deployments are completely unaffected: no configuration changes, no re-registration, no decoder rewrites. You opt individual devices into MQTT as their hardware warrants.

Authentication: Connection-Scoped, Credential-Reusing

One of the less obvious design decisions in the MQTT integration is the choice to reuse existing device credentials rather than introduce a new credential type. Every AdaTrack device already has a Device UUID and a Device Secret — a 32-byte key used for HMAC-SHA256 packet signing over UDP. For MQTT, the device presents its UUID as both the MQTT ClientID and username, and the hex-encoded Device Secret as the password.

Authentication is connection-scoped rather than per-packet. When a device establishes a TCP session, the broker's OnConnectAuthenticate hook fetches the device's secret from the repository (with LRU cache) and performs a constant-time comparison against the provided password using subtle.ConstantTimeCompare — the same timing-safe primitive used throughout our HMAC verification path. Once the session is established, all messages on that connection are trusted without re-signing. This is appropriate because TLS provides the transport-layer integrity that HMAC provides for raw UDP.

A strict per-device ACL enforces topic isolation at the broker level. The OnACLCheck hook allows each device to publish only to data/{device_id} and subscribe only to commands/{device_id}. The broker's internal publish client — identified by cl.Net.Inline == true — bypasses ACL entirely, which is how the platform delivers downlink commands without the ACL blocking them. This separation between the device-facing ACL and the internal broker client is a clean and robust boundary.

One Pipeline, Two On-Ramps

The most consequential architectural choice in the MQTT plugin is where it connects to the existing platform. When a device publishes a payload to data/{device_id}, the broker's OnPublish message hook calls host.SubmitPacket(ctx, deviceID, rawPayload) — a new method on the PluginHost interface backed by a new PacketIngestor abstraction. This injects the raw payload directly into PacketProcessor.IngestRawPayload(), which forwards it to processAuthenticated() — the same private method called by the UDP path after HMAC verification.

From that point, MQTT and UDP telemetry are indistinguishable. Both go through:

  • Quota enforcement against the device's subscription tier
  • Device and profile lookup from the LRU cache, with PostgreSQL fallback
  • JavaScript decoder execution in the Goja VM pool
  • Location, RSSI, and SNR extraction from the decoded payload
  • Batch insert into the TimescaleDB readings hypertable via pgx
  • PostGIS geofence evaluation for enter/exit triggers
  • Alert rule evaluation across the active rule set
  • Workflow trigger evaluation
  • PostgreSQL LISTEN/NOTIFY → WebSocket hub → Deck.gl map update

Existing device profiles and JavaScript decoders work with MQTT payloads without any modification. The payload format is identical: the same binary structure the UDP path expects. This was an intentional constraint — we did not want MQTT to become a second encoding dialect that operators would need to maintain separately.

Downlink Commands: Closing the Loop

The use case that UDP categorically cannot satisfy is sending instructions back to a device. With MQTT, this becomes a first-class platform feature. Operators can publish an arbitrary command payload to any connected device — from the device details panel in the UI or directly via POST /api/v1/plugins/mqtt_broker/downlink/{device_id}.

The downlink path uses the broker's internal publish client to write to commands/{device_id}, bypassing the ACL layer. Every command is logged with the sending user's identity, a timestamp, and a delivery status flag — giving operators an audit trail for everything sent to the fleet. Devices must subscribe to their commands topic at connect time to receive these payloads; the broker supports retained messages as an optional configuration to cache the last command value for devices that reconnect intermittently.

The broker admin page surfaces live operational metrics: connected client count updated over WebSocket, messages ingested and sent per minute, and a real-time session table showing each connected device's UUID, remote IP, and connection timestamp. Two new EventBus events — mqtt.device.connected and mqtt.device.disconnected — are emitted on session lifecycle changes, making them available as workflow and alert triggers alongside the existing platform events.

Plugin SDK Improvements That Made This Possible

The MQTT broker plugin required — and drove — three meaningful extensions to the platform's plugin SDK that benefit all plugin authors going forward.

The SubmitPacket / PacketIngestor interface is the core addition. Before v1.3, there was no way for a plugin to inject a pre-authenticated payload into the telemetry pipeline. Adding this required extracting processAuthenticated() from the UDP processor and exposing it through a clean interface that can be wired to any plugin at startup. This opens the door to future transport plugins — LoRaWAN bridges, CoAP handlers, proprietary radio protocols — without touching the core ingestion logic each time.

Public HTTP routes address a long-standing gap for plugins that need to receive inbound webhooks from external services. Previously, all plugin-declared routes required a valid JWT, which external callers such as payment processors, messaging platforms, or CI systems cannot provide. Plugin manifests now support a Public: true flag on any route declaration, mounting the handler at /api/v1/public/plugins/{id}/... instead of the authenticated subrouter. The chatbot plugin's Telegram webhook was the first consumer of this change.

Automatic RBAC permission lifecycle removes a manual step that had been a friction point since the plugin system launched. When a plugin is enabled, any permissions declared in its manifest are automatically inserted into the permissions table. When the plugin is uninstalled, they are removed. Operators no longer need to manually seed permissions rows when deploying plugins that define custom access controls.

Operational Considerations

The MQTT broker plugin is available on Standard tier and above for both SaaS and self-hosted deployments. Enable it from Admin > Plugins > MQTT Broker. Configuration options include the listen address (default 0.0.0.0:1883), maximum concurrent clients, and TLS — provide a PEM certificate and key path to accept connections on port 8883 alongside plain TCP on 1883.

For production deployments, TLS is strongly recommended. The Device Secret doubles as the MQTT password; transmitting it in cleartext over an untrusted network exposes the same secret used for UDP HMAC verification. A certificate from any trusted CA eliminates this risk without introducing per-device certificate management.

On the device side, the connection guide displayed after device creation provides ready-to-use snippets for Python (paho-mqtt), C (MQTT-C), and Arduino (PubSubClient). The credentials are the Device UUID and Device Secret — values every existing device already has. For devices migrating from UDP, no re-provisioning is required beyond updating the firmware to open an MQTT connection instead of a UDP socket.

v1.3 Is Available Now

The MQTT broker plugin, public plugin routes, and automatic RBAC seeding are all live in the current release. Self-hosted operators can upgrade with adatrack-ctl upgrade — the transport_type migration applies automatically and defaults all existing devices to UDP with no disruption. Read more about running AdaTrack in a self-hosted deployment.

Get Started

Key Highlights

  • Real-time Telemetry Ingestion via UDP
  • High Performance Time Series Storage
  • Advanced WebGL Powered Geospatial Visualization
  • Intelligent Geofencing and Alerting Engine

We are committed to providing cutting-edge solutions that help businesses run robust, scalable, and secure IoT systems.