# 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, from **Kotlin, Swift, Python, Ruby, Go, C#, Java, Dart, Node.js,
Haskell, C, C++, Zig and Lua** through generated [UniFFI](https://mozilla.github.io/uniffi-rs/) bindings, and from
**Elixir and Erlang** through a [Rustler](https://github.com/rusterlium/rustler) NIF. Client *and*
broker ship in every language, one generator per language, each runtime-tested against a real
broker in CI.
[](https://github.com/sorinirimies/stem-mqtt/actions/workflows/ci.yml)
[](https://github.com/sorinirimies/stem-mqtt/actions/workflows/security.yml)
[](https://github.com/sorinirimies/stem-mqtt/actions/workflows/fuzz.yml)
[](LICENSE)
[](Cargo.toml)
| `stem-mqtt-proto` | [](https://crates.io/crates/stem-mqtt-proto) | [](https://crates.io/crates/stem-mqtt-proto) | [](https://docs.rs/stem-mqtt-proto) |
| `stem-mqtt-client-blocking` | [](https://crates.io/crates/stem-mqtt-client-blocking) | [](https://crates.io/crates/stem-mqtt-client-blocking) | [](https://docs.rs/stem-mqtt-client-blocking) |
| `stem-mqtt-client` | [](https://crates.io/crates/stem-mqtt-client) | [](https://crates.io/crates/stem-mqtt-client) | [](https://docs.rs/stem-mqtt-client) |
| `stem-mqtt-broker` | [](https://crates.io/crates/stem-mqtt-broker) | [](https://crates.io/crates/stem-mqtt-broker) | [](https://docs.rs/stem-mqtt-broker) |
- [Quick start](#quick-start-rust) · [Installation](#installation) · [Features](#features) ·
[Running the broker](#running-the-broker) · [Other languages](#generating-foreign-language-bindings)
## Quick start (Rust)
```sh
cargo add stem-mqtt-client stem-mqtt-broker anyhow
cargo add tokio --features full
```
An embedded broker and a client talking to each other:
```rust,no_run
use mqtt_broker::{MqttBroker, MqttBrokerConfig};
use mqtt_client::{ConnectOptions, MqttClient, MqttVersion, QoS};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Broker on a free local port.
let broker = MqttBroker::new(MqttBrokerConfig::new("127.0.0.1", 0));
broker.start().await?;
let port = broker.bound_port().expect("bound");
// Client (MQTT 5; use MqttVersion::V311 for 3.1.1).
let client = MqttClient::new(ConnectOptions::new("127.0.0.1", port, "my-client-id", MqttVersion::V5));
client.connect().await?;
client.subscribe("stem/demo".into(), QoS::AtLeastOnce).await?;
client.publish("stem/demo".into(), b"hello".to_vec(), QoS::AtLeastOnce, false).await?;
client.disconnect().await?;
broker.stop().await?;
Ok(())
}
```
Just want a broker? `cargo install stem-mqtt-broker && mqtt-broker --port 1883`
(see [Running the broker](#running-the-broker)).
Need only the wire format (for firmware, a proxy, a test tool)? [`stem-mqtt-proto`](crates/mqtt-proto/README.md)
is the runtime-free codec the client and broker are built on: no async runtime, no TLS, no sockets.
Runnable examples (the `mqtt-client` ones need a broker already running):
```sh
cargo run -p stem-mqtt-broker --example simple_broker
cargo run -p stem-mqtt-client --example pub_sub
cargo run -p stem-mqtt-client --example will_and_retain
cargo run -p stem-mqtt-client --example mqtt_versions # MQTT 3.1.1 + 5.0 on the same broker
cargo run -p stem-mqtt-client --example topics # topic hierarchies, `+` / `#` wildcards
cargo run -p stem-mqtt-client --example long_lived_connection # persistent connection, keep-alive
cargo run -p stem-mqtt-client --example shared_subscriptions # $share/<group>/… round-robin work queue
cargo run -p stem-mqtt-broker --example auth_broker # then, in another terminal:
cargo run -p stem-mqtt-client --example auth_client # one login rejected, one accepted
```
Full crate docs: [`crates/mqtt-proto`](crates/mqtt-proto/README.md),
[`crates/mqtt-client`](crates/mqtt-client/README.md),
[`crates/mqtt-broker`](crates/mqtt-broker/README.md).
## 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](https://github.com/charmbracelet/vhs): `just vhs-tape <name>` or `just vhs-all`
(tapes in [`examples/vhs`](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; `stem-mqtt-proto` is a Rust-only crate, with no foreign-language package); import names in code are unchanged. A package is available once a
release has published it to that registry.
| **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` |
| Elixir / Erlang | `{:stem_mqtt, "~> 0.5"}` (Hex) | `StemMqtt.Client` · `stem_mqtt` |
| Ruby | `gem install stem-mqtt-client stem-mqtt-broker` | `require "stem_mqtt_client"` |
| 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/`](docs/languages/README.md):
| Rust | [rust.md](docs/languages/rust.md) | `tokio`; the [quick start](#quick-start-rust) above |
| Python | [python.md](docs/languages/python.md) | `asyncio`; wheels bundle the native library |
| Node.js | [node.md](docs/languages/node.md) | ESM, server-side; Promises |
| Kotlin | [kotlin.md](docs/languages/kotlin.md) | coroutines; JVM and Android |
| Java | [java.md](docs/languages/java.md) | JDK 22+ (FFM); `CompletableFuture` |
| C# | [csharp.md](docs/languages/csharp.md) | `async`/`await`; .NET 8+ |
| Go | [go.md](docs/languages/go.md) | cgo; install the native libraries separately |
| Dart | [dart.md](docs/languages/dart.md) | pull-style API; needs `cargo` to build |
| Haskell | [haskell.md](docs/languages/haskell.md) | pull-style API; needs `cargo` to build |
| Elixir / Erlang | [elixir.md](docs/languages/elixir.md) | Rustler NIF; events as messages; precompiled |
| Ruby | [ruby.md](docs/languages/ruby.md) | per-platform gems; async calls block (fiber-aware) |
| Swift | [swift.md](docs/languages/swift.md) | `async`/`await`; XCFrameworks for Apple platforms |
| C | [c.md](docs/languages/c.md) | C11 sources + the release libraries; `_async` variants with completion callbacks |
| C++ | [cpp.md](docs/languages/cpp.md) | C++17; `uniffi::Future`; exceptions or `uniffi::Result` |
| Zig | [zig.md](docs/languages/zig.md) | Zig 0.17; Zig errors and allocators |
| Lua | [lua.md](docs/languages/lua.md) | Lua C modules (smoke-tested on 5.4 and 5.5); callbacks run on the Lua thread |
Callbacks are available everywhere except Dart and Haskell, which use the
[pull-style API](docs/languages/README.md#receiving-messages-and-events).
## Features
- **MQTT 3.1.1 and MQTT 5.0** — one codec (`stem-mqtt-proto`), 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**, in its own runtime-free crate (`stem-mqtt-proto`, usable
on a microcontroller) — 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-Rust `rustls`, 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 —
see [`demo/`](demo) 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, …) via
`MqttEnhancedAuthProvider` on the broker and `MqttAuthHandler` on 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_message` on the client,
`enable_event_queue` / `next_event` on the broker.
- **UniFFI bindings** — the client and the broker build as `cdylib`/`staticlib`
(`cargo rustc --lib --crate-type cdylib,staticlib`) and ship a
`uniffi-bindgen` binary 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-fuzz` targets in [`fuzz/`](fuzz)) — see `CHANGELOG.md`.
## Running the broker
```sh
cargo install stem-mqtt-broker # or: cargo run -p stem-mqtt-broker --bin mqtt-broker --
mqtt-broker --bind 0.0.0.0 --port 1883 --ws-port 8083 --allow-anonymous
```
| `--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
```sh
docker compose up --build # broker (TCP + WS) + a browser client at http://localhost:8090
just demo-dashboard # broker + the Rust/Topcoat web dashboard (multi-client, live feed) on :3000
```
[`demo/README.md`](demo/README.md) covers the plain-binaries path; [`packaging/k8s`](packaging/k8s/README.md)
has a one-Pod and a Deployment + Service Kubernetes demo.
## Workspace layout
| [`crates/mqtt-proto`](crates/mqtt-proto) | The MQTT 3.1.1 / 5.0 wire codec (`stem-mqtt-proto`): pure and synchronous, no async runtime, TLS or UniFFI. Shared by the client and the broker. |
| [`crates/mqtt-client-blocking`](crates/mqtt-client-blocking) | The **blocking** client over any `std::io` transport, built on the codec: no async runtime, runs on an ESP32 with ESP-IDF `std`. The small counterpart of `mqtt-client`. |
| [`crates/mqtt-client`](crates/mqtt-client) | Async, `tokio`-based MQTT client; re-exports the codec as `mqtt_client::protocol`. |
| [`crates/mqtt-broker`](crates/mqtt-broker) | Standalone broker (library + `mqtt-broker` CLI), built on the same codec. |
| [`demo/`](demo) | Browser client (`web/`) and a Rust + Topcoat dashboard (`dashboard/`). |
| [`tests/bindings`](tests/bindings) | One end-to-end smoke test per language, run by `scripts/test_bindings.nu`. |
| [`packaging/`](packaging) | Per-language package templates, Dockerfile and Kubernetes manifests. |
| [`scripts/`](scripts) | Nushell scripts for bindings, packaging, publishing and releases (tested in `scripts/tests`). |
| [`fuzz/`](fuzz) | `cargo-fuzz` targets (on `stem-mqtt-proto`): packet decoder, encode/decode round-trip, chunked reads, varint/strings, PUBLISH, topic matching (`just fuzz <target>`). |
`stem-mqtt-proto` is the shared foundation: a pure, synchronous codec with no networking or async
dependencies. The client re-exports it as `mqtt_client::protocol` / `mqtt_client::error` (the paths are
unchanged) and declares `QoS`, `MqttVersion` and `MqttError` to UniFFI as remote types, so the generated
bindings keep their shape. Publish order: `stem-mqtt-proto`, then `stem-mqtt-client`, then `stem-mqtt-broker`.
## Generating foreign-language bindings
Everything is driven by [Nushell](https://www.nushell.sh/) scripts; the generator
table (languages, pinned generator versions, required UniFFI release, registry)
lives in one file, [`scripts/bindings/spec.nu`](scripts/bindings/spec.nu).
```sh
nu scripts/generate_bindings.nu kotlin # kotlin | swift | python (built into UniFFI)
nu scripts/install_bindgens.nu # install the pinned third-party generators
nu scripts/generate_bindings.nu go mqtt-broker # go | csharp | java | dart | node | haskell | c | cpp | zig | lua
just bindings-all # or: just bindings-third-party
```
| 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](https://github.com/NordSecurity/uniffi-bindgen-go) | ✅ client + broker + all callbacks | stable |
| C# | [uniffi-bindgen-cs](https://github.com/NordSecurity/uniffi-bindgen-cs) | ✅ client + broker + all callbacks | stable |
| Java (JDK 22+) | [uniffi-bindgen-java](https://github.com/IronCoreLabs/uniffi-bindgen-java) | ✅ client + broker + all callbacks | stable |
| Dart | [uniffi-dart](https://github.com/acterglobal/uniffi-dart) | ✅ client + broker through the pull-style API ¹ | experimental |
| Node.js | [uniffi-bindgen-node-js](https://github.com/criccomini/uniffi-bindgen-node-js) | ✅ client + broker + all callbacks | experimental |
| Haskell | [uniffi-bindgen-haskell](https://github.com/mercury/uniffi-bindgen-haskell) + [our patch](packaging/haskell) (PR [#3](https://github.com/mercury/uniffi-bindgen-haskell/pull/3)) | ✅ client + broker through the pull-style API ¹ | experimental |
| C | [uniffi-bindgen-c](https://github.com/sorinirimies/uniffi-bindgen-c) | ✅ client + broker + all callbacks | experimental |
| C++ | [uniffi-bindgen-cpp](https://github.com/sorinirimies/uniffi-bindgen-cpp) | ✅ client + broker + all callbacks | experimental |
| Zig | [uniffi-bindgen-zig](https://github.com/sorinirimies/uniffi-bindgen-zig) | ✅ client + broker + all callbacks | experimental |
| Lua | [uniffi-bindgen-lua](https://github.com/sorinirimies/uniffi-bindgen-lua) ³ | ✅ client + broker + all callbacks | 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`](packaging/haskell/uniffi-bindgen-haskell.patch)
fixes all three (and is proposed upstream, see [`packaging/haskell`](packaging/haskell)); `install_bindgens.nu` builds the generator from the pinned revision with the patch applied.
³ Lua needs uniffi-bindgen-lua 0.1.2 or later: 0.1.1 deadlocked when Lua waited in one module (a
client's `connect`) while Rust called back into the other (the broker's auth provider).
C, C++, Zig and Lua ship as sources (`bindings-<language>.zip` on each release) that you compile into
your program and link to the libraries of the `mqtt-client-` / `mqtt-broker-<version>-<target>` archives.
**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 (the workspace and Java are on 0.32,
Haskell on 0.32.0, Go, C#, Dart and Node still on 0.31). `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?
```sh
nu scripts/test_bindings.nu # every language (missing toolchains are skipped)
nu scripts/test_bindings.nu go java --strict
just test-bindings go
```
For each language this builds the native libraries, generates the bindings, stages a
scratch project with the smoke test from [`tests/bindings/<language>/`](tests/bindings) 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](docs/development.md) · [publishing](docs/publishing.md).
## License
MIT — see [LICENSE](LICENSE).