stem-mqtt
An MQTT 3.1.1 / 5.0 client and broker written in Rust — async (tokio), TLS, WebSocket,
QoS 0/1/2, shared subscriptions, enhanced authentication — usable as a Rust library, as a
standalone broker binary, and from Kotlin, Swift, Python, Go, C#, Java, Dart, Node.js and
Haskell through generated UniFFI bindings. Client and
broker ship in every language, one generator per language, each runtime-tested against a real
broker in CI.
| Crate | Version | Downloads | Docs |
|---|---|---|---|
stem-mqtt-client |
|||
stem-mqtt-broker |
Quick start (Rust)
An embedded broker and a client talking to each other:
use ;
use ;
async
Just want a broker? cargo install stem-mqtt-broker && mqtt-broker --port 1883
(see Running the broker).
Runnable examples (the mqtt-client ones need a broker already running):
Full crate docs: crates/mqtt-client,
crates/mqtt-broker.
Preview
Publish / subscribe

The mqtt-broker CLI, then the pub_sub example against it: a full publish/subscribe round trip.
MQTT 3.1.1 and 5.0 side by side

Two clients on different protocol versions share one broker, each publishing and both receiving both messages — the version is negotiated per connection, not per broker.
Topics and wildcards

An exact topic, a + single-level and a # multi-level filter watch the same topic tree; four publishes show which filter catches which message.
Shared subscriptions

Three workers join $share/workers/jobs/# and split nine jobs round-robin, three each, while an ordinary subscriber on the same topics still sees all nine.
Retained messages and Last Will

A subscriber that arrives after a retained publish still receives it; when a client vanishes without DISCONNECT, the broker publishes its will.
Authentication

A pluggable auth provider rejects two wrong logins and accepts the right one; the broker's own log then shows every decision and connection.
Long-lived connection

