-
-
Notifications
You must be signed in to change notification settings - Fork 6
WASM Guide
Run MQTT clients and an in-tab broker in the browser with WebAssembly, via the mqtt5-wasm
package (v1.4.0).
mqtt5-wasm compiles the MQTT v5.0 / v3.1.1 stack to WebAssembly and exposes a JavaScript-friendly
API through wasm-bindgen. JS class and method names are camelCase (the underlying Rust types
are named Wasm*, but the exported JS names drop that prefix).
- MqttClient — connect to external brokers over WebSocket, or to an in-tab broker
- Broker — run a complete MQTT broker inside a browser tab (memory-only storage)
- Three transports — WebSocket (external), MessagePort (in-tab), BroadcastChannel (cross-tab)
┌─────────────────────────────────────────────────────────────────┐
│ Browser │
├─────────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ WebSocket │ │ MessagePort │ │ Broadcast │ │
│ │ (external) │ │ (in-tab) │ │ Channel │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ └──────────────────┴──────────────────┘ │
│ │ │
│ ┌──────▼──────┐ │
│ │ MqttClient │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────────┘
npm install mqtt5-wasm
# or
yarn add mqtt5-wasmYou must call the default export init() once before constructing any client or broker.
import init, { MqttClient } from "mqtt5-wasm";
await init();const client = new MqttClient("browser-client");
await client.connect("ws://broker.example.com:8000/mqtt");
// Or secure WebSocket
await client.connect("wss://broker.example.com:443/mqtt");Payloads are Uint8Array byte buffers.
const encoder = new TextEncoder();
await client.publish("sensors/temp", encoder.encode("25.5"));const packetId = await client.publishQos1("sensors/temp", encoder.encode("25.5"), (reasonCode) => {
if (reasonCode === 0) console.log("Message acknowledged");
else console.error("Publish failed, reason:", reasonCode);
});const packetId = await client.publishQos2("commands/critical", encoder.encode("execute"), (reasonCode) => {
console.log("PUBCOMP received, reason:", reasonCode);
});The callback receives (topic, payload, properties).
await client.subscribeWithCallback("sensors/+/data", (topic, payload, properties) => {
const decoder = new TextDecoder();
console.log(`${topic}: ${decoder.decode(payload)}`);
if (properties.responseTopic) console.log("reply to:", properties.responseTopic);
});const packetId = await client.subscribe("sensors/#");
console.log("Subscribed with packet ID:", packetId);await client.unsubscribe("sensors/temp");client.onConnect((reasonCode, sessionPresent) => {
console.log("Connected!", reasonCode, sessionPresent);
});
client.onDisconnect(() => console.log("Disconnected from broker"));
client.onError((error) => console.error("MQTT Error:", error));
client.onConnectivityChange((online) => console.log("Browser online:", online));
if (client.isConnected()) console.log("Currently connected");
await client.disconnect();Run a complete MQTT broker inside the browser tab. Clients connect to it over a MessagePort:
import init, { Broker, MqttClient } from "mqtt5-wasm";
await init();
const broker = new Broker();
const client = new MqttClient("local-client");
const port = broker.createClientPort();
await client.connectMessagePort(port);
const encoder = new TextEncoder();
await client.subscribeWithCallback("test/#", (topic, payload, properties) => {
console.log("Received:", topic);
});
await client.publish("test/hello", encoder.encode("world"));The in-tab broker provides full MQTT v5.0 support (QoS 0/1/2, retained messages, shared
subscriptions) with memory-only storage. By default it denies anonymous connections — set
allowAnonymous = true on BrokerConfig or add users with addUser().
- Offline-first apps — MQTT works without a network
- Testing — unit-test MQTT code in the browser
- Demos — interactive examples without a server
Multiple tabs joined to the same channel name exchange MQTT packets. One tab runs the broker.
const broker = new Broker();
const localClient = new MqttClient("broker-tab");
const port = broker.createClientPort();
await localClient.connectMessagePort(port);const client = new MqttClient("other-tab");
await client.connectBroadcastChannel("mqtt-channel");
await client.subscribeWithCallback("notifications", (topic, payload) => {
console.log("Received:", new TextDecoder().decode(payload));
});
await client.publish("notifications", new TextEncoder().encode("Hello"));import { useState, useEffect, useCallback } from "react";
import init, { MqttClient } from "mqtt5-wasm";
export function useMqtt(clientId, brokerUrl) {
const [client, setClient] = useState(null);
const [connected, setConnected] = useState(false);
const [messages, setMessages] = useState([]);
useEffect(() => {
let mqttClient;
async function connect() {
await init();
mqttClient = new MqttClient(clientId);
mqttClient.onConnect(() => setConnected(true));
mqttClient.onDisconnect(() => setConnected(false));
await mqttClient.connect(brokerUrl);
setClient(mqttClient);
}
connect();
return () => { if (mqttClient) mqttClient.disconnect(); };
}, [clientId, brokerUrl]);
const subscribe = useCallback(async (topic) => {
if (client) {
await client.subscribeWithCallback(topic, (t, payload) => {
const decoder = new TextDecoder();
setMessages((prev) => [...prev, { topic: t, payload: decoder.decode(payload) }]);
});
}
}, [client]);
const publish = useCallback(async (topic, message) => {
if (client) await client.publish(topic, new TextEncoder().encode(message));
}, [client]);
return { connected, messages, subscribe, publish };
}import { ConnectOptions, WillMessage } from "mqtt5-wasm";
const options = new ConnectOptions();
options.keepAlive = 30;
options.cleanStart = true;
options.username = "user";
options.set_password(new TextEncoder().encode("secret")); // set_password stays snake_case
options.sessionExpiryInterval = 3600;
options.receiveMaximum = 65535;
options.maximumPacketSize = 1048576;
options.topicAliasMaximum = 10;
options.requestResponseInformation = true;
options.protocolVersion = 5; // 4 for MQTT 3.1.1
options.authenticationMethod = "SCRAM-SHA-256";
options.addUserProperty("app-version", "1.0.0");
options.addBackupUrl("ws://backup.broker.com/mqtt");
const will = new WillMessage("status/offline", new TextEncoder().encode("true"));
will.qos = 1;
will.retain = true;
will.willDelayInterval = 30;
options.setWill(will);
await client.connectWithOptions("ws://broker.example.com/mqtt", options);import { PublishOptions } from "mqtt5-wasm";
const pubOptions = new PublishOptions();
pubOptions.qos = 1;
pubOptions.retain = true;
pubOptions.messageExpiryInterval = 3600;
pubOptions.responseTopic = "responses/client-123";
pubOptions.set_correlationData(new TextEncoder().encode("request-id-456")); // snake_case
pubOptions.contentType = "application/json";
pubOptions.topicAlias = 1;
pubOptions.payloadFormatIndicator = true; // UTF-8 text
pubOptions.addUserProperty("timestamp", Date.now().toString());
await client.publishWithOptions("sensors/temp", new TextEncoder().encode("25.5"), pubOptions);import { SubscribeOptions } from "mqtt5-wasm";
const subOptions = new SubscribeOptions();
subOptions.qos = 1;
subOptions.noLocal = true;
subOptions.retainAsPublished = true;
subOptions.retainHandling = 1; // 0=send, 1=send if new, 2=don't send
subOptions.subscriptionIdentifier = 42;
// Note the argument order: (topic, callback, options)
await client.subscribeWithOptions("sensors/#", (topic, payload, props) => {
console.log("Received:", topic, props.responseTopic);
}, subOptions);import { ReconnectOptions } from "mqtt5-wasm";
const reconnectOpts = new ReconnectOptions();
reconnectOpts.enabled = true;
reconnectOpts.initialDelayMs = 1000;
reconnectOpts.maxDelayMs = 60000;
reconnectOpts.backoffFactor = 2.0;
reconnectOpts.maxAttempts = 10; // or null for unlimited
client.setReconnectOptions(reconnectOpts);client.onReconnecting((attempt, delayMs) => {
console.log(`Reconnect attempt ${attempt} in ${delayMs}ms`);
});
client.onReconnectFailed((error) => console.error("All reconnect attempts failed:", error));
// Enhanced auth (SCRAM, etc.) — callback receives (method, data)
client.onAuthChallenge((method, data) => {
const response = computeResponse(method, data);
client.respondAuth(response);
});Message properties are always delivered as the third callback argument
(MessageProperties):
client.subscribeWithCallback("topic/#", (topic, payload, props) => {
props.responseTopic; // string | null
props.correlationData; // Uint8Array | null
props.contentType; // string | null
props.payloadFormatIndicator; // boolean | null
props.messageExpiryInterval; // number | null
props.subscriptionIdentifiers; // number[]
props.getUserProperties(); // Array<[string, string]>
});const broker = new Broker();
// Or with a config
import { BrokerConfig } from "mqtt5-wasm";
const config = new BrokerConfig();
config.maxClients = 500;
config.allowAnonymous = true;
const broker2 = Broker.withConfig(config);
// User management
broker.addUser("alice", "password123");
broker.removeUser("alice"); // returns boolean
// ACL rules (async; permission: 'read' | 'write' | 'readwrite' | 'deny')
await broker.addAclRule("alice", "sensors/#", "readwrite");
await broker.setAclDefaultDeny();
// Role-based access (async)
await broker.addRole("admin");
await broker.addRoleRule("admin", "#", "readwrite");
await broker.assignRole("alice", "admin");
// Bridge to another broker
import { BridgeConfig, TopicMapping, BridgeDirection } from "mqtt5-wasm";
const bridgeConfig = new BridgeConfig("upstream");
const mapping = new TopicMapping("sensors/#", BridgeDirection.Out);
mapping.qos = 1;
bridgeConfig.addTopic(mapping);
await broker.addBridgeWebSocket(bridgeConfig, "ws://broker.example.com:8000/mqtt");
// $SYS topics
broker.startSysTopics(); // every 10s
broker.startSysTopicsWithIntervalSecs(30); // custom interval
broker.stopSysTopics();Broker lifecycle callbacks each receive a single event object:
broker.onClientConnect((event) => console.log("connected:", event.clientId));
broker.onClientPublish((event) => console.log(`${event.clientId} -> ${event.topic}`));- Sends PINGREQ on the negotiated keep-alive interval
- Detects a missed PINGRESP and surfaces the failure via
onError/ triggers reconnection
- Full four-way handshake (PUBLISH → PUBREC → PUBREL → PUBCOMP)
| Feature | Status |
|---|---|
| TCP connections | Not available (browser restriction) |
| TLS/mTLS | Browser handles wss://
|
| File I/O | Memory-only; no persistence |
| Server sockets | Not available |
| Browser | Minimum Version |
|---|---|
| Chrome/Edge | 90+ |
| Firefox | 88+ |
| Safari | 15.4+ |
mqtt5-wasm is built with wasm-pack (or wasm-bindgen). Features select what is compiled in:
client, broker, and codec (payload compression).
# Client only
wasm-pack build crates/mqtt5-wasm --target web --features client
# Client + in-tab broker
wasm-pack build crates/mqtt5-wasm --target web --features client,broker
# Client + broker + compression codecs
wasm-pack build crates/mqtt5-wasm --target web --features client,broker,codec
# Output is in crates/mqtt5-wasm/pkg/ (mqtt5.js, mqtt5_bg.wasm, mqtt5.d.ts)When importing the locally-built package (rather than the npm package), import from the generated
file, e.g. import init, { MqttClient } from "./pkg/mqtt5.js";.
Getting Started
Broker Guide
Client Guide
Platform Guides
CLI Reference
Development