Skip to content

WASM Guide

Fabrício Bracht edited this page Jul 3, 2026 · 1 revision

WASM Guide

Run MQTT clients and an in-tab broker in the browser with WebAssembly, via the mqtt5-wasm package (v1.4.0).


Overview

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 │                             │
│                     └─────────────┘                             │
└─────────────────────────────────────────────────────────────────┘

Installation

npm install mqtt5-wasm
# or
yarn add mqtt5-wasm

Basic Usage

Initialize WASM

You must call the default export init() once before constructing any client or broker.

import init, { MqttClient } from "mqtt5-wasm";

await init();

Connect to an External Broker (WebSocket)

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");

Publishing

Payloads are Uint8Array byte buffers.

QoS 0 (Fire and Forget)

const encoder = new TextEncoder();
await client.publish("sensors/temp", encoder.encode("25.5"));

QoS 1 (Acknowledged)

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);
});

QoS 2 (Exactly Once)

const packetId = await client.publishQos2("commands/critical", encoder.encode("execute"), (reasonCode) => {
  console.log("PUBCOMP received, reason:", reasonCode);
});

Subscribing

With Callback

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);
});

Without Callback

const packetId = await client.subscribe("sensors/#");
console.log("Subscribed with packet ID:", packetId);

Unsubscribe

await client.unsubscribe("sensors/temp");

Connection Events

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();

In-Tab Broker (MessagePort)

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

Use Cases

  • Offline-first apps — MQTT works without a network
  • Testing — unit-test MQTT code in the browser
  • Demos — interactive examples without a server

Cross-Tab Communication (BroadcastChannel)

Multiple tabs joined to the same channel name exchange MQTT packets. One tab runs the broker.

Broker tab

const broker = new Broker();
const localClient = new MqttClient("broker-tab");
const port = broker.createClientPort();
await localClient.connectMessagePort(port);

Other tabs

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"));

React Integration

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 };
}

Advanced Options

Connect Options

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);

Publish 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);

Subscribe Options

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);

Reconnect Options

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);

Additional Events

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]>
});

Broker Methods

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}`));

Automatic Features

Keepalive

  • Sends PINGREQ on the negotiated keep-alive interval
  • Detects a missed PINGRESP and surfaces the failure via onError / triggers reconnection

QoS 2 Flow

  • Full four-way handshake (PUBLISH → PUBREC → PUBREL → PUBCOMP)

Limitations

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 Compatibility

Browser Minimum Version
Chrome/Edge 90+
Firefox 88+
Safari 15.4+

Building from Source

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";.

Clone this wiki locally