Skip to main content
The MQTT bridge is the real-time telemetry layer of the cloud relay. A Mosquitto broker receives the agent’s status and telemetry publishes, and a small Node bridge subscribes to those topics and POSTs them to your Convex backend so Mission Control can display live data. You only need this layer when you want telemetry faster than the 5-second Convex poll, or when the GCS and the drone are not on the same LAN.
The bridge and the broker live in the GCS repository at 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_passwd tool, which ships inside the eclipse-mosquitto image, so you do not need it on the host directly.

The compose stack

The reference tools/mqtt-bridge/deploy/docker-compose.yml runs three services: The 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 websockets in mosquitto.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 with wss://, which the cloudflared tunnel terminates in front of the 9001 listener.
So a typical flow has agents and the bridge on 1883, and browsers on 9001.

Configure the environment

Copy the example file and fill in your values:
The bridge reads four environment variables: A filled .env looks like this. Use placeholders for your own host:
CONVEX_URL must point at the Convex site origin (:3211), the one that serves HTTP actions. If you point it at the client API origin (:3210), the bridge starts cleanly but every POST fails, and no live telemetry ever reaches the GCS. The bridge exits immediately if CONVEX_URL is unset.
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 reference mosquitto.conf ships with authentication on: anonymous access is disabled, and the broker checks a password file and a topic ACL. Three principals connect: The ACL (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, the passwd file does not exist yet. Bring the broker up, then create it. Add the bridge service account and one entry per paired device:
1

Start the stack

2

Create the file with the first entry

The -c flag creates a new file. mosquitto_passwd prompts for the password.
Use the same password here that you put in MQTT_PASSWORD.
3

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.
4

Reload the broker

Mosquitto re-reads the password file on SIGHUP, so you do not need a full restart.
mosquitto_passwd prompts for each password interactively. To script it, pass -b and feed the password from a secrets manager. Never read passwords from shell history.

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 under ados/<deviceId>/.... The bridge subscribes to exactly two topic patterns: All payloads are JSON. For each message, the bridge extracts the 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 and passwd is populated, confirm each principal behaves correctly. Run these inside the broker container:
On the bridge side, the container logs each subscription and any failed Convex POST. Watch them with:
A healthy bridge logs 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.