MQTTastic Client KMP

A fully-featured MQTT 5.0 and 3.1.1 client library for Kotlin Multiplatform — connecting JVM, Android, iOS, macOS, Linux, Windows, and browsers through a single, idiomatic Kotlin API.
The bundled Compose Multiplatform sample app, live on tls://mqtt.meshtastic.org:8883.
Features
Why MQTTastic?
Platform Support
Architecture
All protocol logic — packet encoding/decoding, the client state machine, QoS flows, and property handling — lives in the mqtt-client-core module as pure commonMain Kotlin, with zero transport dependencies. Each transport ships as its own artifact, so a consumer pulls in only what it uses. Every bug fix, feature, and optimization in core applies to all 9 targets simultaneously.
┌─────────────────────────────────────────────┐
│ mqtt-client-core │ ← public API: suspend + Flow
│ MqttClient / MqttConnection / QoS machines │ ← protocol logic, keepalive
│ MqttPacket / Encoder / Decoder │ ← MQTT 5.0 wire format
│ MqttTransport / MqttTransportFactory (SPI) │ ← the transport seam
└───────────────────────┬─────────────────────┘
▲ │ api(core) ▲
│ ▼ │
┌───────────┴───────────┐ ┌───────────────────┴───────────┐
│ mqtt-client- │ │ mqtt-client-transport-ws │
│ transport-tcp │ │ WebSocketTransport(Factory) │
│ TcpTransport(Factory) │ │ ktor-client-websockets │
│ ktor-network + TLS │ │ all targets incl. browser │
│ (no browser) │ │ │
└───────────────────────┘ └────────────────────────────────┘
MqttTransport / MqttTransportFactory are the public service-provider interface — the sole platform abstraction boundary. Core has no compile-time dependency on any transport module; you supply a factory (, , or both combined with ) via . Coroutines drive everything: functions for operations, for incoming messages, and for lifecycle observation.
Installation
Artifacts are published to Maven Central under the org.meshtastic group. Depend on
mqtt-client-core plus the transport(s) you need. The mqtt-client-bom pins every module to one
version so you don't repeat it:
repositories {
mavenCentral()
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(platform("org.meshtastic:mqtt-client-bom:0.3.0"))
implementation("org.meshtastic:mqtt-client-core")
implementation("org.meshtastic:mqtt-client-transport-tcp")
implementation("org.meshtastic:mqtt-client-transport-ws")
}
}
}
Then supply the matching factory when building the client (combine with + if you use both):
val client = MqttClient("my-client") {
transportFactory = TcpTransportFactory() + WebSocketTransportFactory()
}
Browser (wasmJs) can only use mqtt-client-transport-ws — raw TCP is unavailable there.
Groovy DSL
// build.gradle
kotlin {
sourceSets {
commonMain {
dependencies {
implementation platform('org.meshtastic:mqtt-client-bom:0.3.0')
implementation 'org.meshtastic:mqtt-client-core'
implementation 'org.meshtastic:mqtt-client-transport-tcp'
implementation 'org.meshtastic:mqtt-client-transport-ws'
}
}
}
}
Single-platform (JVM / Android only)
dependencies {
implementation(platform("org.meshtastic:mqtt-client-bom:0.3.0"))
implementation("org.meshtastic:mqtt-client-core")
implementation("org.meshtastic:mqtt-client-transport-tcp")
}
Quick Start
import org.meshtastic.mqtt.*
import org.meshtastic.mqtt.transport.tcp.TcpTransportFactory
client = MqttClient() {
transportFactory = TcpTransportFactory()
keepAliveSeconds =
autoReconnect =
defaultQos = QoS.AT_LEAST_ONCE
}
client.use(MqttEndpoint.parse()) { c ->
c.subscribe()
c.publish(, )
c.messagesForTopic().collect { msg ->
println()
}
}
Verbose equivalent (without convenience APIs)
val config = MqttConfig(
clientId = "my-client",
keepAliveSeconds = 30,
autoReconnect = ,
transportFactory = TcpTransportFactory(),
)
client = MqttClient(config)
client.connect(MqttEndpoint.Tcp(host = , port = ))
client.subscribe(, QoS.AT_LEAST_ONCE)
client.publish(
MqttMessage(
topic = ,
payload = ByteString(.encodeToByteArray()),
qos = QoS.AT_LEAST_ONCE,
),
)
client.messages.collect { msg ->
(msg.topic == ) {
println()
}
}
client.close()
MQTT 3.1.1 Support
By default, the client automatically negotiates the protocol version. It connects with MQTT 5.0 first and, if the broker rejects it with UNSUPPORTED_PROTOCOL_VERSION, seamlessly retries with MQTT 3.1.1 on a fresh connection — no configuration needed:
val client = MqttClient("my-client") {
transportFactory = TcpTransportFactory()
keepAliveSeconds = 30
}
client.use(MqttEndpoint.parse("tcp://any-broker:1883")) { c ->
println("Connected with ${c.negotiatedProtocolVersion}")
c.subscribe("sensors/#")
c.messagesForTopic("sensors/#").collect { msg ->
println("Received: ${msg.payloadAsString()}")
}
}
To force a specific version or disable negotiation:
val v311Client = MqttClient("my-client") {
protocolVersion = MqttProtocolVersion.V3_1_1
}
val v5OnlyClient = MqttClient("my-client") {
negotiateVersion = false
}
MQTT 3.1.1 mode automatically:
- Omits properties sections from all packets
- Uses 3.1.1 CONNACK return codes (mapped to
ReasonCode)
- Encodes subscribe options as QoS-only (no
noLocal, retainAsPublished, retainHandling)
- Sends a body-less DISCONNECT on close
- Skips topic aliases and flow control (Receive Maximum)
5.0-only config options (sessionExpiryInterval, authenticationMethod) are rejected at config-build time when V3_1_1 is explicitly selected. When using auto-negotiation, fallback is skipped if the config uses 5.0-only features — the original rejection is re-thrown so you know the broker doesn't support your configuration.
Convenience APIs
The library ships several ergonomic extensions to reduce boilerplate:
Endpoint Parsing
Parse broker URIs instead of constructing endpoints manually:
MqttEndpoint.parse("tcp://broker:1883")
MqttEndpoint.parse("ssl://broker:8883")
MqttEndpoint.parse("mqtts://broker")
MqttEndpoint.parse("wss://broker/mqtt")
Topic-Filtered Message Flows
client.messagesForTopic("sensors/temperature").collect { ... }
client.messagesMatching("sensors/+/temperature").collect { ... }
Builder DSL
Use the builder DSL for complex configurations (annotated with @MqttDsl for scope safety, like Ktor's @KtorDsl):
val config = MqttConfig.build {
clientId = "sensor-hub-01"
keepAliveSeconds = 30
cleanStart = false
autoReconnect = true
defaultQos = QoS.AT_LEAST_ONCE
logger = MqttLogger.println()
logLevel = MqttLogLevel.DEBUG
will {
topic = "sensors/status"
payload("offline")
qos = QoS.AT_LEAST_ONCE
retain = true
}
}
Logging
The library provides a zero-overhead logging interface. When no logger is configured (the default), message lambdas are never evaluated:
val config = MqttConfig(
clientId = "debug-client",
logger = MqttLogger.println(),
logLevel = MqttLogLevel.DEBUG,
)
val config = MqttConfig(
clientId = "production-client",
logger = : MqttLogger {
{
myAppLogger.log(level.name, , throwable)
}
},
logLevel = MqttLogLevel.INFO,
)
Log levels from most to least verbose: TRACE → DEBUG → INFO → WARN → ERROR → NONE.
Android / KMP Integration
The library is designed as a drop-in MQTT client for KMP projects. Consumer ProGuard/R8 rules are bundled automatically.
ViewModel-scoped client
Collecting in Compose
@Composable
fun MqttScreen(viewModel: MqttViewModel) {
val state by viewModel.connectionState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.observeMessages().collect { msg ->
}
}
}
Version alignment
The library uses Ktor 3.4.2 and kotlinx-coroutines 1.10.2. If your project uses the same versions, no conflicts will arise. Pin versions in your libs.versions.toml to avoid Gradle resolution surprises.
MQTT 5.0 Coverage
Protocol
| Feature | Status | Spec Section |
|---|
| All 15 packet types | ✅ |
Quality of Service
Session & Connection
Advanced Features
Observability
| Feature | Status | Spec Section |
|---|
| Configurable logging (6 levels) | ✅ | — |
| Connection state observation | ✅ | — |
Known Limitations
| Limitation | Detail |
|---|
| Enhanced auth during CONNECT | Auth challenges are delivered only after the connection is established. SASL-style challenge/response during the CONNECT handshake (§4.12.1) is not yet supported. |
| Client-side session persistence | When cleanStart=false, the broker resumes session state, but the client does not persist in-flight QoS 1/2 messages across reconnects. Unacknowledged messages may be lost. |
Building
See CONTRIBUTING.md for build setup, development workflow, and the full command reference.
Documentation
Contributing
Contributions are welcome! Please read CONTRIBUTING.md for guidelines on:
- Setting up your development environment
- Code style and conventions
- Submitting pull requests
For vulnerability reports, see the Security Policy.
All participants are expected to follow the Code of Conduct.
License
This project is licensed under the GNU General Public License v3.0,
consistent with all repositories in the Meshtastic organization.