mod_ros2) reads to shape what it forwards and how.
Three console screens cover the bridge:
Policies are durable: they live in the
mqtt_topic_policy table in
Supabase behind row-level security, scoped to your orgId. Client and
topic state is live broker state published by mod_mqtt through Redis —
it is not history, and it disappears when the broker stops reporting it.
What the bridge forwards
Nothing is forwarded because a device published it. Forwarding is driven by the topic policy list. Each policy is a topic filter plus three delivery settings and an enable flag:
A policy with
enabled unchecked stays in the list and keeps its
status, but is not applied. Use it to park a rule rather than deleting
and retyping the pattern.
Topic filter syntax
The pattern field uses standard MQTT subscription syntax, and the console validates it against the same rules the broker applies to subscriptions before it will let you save:+matches exactly one level, so it must occupy a whole level.robots/+/telemetryis valid;robots/rob+/telemetryis rejected.#matches the rest of the tree, so it must also occupy a whole level —robots/+/telemetry/#, notrobots/telemetry#.#may only appear as the final level. Because it already matches everything below it, a#with levels after it is rejected.
QoS preference
Theqos field is the delivery guarantee the bridge prefers for topics
matching the pattern:
Exactly-once delivery costs a four-way handshake per message on the MQTT
side, so reserve
0/1/2 for topics where a duplicate would actually
cause harm — commands and state transitions rather than telemetry.
Retained handling: cache, pass, drop
MQTT lets a publisher mark a message retained, so that the broker serves the last value to any later subscriber. Theretained field says
what the bridge does with those payloads:
cache is what you want for slow-changing state that a late joiner
needs immediately (a configuration value, a mode, a last-known pose).
pass suits streaming telemetry, where the previous sample has no value
to a new subscriber. drop is for publishers that set the retained flag
indiscriminately and would otherwise fill the retained store.
ACL inheritance and override
Theacl field resolves against your workspace ACL configuration:
inherit— the pattern uses the workspace default publisher and subscriber lists from your ACL config. This is the default.override— this rule carries its own publisher / subscriber list, and the workspace defaults do not apply to matching topics.
inherit unless a pattern genuinely needs a narrower or
wider audience than the rest of the namespace. Overrides are the part of
this table that is easiest to forget about later.
Policy status: active, pending, error
Every row carries a server-reportedstatus. The console renders and
filters on it; it never sets it.
The filter chips above the table (All rules, Active, Pending,
Error) narrow the list and show a count per status, which is the
quickest way to find a rule that failed to land in a long list.
Two different failures surface in two different places:
- Rejected at save time. A missing or malformed pattern is caught in the console before the mutation is sent. Anything the server rejects — a conflicting pattern, a permission problem — comes back as the mutation’s error message, rendered inline next to the Save policy button. Nothing is written.
- Stored but not applied. The row is created and then shows
error. The policy exists in Supabase; the engine did not take it. Re-saving the row withupsertis the retry path.
pending has been written to Supabase but the engine
has not acknowledged it. The policy list does not poll — it refetches
only after your own create or delete — so reload the screen before
concluding that a rule is stuck.
Deleting asks for confirmation and then removes the row by id. There
is no undo; the pattern has to be retyped.
What the client list reports
The MQTT clients screen pollsmqttClients.list every 10 seconds and
renders one row per connected client. Today the procedure reports three
things per client:
The KPI strip above the table derives Connected from the row count.
Everything else in the strip reads
— for the reason described below.
Fields that are not reported yet
The client table keeps columns for fields the broker does not yet expose through this procedure. They render blank or as— rather than being
filled with assumed protocol defaults, which would be worse than an
empty cell:
- Auth (authenticated username)
- Source IP
- MQTT (protocol version — 5 or 3.1.1)
- QoS pref
- Retained (per-client retained topic count)
- Hz (recent publish rate)
- Per-row connection status (
active/idle/reconnecting) - The Inflight QoS 1/2 KPI, which is not wired to a source at all
— and
are labelled not reported. If you need to attribute traffic to a
source address or an authenticated user, the client list is not the
place to do it today.
Forcing a client off
Select one or more rows — click anywhere on a row, or use its checkbox — and use Disconnect selected in the page header. The button stays disabled until at least one row is selected. This callsmqttClients.disconnect with your orgId and the selected
client ids. It enqueues a kick; the engine drops those clients on its
next tick. The console then clears the selection and refetches the list,
so a successful kick shows up as the row disappearing on the next
render.
Two consequences of the procedure’s shape are worth knowing before you
use it:
- The mutation takes only
orgIdandclientIds. There is no ban duration, blocklist, or reason parameter — a disconnect is a one-time drop, not a revocation. Nothing in this screen stops the same client id from connecting again immediately. - Because Inflight QoS 1/2 is not reported, the console cannot tell you what was in flight for a client at the moment you kicked it. Do not use this screen to reason about message loss; use it to clear a client you know is misbehaving.
The topic namespace view
The Topics screen pollsmqttTopics.list every 10 seconds and receives
a flat list of topic strings. Its only metric is Distinct topics,
the number of entries after filtering.
The search box in the header does a case-insensitive substring match
over that flat list before the tree is built, so filtering to
fleet/ collapses the tree to just that subtree. The chips below the
KPI strip (Retained, QoS 2 active, High rate, No
subscribers) are placeholders for filters the flat topic list cannot
yet support; they are not wired to a handler.
How the topic tree is built
The tree is derived entirely in the browser. Each topic string is split on/ and the segments are merged into a nested structure, so
fleet/rob-01/telemetry and fleet/rob-01/pose render as one fleet
node with one rob-01 child and two leaves. Empty segments are skipped.
That means the tree shows shape, not identity: an intermediate node
like rob-01 exists because something below it was published, and does
not imply a topic at that level. Node metadata renders as —, and the
retained and subscription chips have no data source in this view.
Counters that read as a dash
Messages / s, Retained set and Bytes / s are all— on the
Topics screen. Rate, byte and retained counters are not tracked by the
bridge subset that backs this view. The same applies to per-node
metadata in the tree.
Nothing in the console derives these numbers, and nothing caches a
previous value — a — means “not measured”, never “measured as zero”.
For quantitative traffic questions, go to the mesh-side telemetry
instead of this screen.
Related: the topic list is a live snapshot. The console keeps no
client-side history, so a topic stays listed for exactly as long as
mqttTopics.list keeps returning it, and vanishes on the first poll
that omits it.
Troubleshooting an empty topic tree
An empty tree renders as “Your topic tree is empty — no devices are publishing yet.” Work through it in this order:- Check the filter box. The empty state also appears when the substring filter matches nothing. Clear it first.
- Check the clients screen. If no clients are listed, this is a connectivity or credentials problem at the broker, not a bridge problem. The topic list cannot contain anything if nothing is connected.
- Check that clients are connected but subscribing only. A row’s last-activity cell shows a subscription count. Subscribers alone do not populate the topic namespace; something has to publish.
- Check your policy statuses. Filter Topic policies by Error and Pending. A pattern that never reached the engine forwards nothing.
- Check the pattern actually matches.
robots/+/telemetrymatches exactly one level where the+sits — it will not matchrobots/rob-01/left/telemetry. Use a trailing#when the depth below a level varies. - Check
enabled. A disabled policy still renders with its status chip, which makes it easy to read a parked rule as a live one.
Related
- Authentication — API keys and relay tokens for the control and data planes
- Telemetry — metrics, traces, and packet capture streams

