An MQTT 5 topic explorer built with Rust and GPUI Kit.
- Connection management to quickly connect to your favorite brokers, with optional TLS and WebSocket support.
- Live topic tree
- Payload pane to inspect the message payloads
- Live payload diffs
- Publish messages.
- Deletion of retained messages for a selected topic and its known subtopics.
- Live line and area charts for monitored numeric JSON fields
Settings are stored in config.json in the application configuration directory. On Linux, this is $XDG_CONFIG_HOME/topq, or ~/.config/topq when unset.
config.json:
{
"mode": "system",
"light_theme": "Ayu Light",
"dark_theme": "Ayu Dark",
"reduce_motion": "system",
"inline_payload": true,
"inline_payload_limit_enabled": false,
"inline_payload_max_chars": 10,
"connections": [
{
"name": "Home broker",
"connection": "mqtts://user@broker.example.com:8883",
"client_id": "topq-workstation",
"topics": [
{ "topic": "home/#", "qos": 1 },
{ "topic": "$SYS/#", "qos": 0 }
],
"validate_certificate": true
}
],
"active_connection": "Home broker"
}active_connectionis the name of the connection to connect to at startup. Usenullor omit it to start without connecting. An unknown name falls back to the first connection. When multiple connections have the same non-empty name, only the first is loaded; give each connection a unique name.connectionis the broker URI: usemqtt://,mqtts://,ws://, orwss://to select MQTT, MQTT over TLS, WebSocket, or secure WebSocket. Include the broker host and preferably an explicit port, for examplemqtts://mqtt-user@mqtt.lan:8883. Put the username before@; omit it for an empty username. URL-encode reserved characters in the username. Do not put a password in the URI. If the URI omits the port, it defaults to1883,8883,80, or443for those schemes, respectively. IPv6 hosts must be bracketed, for examplemqtt://user@[::1]:1883.client_ididentifies the MQTT client. Use a different ID for each concurrently connected client on the same broker to avoid disconnecting each other. If omitted, an ID is generated when the connection is loaded.topicsmust contain at least one unique MQTT filter.+matches one topic level; a final#matches any number of levels.#does not include topics starting with$; subscribe to$SYS/#separately for broker statistics.qosis the requested maximum delivery QoS:0(at most once),1(at least once), or2(exactly once). Iftopicsis omitted, it defaults to[{"topic":"#","qos":0}].
| Transport | URI scheme | Connection editor's default port |
|---|---|---|
| MQTT | mqtt:// |
1883 |
| MQTT over TLS | mqtts:// |
8883 |
| MQTT over WebSocket | ws:// |
9001 |
| MQTT over secure WebSocket | wss:// |
9002 |
WebSocket connections use the fixed path /mqtt; custom paths are not configurable.
validate_certificate defaults to true: TLS uses the system's trusted certificates and verifies the broker hostname. Setting it to false disables certificate and hostname verification; encryption remains, but the broker is not authenticated. It has no effect without TLS.
Passwords are stored only in secure credential storage and must not be included in the URI. A JSON password field is ignored. For connections with a username, the password is retrieved from credential storage. Anonymous connections (without a username) do not need a password. Saving or restoring passwords requires an available, unlocked credential store; there is no plaintext fallback.
Credential storage uses the native Keychain on macOS and Credential Manager on Windows. On Linux, oo7 automatically selects the desktop Secret Service for regular installations or an encrypted file backed by the Secret portal inside Flatpak. Flatpak password storage requires a host implementation of org.freedesktop.portal.Secret; normal operation does not require direct access to org.freedesktop.secrets. Keep the Flatpak application ID stable because the portal encryption key is application-scoped. Passwords are not migrated between native and Flatpak installations; re-enter them when switching.
Appearance settings live alongside connection settings in config.json:
mode:system,light, ordark.system(default) follows the OS appearance.light_themeanddark_theme: exact theme names for each mode, not filenames. Defaults areAyu LightandAyu Dark. Missing themes or themes for the wrong mode fall back to the defaults.reduce_motion:system(default) follows the OS preference,onreduces motion, andoffoverrides the OS preference to allow motion.inline_payload:true(default) shows JSON and text previews in the topic tree. Payloads that are too long or are not text show a datatype tag instead, such asJSON,Text,image/jpeg, orBinary. Set tofalseto hide both previews and datatype tags; topic summaries and the full payload view are unchanged.inline_payload_limit_enabled:false(default) does not hide previews based on payload length, preserving the existing behavior and its 100-character presentation cap. Set totrueto applyinline_payload_max_charswheninline_payloadis also enabled. Disabling the limit retains the saved maximum.inline_payload_max_chars:10(default) is the stored maximum, used only wheninline_payload_limit_enabledistrue. Textual payloads are measured by Unicode character count, not byte count. Text whose character count is less than or equal to the limit qualifies for an inline preview; longer text shows a datatype tag rather than being truncated to fit the limit. Non-text payloads always show a datatype tag, using the detected MIME type when available.0is allowed; with the limit enabled, only an empty textual payload qualifies at this limit. The full payload view is unchanged.
For example, to enable a 10-character limit:
{
"inline_payload": true,
"inline_payload_limit_enabled": true,
"inline_payload_max_chars": 10
}Place custom theme JSON files in ~/.config/topq/themes/ (or $XDG_CONFIG_HOME/topq/themes/ when set on Linux).
Theme files use the GPUI Kit theme format
Activate the devenv virtual environment, then run:
just runBig thanks to MQTT Explorer for the source of inspiration. I have been a long time user of MQTT Explorer and have enjoyed using it, but in a quest to reduce the number of Electron apps I use, I decided to build a native alternative.
