data data flow
The data modality is “if you’d reach for MQTT, reach for this instead.” Hierarchical topics, MQTT-style + (one segment) and # (rest) wildcards, retained messages, and a choice of reliable or lossy delivery — all over the same QUIC connection and ClutchCall relay mesh you already use for voice, streams, games, and robotics. There is no separate broker to deploy, secure, or scale: the relay mesh is the fan-out. Use it for device telemetry, fleet state, config distribution, application event buses, presence, and any “many publishers, many subscribers, routed by topic” shape where you want typed pub/sub without operating a message broker.

At a glance

No broker

The relay mesh fans messages out. One operational story shared with every other modality — nothing extra to run.

MQTT-style topics

Hierarchical a/b/c topics with + (single segment) and # (multi-segment, trailing) filters — the semantics you already know from MQTT 3.1.1.

Two lanes

A lossy QUIC datagram lane for high-rate telemetry, a reliable ordered lane for events that must arrive. Pick per message.

Retained messages

Publish current state once; every late-joining subscriber gets the last retained value on attach.

Wire model

A Data client binds to a stable clientId and speaks hierarchical topics. Each published message carries a small header — the publisher’s clientId and the full topic — in front of your opaque payload, so subscribers can both filter and attribute messages without standing up a track per publisher.
[u8 from_len][from_client_id][u8 topic_len][topic][payload]
Both header fields are bounded at 255 bytes; the payload is arbitrary opaque bytes (Uint8Array / bytes). On the substrate, the SDK maps one MoQT track per top-level topic segment under the data/<segment> namespace:
sensors/room1/temperature   →   track  data/sensors
home/lights/kitchen/level   →   track  data/home
events/orders/shipped       →   track  data/events
So all sensors/* traffic shares one track; all home/* traffic shares another. The publisher sends the full topic in the frame header, and the SDK filters the rest of the path MQTT-style on the subscriber side. The MoQT capability tag for the track is data.pubsub.
The top-level segment of a filter must be concretesensors/+/temp is fine, +/room1/temp and # are not. The top-level segment selects the track; a wildcard there would mean subscribing to every namespace at once. Pick a concrete first segment and wildcard below it.

Topic filters

Same semantics as MQTT 3.1.1:
FilterMatches
sensors/room1/tempexactly that topic
sensors/+/tempsensors/<any one segment>/temp
sensors/#sensors and everything below it, any depth
sensors/room1/#sensors/room1 and everything under it
events/+/+any two segments after events
Rules, matching MQTT:
  • + matches exactly one path segment.
  • # matches the rest of the path and must be the final segment of the filter.
  • An exact topic string matches itself.

Lanes and QoS

Every publish picks a lane:
reliable: false (the default) rides the QUIC datagram lane: lossy, unordered, lowest latency, no head-of-line blocking. This is the right choice for high-rate sensor readings and any signal where the freshest value matters more than every value arriving.
Under the hood, reliable and retained messages are sent at an elevated priority on the stream lane; best-effort telemetry stays on the datagram lane. A subscriber can tell a retained bootstrap message from a live one via the retained flag on each DataMessage.

Retained messages

A message published with retained: true is cached by the relay, keyed on (namespace, topic). Any subscriber whose filter matches that topic receives the last retained value immediately on attach, before any live messages. This is the canonical “current state” pattern:
// Device announces it is online — sticks until replaced or cleared.
await data.publish({
  topic:    "devices/device-7/state",
  payload:  new TextEncoder().encode(JSON.stringify({ online: true })),
  retained: true,
});
A later subscriber to devices/device-7/state (or devices/device-7/#) gets that payload on attach. Publish a zero-length payload with retained: true to clear a retained topic — typically on clean shutdown, so a dashboard reads “offline” without waiting for a heartbeat timeout.
Pair a retained .../state topic (sticky current state) with a lossy .../telemetry topic (live readings). Subscribers get the snapshot instantly, then track the live stream.

Per-client demux

Because every message carries the publisher’s fromClientId, a single subscriber can fan in from a whole fleet and still know who sent what — no track per device. This is the “thousands of devices report to one dashboard” shape, handled by one subscription and one filter.

When to use it

Reach for data when…

You want topic-routed pub/sub, MQTT wildcards, retained state, or a typed event bus — and you do not want to run a broker.

Reach elsewhere when…

You need media tracks (use voice / streams), tick-rate game channels (use games), or ROS 2 / DDS QoS semantics (use robotics).

Why MoQT instead of a broker

  • No broker. The relay mesh is the fan-out. One thing to operate, shared with every other modality.
  • One auth path. The same tenant token that authorizes voice, streams, and games authorizes the data plane — the token’s claims carry the tenant and the clientId.
  • Native browser client. No MQTT-over-WebSocket bridge. The browser SDK speaks the same wire as your devices over WebTransport.
  • Mix with other modalities. A game client publishing match telemetry, or a voice app publishing call events, reuses its existing QUIC connection and token — no second stack.

Architecture

Underneath, data is just frames over MoQT multiplexed on one QUIC connection and fanned out by the relay mesh. The relay data plane runs an AF_XDP kernel-bypass fast path — packets move via eBPF/XSK programs over shared UMEM, zero-copy, with lock-free mcache / dcache rings on a shard-per-core (thread-per-core) reactor driven by io_uring. That is why adding the data modality to an existing app costs one more topic namespace, not one more server to run.