tools/mqtt-bridge/. Everything below uses the reference deployment under
tools/mqtt-bridge/deploy/.
This is the per-tool route. If you want the whole stack (Convex, GCS,
broker, bridge, and video relay) on one host, the
all-in-one stack wires the same
broker and bridge for you.
Prerequisites
- A self-hosted Convex backend already running. The bridge POSTs to its
site origin (
:3211), not the client API origin (:3210). See Mission Control and Convex for the split. - Docker and Docker Compose on the host.
- The
mosquitto_passwdtool, which ships inside theeclipse-mosquittoimage, so you do not need it on the host directly.
The compose stack
The referencetools/mqtt-bridge/deploy/docker-compose.yml runs three
services:
| Service | Image | Purpose |
|---|---|---|
mosquitto | eclipse-mosquitto:2 | The MQTT broker. Listens on 1883 (TCP) and 9001 (WebSocket). |
bridge | built from deploy/Dockerfile | Subscribes to telemetry topics and POSTs to Convex. |
cloudflared | cloudflare/cloudflared:latest | Optional. Exposes the WebSocket listener to browsers across networks. |
bridge service builds from the repository source (a small
TypeScript program with one dependency, mqtt). The cloudflared
service is only needed if browsers will reach the broker from outside the
LAN; on a single-LAN setup you can drop it.
The ports: 1883 and 9001
Mosquitto runs two listeners, and they serve two different clients:- TCP 1883 is plain MQTT. The drone agent and the bridge connect here. The agent’s MQTT client speaks native MQTT over TCP.
- WebSocket 9001 is MQTT over WebSockets (
protocol websocketsinmosquitto.conf). The Mission Control browser connects here, because a browser cannot open a raw TCP socket. When the GCS is served over HTTPS, it connects withwss://, which thecloudflaredtunnel terminates in front of the 9001 listener.
Configure the environment
Copy the example file and fill in your values:| Variable | Default | Description |
|---|---|---|
MQTT_BROKER_URL | mqtt://localhost:1883 | The broker address. Inside the compose network this is mqtt://mosquitto:1883. |
MQTT_USERNAME | (none) | The bridge service account username. Use ados. |
MQTT_PASSWORD | (none) | The bridge service account password. Set a strong value. |
CONVEX_URL | (required) | Your Convex site origin. The bridge POSTs to ${CONVEX_URL}/agent/status. |
.env looks like this. Use placeholders for your own host:
CLOUDFLARE_TUNNEL_TOKEN is only consumed by the optional cloudflared
service. Leave it as the placeholder (or remove the service) if you are
not tunneling.
Broker authentication
The referencemosquitto.conf ships with authentication on:
anonymous access is disabled, and the broker checks a password file and a
topic ACL. Three principals connect:
| Principal | Username | Permissions |
|---|---|---|
| Drone agent | its deviceId | read and write its own subtree, ados/<deviceId>/# |
| Bridge service account | ados | read everything under ados/# |
| GCS browser session | the viewer account | read across ados/+/# |
acl.conf) is deny-by-default. The agent’s MQTT password is the
same apiKey it received during pairing, the value it also sends as the
X-ADOS-Key HTTP header. So a device’s broker credential and its HTTP
credential are one and the same.
Create the password file
On first deploy, thepasswd file does not exist yet. Bring the broker
up, then create it. Add the bridge service account and one entry per
paired device:
Create the file with the first entry
The Use the same password here that you put in
-c flag creates a new file. mosquitto_passwd prompts for the
password.MQTT_PASSWORD.Append each paired device
For every paired drone, add its
deviceId as the username and its
apiKey as the password. Both are on the cmd_drones table in your
Convex deployment after pairing. Drop the -c flag so the file is
appended, not overwritten.When you pair a new device
The broker will not let an agent connect until its(deviceId, apiKey) pair lands in passwd. After a new drone finishes
pairing, append it with mosquitto_passwd and SIGHUP the broker, the
same two steps as above. If you rotate a device’s apiKey (by
re-pairing), update its passwd entry before the agent reconnects.
Bench mode (no auth)
For a local bench or CI fixtures,mosquitto.conf carries a commented
bench block that turns anonymous access on. It is for local use only.
Never run an internet-facing broker with allow_anonymous true.
The topics
The agent publishes underados/<deviceId>/.... The bridge subscribes to
exactly two topic patterns:
| Topic | Carries |
|---|---|
ados/{deviceId}/status | Agent status: online state, version, uptime |
ados/{deviceId}/telemetry | Flight telemetry: position, attitude, battery |
deviceId from the topic, parses the JSON body, and POSTs
{ deviceId, topic, ...payload } to ${CONVEX_URL}/agent/status. To
protect the backend from telemetry bursts, the bridge debounces per
device at 3 seconds: it forwards at most one message per device every 3
seconds. Each POST has a 5-second timeout, and a failed POST is logged
and dropped, never retried, so a slow backend cannot back up the bridge.
Verify it works
Once the stack is up andpasswd is populated, confirm each principal
behaves correctly. Run these inside the broker container:
Connected to MQTT broker and
Subscribed to ados/+/status plus ados/+/telemetry on startup. If you
see repeated Convex POST failed lines, recheck CONVEX_URL against the
warning above.
Where to go next
Mission Control and Convex
Stand up the backend the bridge POSTs to.
Video relay
Add live video alongside the telemetry layer.
All-in-one stack
Run the broker and bridge as part of the one-host stack.
Troubleshooting and ports
The full port table and common failure modes.