stem-mqtt-client-blocking 0.8.3

A small blocking MQTT 3.1.1 / 5.0 client over any std::io transport (TcpStream, TLS), built on stem-mqtt-proto. No async runtime: for firmware (ESP32 with std), tools and tests.
Documentation
# 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.

[![CI](https://github.com/sorinirimies/stem-mqtt/actions/workflows/ci.yml/badge.svg)](https://github.com/sorinirimies/stem-mqtt/actions/workflows/ci.yml)
[![Audit](https://github.com/sorinirimies/stem-mqtt/actions/workflows/security.yml/badge.svg)](https://github.com/sorinirimies/stem-mqtt/actions/workflows/security.yml)
[![Fuzz](https://github.com/sorinirimies/stem-mqtt/actions/workflows/fuzz.yml/badge.svg)](https://github.com/sorinirimies/stem-mqtt/actions/workflows/fuzz.yml)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
[![MSRV](https://img.shields.io/badge/rust-1.75%2B-orange.svg)](Cargo.toml)

| Crate | Version | Downloads | Docs |
| --- | --- | --- | --- |
| `stem-mqtt-proto` | [![crates.io]https://img.shields.io/crates/v/stem-mqtt-proto.svg]https://crates.io/crates/stem-mqtt-proto | [![downloads]https://img.shields.io/crates/d/stem-mqtt-proto.svg]https://crates.io/crates/stem-mqtt-proto | [![docs.rs]https://img.shields.io/docsrs/stem-mqtt-proto]https://docs.rs/stem-mqtt-proto |
| `stem-mqtt-client-blocking` | [![crates.io]https://img.shields.io/crates/v/stem-mqtt-client-blocking.svg]https://crates.io/crates/stem-mqtt-client-blocking | [![downloads]https://img.shields.io/crates/d/stem-mqtt-client-blocking.svg]https://crates.io/crates/stem-mqtt-client-blocking | [![docs.rs]https://img.shields.io/docsrs/stem-mqtt-client-blocking]https://docs.rs/stem-mqtt-client-blocking |
| `stem-mqtt-client` | [![crates.io]https://img.shields.io/crates/v/stem-mqtt-client.svg]https://crates.io/crates/stem-mqtt-client | [![downloads]https://img.shields.io/crates/d/stem-mqtt-client.svg]https://crates.io/crates/stem-mqtt-client | [![docs.rs]https://img.shields.io/docsrs/stem-mqtt-client]https://docs.rs/stem-mqtt-client |
| `stem-mqtt-broker` | [![crates.io]https://img.shields.io/crates/v/stem-mqtt-broker.svg]https://crates.io/crates/stem-mqtt-broker | [![downloads]https://img.shields.io/crates/d/stem-mqtt-broker.svg]https://crates.io/crates/stem-mqtt-broker | [![docs.rs]https://img.shields.io/docsrs/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**

![Publish / subscribe](examples/vhs/generated/pub-sub-demo.gif)

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**

![MQTT 3.1.1 and 5.0 side by side](examples/vhs/generated/mqtt-versions-demo.gif)

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**

![Topics and wildcards](examples/vhs/generated/topics-demo.gif)

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**

![Shared subscriptions](examples/vhs/generated/shared-subscriptions-demo.gif)

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**

![Retained messages and Last Will](examples/vhs/generated/will-and-retain-demo.gif)

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

**Authentication**

![Authentication](examples/vhs/generated/auth-demo.gif)

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**

![Long-lived connection](examples/vhs/generated/long-lived-connection-demo.gif)

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.

| 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` |
| 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):

| Language | Guide | Notes |
| --- | --- | --- |
| 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
```

| 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

```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

| Path | Description |
| --- | --- |
| [`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
```

| 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]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).