One connection held open across several keep-alive intervals with a heartbeat every two seconds — the shape of a background service or IoT device.
Recorded with VHS: just vhs-tape <name> or just vhs-all
(tapes in examples/vhs; each tape runs the example of the same name).
Installation
Every published package is prefixed stem-mqtt- (mqtt-client / mqtt-broker are taken by unrelated
projects on crates.io and PyPI); import names in code are unchanged. A package is available once a
release has published it to that registry.
| Language | Install | Import |
|---|---|---|
| Rust | cargo add stem-mqtt-client stem-mqtt-broker |
use mqtt_client::…; use mqtt_broker::…; |
| Python | pip install stem-mqtt-client stem-mqtt-broker |
import mqtt_client, mqtt_broker |
| Node.js | npm i stem-mqtt |
stem-mqtt/client · stem-mqtt/broker |
| Kotlin (JVM/Android) | io.github.sorinirimies.stemmqtt:stem-mqtt-kotlin (Maven Central) |
import uniffi.mqtt_client.* |
| Java (JDK 22+) | io.github.sorinirimies.stemmqtt:stem-mqtt-java (Maven Central) |
import uniffi.mqtt_client.*; |
| C# | dotnet add package StemMqtt |
using uniffi.mqtt_client; |
| Go | go get github.com/sorinirimies/stem-mqtt-go |
…/stem-mqtt-go/mqtt_client |
| Dart | dart pub add stem_mqtt |
package:stem_mqtt/mqtt_client.dart |
| Haskell | cabal install stem-mqtt |
import UniFFI.MqttClient |
| Swift | StemMqttSwift-<version>.zip from the GitHub Release |
import MqttClient |
Every language gets the same two components — a client and a broker.
Detailed guides — install, requirements, a complete example, client and broker API, authentication, TLS,
language-specific notes, building from source and troubleshooting — live in
docs/languages/:
| Language | Guide | Notes |
|---|---|---|
| Rust | rust.md | tokio; the quick start above |
| Python | python.md | asyncio; wheels bundle the native library |
| Node.js | node.md | ESM, server-side; Promises |
| Kotlin | kotlin.md | coroutines; JVM and Android |
| Java | java.md | JDK 22+ (FFM); CompletableFuture |
| C# | csharp.md | async/await; .NET 8+ |
| Go | go.md | cgo; install the native libraries separately |
| Dart | dart.md | pull-style API; needs cargo to build |
| Haskell | haskell.md | pull-style API; needs cargo to build |
| Swift | swift.md | async/await; XCFrameworks for Apple platforms |
Callbacks are available everywhere except Dart and Haskell, which use the pull-style API.
Ruby isn't supported: UniFFI has no official Ruby backend and no production-ready generator exists (
packaging/ruby).
Features
- MQTT 3.1.1 and MQTT 5.0 — one codec, version negotiated per connection
(
MqttVersion::V311/MqttVersion::V5). - QoS 0, 1, 2 — at-most-once, at-least-once, and the full exactly-once four-part handshake (PUBLISH → PUBREC → PUBREL → PUBCOMP), with real redelivery (DUP=1 resend on timeout) on both the client and broker side — not just a single fire-and-hope attempt.
- Wire codec for all control packet types — CONNECT/CONNACK, PUBLISH and its ack chain, SUBSCRIBE/SUBACK, UNSUBSCRIBE/UNSUBACK, PING, DISCONNECT, and MQTT 5 AUTH — with the enhanced-authentication exchange built on top (see Authentication below).
- TLS (client + broker,
mqtt-client::TlsOptions/mqtt-broker::BrokerTlsConfig) — pure-Rustrustls, custom CA support, and mutual TLS (mTLS) client-certificate verification. No OpenSSL/system- TLS dependency. - Opt-in client auto-reconnect (
ConnectOptions::auto_reconnect) with exponential backoff, replaying every topic the client was subscribed to once reconnected. - Last Will and Testament, retained messages, keep-alive pings.
- MQTT-over-WebSocket (
--ws-port) alongside raw TCP, so browser clients (which can't open raw TCP sockets) can connect directly — seedemo/for a full browser pub/sub demo webpage. - Authentication — pluggable username/password (
MqttAuthProvider) and MQTT 5 enhanced authentication (multi-round challenge/response: SCRAM, Kerberos, OAuth, …) viaMqttEnhancedAuthProvideron the broker andMqttAuthHandleron the client. - Shared subscriptions (
$share/<group>/<filter>) — one delivery per group, round-robin — routed through a topic-trie subscription index instead of a per-publish scan of every session. - Session expiry (
session_expiry_secs) for persistent sessions that never return. - Event observation (
MqttBrokerEventListener) and, for runtimes that can't receive callbacks (Dart, Haskell), a pull-style API:enable_message_queue/next_messageon the client,enable_event_queue/next_eventon the broker. - UniFFI bindings — both crates build as
cdylib/staticlib(cargo rustc --lib --crate-type cdylib,staticlib) and ship auniffi-bindgenbinary for Kotlin, Swift and Python; Go, C#, Java, Dart, Node.js and Haskell are generated through pinned community generators — all of them runtime-tested in CI against a real broker. - Hardened by default — packet-size limits, bounded per-client send queues,
keep-alive timeout detection, credential-redacting
Debug, foreign callbacks that can't crash I/O tasks, and a decoder that is fuzzed (randomised tests in every build +cargo-fuzztargets infuzz/) — seeCHANGELOG.md.
Running the broker
| Option | Meaning |
|---|---|
--bind, --port |
TCP listener (default 0.0.0.0:1883) |
--ws-port |
also accept MQTT-over-WebSocket (browser clients) |
--allow-anonymous |
accept clients without credentials |
--max-clients |
connection limit (0 = unlimited) |
--log-level |
error … trace (default info) |
Pre-built Linux and Windows mqtt-broker binaries are attached to every release.
Demos
demo/README.md covers the plain-binaries path; packaging/k8s
has a one-Pod and a Deployment + Service Kubernetes demo.
Workspace layout
| Path | Description |
|---|---|
crates/mqtt-client |
Async, tokio-based MQTT client; also hosts the wire-protocol codec (mqtt_client::protocol) shared with the broker. |
crates/mqtt-broker |
Standalone broker (library + mqtt-broker CLI), reusing the client's codec. |
demo/ |
Browser client (web/) and a Rust + Topcoat dashboard (dashboard/). |
tests/bindings |
One end-to-end smoke test per language, run by scripts/test_bindings.nu. |
packaging/ |
Per-language package templates, Dockerfile and Kubernetes manifests. |
scripts/ |
Nushell scripts for bindings, packaging, publishing and releases (tested in scripts/tests). |
fuzz/ |
cargo-fuzz targets: packet decoder, encode/decode round-trip, chunked reads, varint/strings, PUBLISH, topic matching (just fuzz <target>). |
There is deliberately no separate "core" crate: mqtt-client::protocol is a pure, synchronous codec with
no networking or async dependencies, so it is the shared foundation for both client and broker.
Generating foreign-language bindings
Everything is driven by Nushell scripts; the generator
table (languages, pinned generator versions, required UniFFI release, registry)
lives in one file, scripts/bindings/spec.nu.
| Language | Generator | Runtime-tested | Status |
|---|---|---|---|
| Kotlin | UniFFI (built in) | Gradle packaging test in CI | stable |
| Swift | UniFFI (built in) | ✅ client + broker + all callbacks | stable |
| Python | UniFFI (built in) | ✅ client + broker + all callbacks | stable |
| Go | uniffi-bindgen-go | ✅ client + broker + all callbacks | stable |
| C# | uniffi-bindgen-cs | ✅ client + broker + all callbacks | stable |
| Java (JDK 22+) | uniffi-bindgen-java | ✅ client + broker + all callbacks | stable |
| Dart | uniffi-dart | ✅ client + broker through the pull-style API ¹ | experimental |
| Node.js | uniffi-bindgen-node-js | ✅ client + broker + all callbacks | experimental |
| Haskell | uniffi-bindgen-haskell + our patch (PR #3) | ✅ client + broker through the pull-style API ¹ | experimental |
¹ Dart: foreign callbacks cannot be invoked from Rust's own threads (the VM aborts). Haskell: callback
interfaces are exposed only as opaque handles. So MqttMessageListener, MqttAuthProvider,
MqttBrokerEventListener and the enhanced-auth callbacks can't be implemented there — use the
pull-style API that exists for exactly this: enable_message_queue / next_message(timeout_ms) on the
client and enable_event_queue / next_event(timeout_ms) on the broker. Their smoke tests receive
messages and observe broker events that way. Authentication uses the pull-style auth queue (enable_auth_queue / next_auth_request / resolve_auth_request).
² Upstream's Haskell generator had three bugs that made every binding unusable: constructors never
lowered their arguments (generated code didn't compile), flat-error variants dropped their message
(decoder failed with "left N trailing bytes"), and records sharing a field name broke the public module.
packaging/haskell/uniffi-bindgen-haskell.patch
fixes all three (and is proposed upstream, see packaging/haskell); install_bindgens.nu builds the generator from the pinned revision with the patch applied.
One solution per language, client and broker everywhere. Each language has exactly one generator
(no parallel hand-written addons), and each component is self-contained: the broker owns its own QoS
enum instead of importing the client's, so no generator ever meets a cross-crate "external type" (the
Node generator rejects them) and every language gets a standalone broker package.
UniFFI bindgens can only read metadata from the UniFFI release they were built
for, and the third-party generators disagree (most target 0.31, Haskell 0.32). generate_bindings.nu therefore builds the library for
such a generator in a scratch workspace under target/uniffi-<version>/ pinned
to the right release; your workspace is never touched.
Do the bindings actually work?
For each language this builds the native libraries, generates the bindings, stages a
scratch project with the smoke test from tests/bindings/<language>/ and runs
it against real sockets: start a broker, subscribe, publish QoS 1, receive it through a foreign
callback, refuse a bad client through a foreign auth callback, observe connections through a
foreign event listener, stop. Languages that can't do callbacks run a reduced scenario (see the
header of each test). Verdicts: PASS, FAIL, SKIP (toolchain missing). CI runs the same command for every
language on every push, and each package is built and installed the way a user would before it is released.
Maintainer docs: development · publishing.
License
MIT — see LICENSE.