Quick start
After installation the broker listens on three ports: :1883 for MQTT over TCP, :8883 for MQTT over TLS, and :8080 for the web dashboard and MQTT over WebSocket at the /mqtt path.
Open http://localhost:8080 and sign in as the administrator. The default login is admin / admin; change the password in the Profile section right after your first sign-in.
Create your first user: a name, a password of at least eight characters and an ACL rule. The rule $u/# gives a device full access to its own branch — the dashboard has an “Own branch” button for exactly this.
mosquitto_pub -h localhost -p 1883 -u sensor-42 -P s3nsorPass! \
-t 'sensor-42/temp' -m '21.5'
mosquitto_sub -h localhost -p 1883 -u sensor-42 -P s3nsorPass! \
-t 'sensor-42/#' -v
Installing on Linux
The recommended route is the APT repository: updates arrive with your system updates.
sudo wget -qO /usr/share/keyrings/elx-repo.gpg https://repo.um-d.ru/elx-repo.gpg
echo "deb [signed-by=/usr/share/keyrings/elx-repo.gpg] https://repo.um-d.ru stable main" | sudo tee /etc/apt/sources.list.d/elx-repo.list
sudo apt-get update
sudo apt-get install elxmqttbroker
The package installs the binary into /opt/elxmqttbroker, creates a system user elxmqtt with no login, places the data and config.json (mode 0600) alongside it, and enables the elxmqttbroker service at boot.
systemctl status elxmqttbroker # status
sudo systemctl restart elxmqttbroker # restart
journalctl -u elxmqttbroker -f # logs
The unit is locked down with ProtectSystem=strict: only the broker's own directory is writable. By default the dashboard listens on 127.0.0.1:8080 and is published through nginx, while the MQTT ports 1883 and 8883 are exposed directly — that is a separate protocol and needs no proxying.
Upgrade and Connection headers and a generous proxy_read_timeout: /ws/live and /mqtt are long-lived WebSocket connections.Installing on Windows
The usual route is the installer, elxmqttbroker-setup.exe: it puts the program in Program Files, the data in C:\ProgramData\ELX MQTT Broker, optionally registers the ELXMQTTBroker service to start with the system, and opens ports 1883, 8883 and 8080 in the firewall. It speaks Russian and English and needs administrator rights.
ProgramData rather than beside the program: the broker rewrites config.json itself from the dashboard, and a service has no business writing into Program Files. Uninstalling leaves that folder behind — it holds the users, their permissions and the messages.The portable route is a single elxmqttbroker.exe: put it in its own folder and run it, and config.json, a data directory and a self-signed TLS certificate appear next to it.
elxmqttbroker.exe # run with the defaults
elxmqttbroker.exe -web :9000 # dashboard on another port
elxmqttbroker.exe -version # version and build stamp
A portable copy can be driven as a service by hand, with the same commands the installer uses:
elxmqttbroker.exe -service install -config "C:\ProgramData\ELX MQTT Broker\config.json" -data "C:\ProgramData\ELX MQTT Broker\data"
elxmqttbroker.exe -service start
elxmqttbroker.exe -service stop
elxmqttbroker.exe -service uninstall
Configuration
config.json is the only file the broker reads. It is created on first run and holds only what must be known before startup: addresses, TLS and the administrator account.
{
"dataDir": "data",
"auth": { "username": "admin", "password": "admin" },
"mqtt": { "tcp": ":1883", "tls": ":8883" },
"web": { "addr": ":8080", "tlsAddr": "" },
"tls": { "cert": "", "key": "", "selfSigned": true,
"hosts": ["localhost", "127.0.0.1", "::1"] },
"logLevel": "info",
"logFormat": "text",
"persistence": true
}
A plaintext password is turned into a passwordHash on first start and wiped from the file. Everything else — protocol limits, SMTP, bridges, rules, API tokens, MQTT users — is configured in the dashboard and lives in data/users.json; no restart is needed for any of it.
Command-line flags override the file:
| Flag | What it sets |
|---|---|
| -config | path to config.json |
| -data | data directory |
| -mqtt | MQTT over TCP address |
| -mqtt-tls | MQTT over TLS address |
| -web | web dashboard address |
| -log | log level |
| -version | print the version and exit |
config.json holds the administrator's password hash, the SMTP password and the credentials of the external authentication source. The file should be mode 0600.Users and permissions
There are two circuits, and they should not be confused. The administrator is a single account defined in config.json that manages the dashboard only: no roles, no sign-up, and once you are in you have full access. MQTT devices are a separate list of accounts: a name of 1 to 64 characters, a password, an enabled flag and topic permissions.
An ACL rule consists of a topic filter, an access mode and a decision:
| Field | Values |
|---|---|
| Filter | an ordinary MQTT filter: sensors/+/temp, dev/#, $u/# |
| Access | read, write or readwrite |
| Decision | allow or deny |
Rules are checked top to bottom, the first match wins, and everything is deny-by-default: without an explicit allow there is no access. Order is part of the configuration, so rules can be dragged with the mouse or moved with the arrow keys.
The filter expands $u — the name of the connecting client. A single $u/# rule confines every device to its own branch and saves you a rule per name.
Passwords are stored as PBKDF2-HMAC-SHA256 with 210,000 iterations and a random salt. Session and API tokens are stored as hashes only.
MQTT authentication
The credential source is chosen under Settings → Authentication, and exactly one is active at a time. Switching happens live: the new source is first checked with the Test button, and only if it answers does the broker switch over and disconnect clients so each one is re-authenticated. If the source is unreachable, nothing changes.
| Source | When it fits |
|---|---|
internal | accounts are managed in the dashboard — the default |
mysql / mariadb | devices already live in your system's database |
sqlite | the same, but the database is a file |
csv | a simple list; files are reloaded when they change |
jwt | the device presents a signed token instead of a password |
http | your service decides on every connection |
The MySQL and MariaDB schema follows EMQX:
CREATE TABLE mqtt_user (username VARCHAR(128) PRIMARY KEY, password VARCHAR(255));
CREATE TABLE mqtt_acl (username VARCHAR(128), action VARCHAR(16),
permission VARCHAR(16), topic VARCHAR(255));
-- action: publish | subscribe | pubsub; permission: allow | deny
INSERT INTO mqtt_user VALUES ('dev', SHA2('devpass',256));
INSERT INTO mqtt_acl VALUES ('dev','pubsub','allow','dev/#');
"mqttAuth": {
"backend": "mariadb",
"sql": {
"dsn": "mqtt:mqttpass@tcp(127.0.0.1:3306)/mqtt",
"passwordQuery": "SELECT password FROM mqtt_user WHERE username = ${username} LIMIT 1",
"aclQuery": "SELECT action, permission, topic FROM mqtt_acl WHERE username = ${username}",
"passwordHash": "sha256"
}
}
${username} and ${clientid} are bound as query parameters rather than concatenated, so injection is impossible. passwordHash accepts plain, sha256, sha512, bcrypt and pbkdf2.
A JWT goes in the password or username field. HS256/384/512 and RS256/384/512 are supported; the signature, exp and nbf are verified, and optionally iss, aud and any claims of your own. Permissions come from the acl claim in either EMQX format, and is_superuser waives the check entirely.
"alg": "none" does not get through.The HTTP source receives {username, password, clientid, peerhost} and answers with {"result": "allow", "acl": […]}. An empty 200 counts as an allow; deny, 401, 403, a timeout and any error are a refusal — an unreachable authorisation service must not become an open door. Answers are cached for cacheSeconds.
Rules: rewrite, auto-subscribe, limits
Topic rewrite substitutes a topic name on the fly. A rule is an MQTT filter for cheap selection, a regular expression and a destination template with groups $1…$9:
| When | Filter | Expression | New topic |
|---|---|---|---|
| Always | raw/# | ^raw/(.+)$ | cooked/$1 |
The order follows EMQX: rewrite first, ACL check second — permissions are checked against the topic the message will actually travel under. UNSUBSCRIBE goes through the same rewrite as SUBSCRIBE, otherwise a client could never unsubscribe.
Auto-subscribe creates subscriptions right after a connection, with no SUBSCRIBE from the client. A rule of %c/# or %u/# gives every client its own branch with no manual setup.
Topic limits are for when a client behaves fine overall but one particular topic arrives far too often:
| Field | What it sets |
|---|---|
| Topic pattern | an ordinary MQTT filter: sensors/+/temp |
| Action | thin down to a rate or drop entirely |
| Messages per second | the target rate; fractions are allowed (0.2 is one per five seconds) |
| Count by | client and topic, or the topic as a whole |
Flood control is configured under Settings → Broker and applies per connection: messages per second, message burst, bytes per second, byte burst. Zero means no limit. Exceeding it does not drop the connection: the broker pauses before the next socket read, TCP closes the window, and the sender slows down on its own.
Delayed publish
Publishing to $delayed/{seconds}/{topic} is held by the broker and released to subscribers when it falls due. The format is EMQX-compatible.
mosquitto_pub -t '$delayed/60/sensors/alarm' -m 'in a minute'
The interval ranges from one second to 4,294,967 (about 49 days). The acknowledgement arrives immediately: the broker vouches for receipt, not for the existence of subscribers. Resolution is one second.
The queue is visible under Rules → Delayed: topic, payload, countdown, and cancellation one by one or all at once. Messages survive a restart, and any that fell due while the broker was down are released right after startup.
Broker bridging
The broker can connect as a client to another MQTT broker and forward topics in both directions. It is configured under Settings → Bridges: the remote address (the port is optional — 1883 by default, 8883 over TLS), login, TLS, protocol version and rules.
| Rule field | Meaning |
|---|---|
| Filter | which topics to forward |
| Direction | out, in or both |
| QoS | delivery quality across the bridge |
| Prefixes | what to prepend on either side |
There is loop protection, automatic reconnect, No Local on version 5 (no echo or duplicates), and status and counters in the dashboard. The bridge works at broker level rather than as a separate client.
REST API
The dashboard calls its own API with a session cookie; external systems use bearer tokens from Settings → API. A token is shown once and only its hash is stored. A token can be issued read-only — then only GET, HEAD and OPTIONS get through.
curl -H "Authorization: Bearer TOKEN" http://localhost:8080/api/stats
The typical scenario is your system provisioning a device and granting it broker access at once:
# create
curl -X POST http://localhost:8080/api/users \
-H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" \
-d '{"username":"sensor-42","password":"s3nsorPass!","enabled":true,
"acl":[{"filter":"$u/#","access":"readwrite","allow":true}]}'
# change the password or permissions (only the fields you send)
curl -X PATCH http://localhost:8080/api/users/sensor-42 \
-H "Authorization: Bearer TOKEN" -H "Content-Type: application/json" \
-d '{"password":"newPass12345","enabled":false}'
# delete
curl -X DELETE http://localhost:8080/api/users/sensor-42 \
-H "Authorization: Bearer TOKEN"
The password is never returned. Errors come back in a single shape, {"error":{"code":"…","message":"…"}}: duplicate, reserved, invalid_input, not_found, read_only, unauthorized.
Changes apply instantly: on a password change, a permission change or a deletion the broker drops that user's live connections itself, so the new rules take effect immediately rather than at the next reconnect.
The whole dashboard API is available: statistics and history, clients, subscriptions, retained messages, publishing, events, users, settings, bridges, rules, delayed messages, import and export.
Storage and restarts
With persistence: true the state lives in data/broker.db — SQLite in WAL mode, pure Go, no CGO. What survives a restart:
- Retained messages — written immediately on every change, not on a timer. Anything past its Message Expiry is discarded at startup.
- Persistent sessions — subscriptions, QoS 1/2 offline queues and unfinished handshakes. Snapshotted every ten seconds and on shutdown. Clean sessions are not saved: by specification they die with the connection.
- Delayed messages — any that fell due while the broker was down are released right after startup.
At startup a line such as state restored retained=128 sessions=7 subscriptions=34 delayed=2 goes to the log. Alongside in data/ sit users.json (accounts, rules and dashboard settings, written atomically) and the self-signed certificate.
Known limitations
- Clustering is not supported: the broker is designed as a single process.
- The bridge speaks QoS 0 and 1; QoS 2 is downgraded to 1.
- Enhanced authentication such as SCRAM exists in the protocol, but the built-in source does not offer it and answers
0x8C Bad authentication method. - Rate limiting applies per connection rather than per user: a client opening ten connections gets ten limits.
- Machine health is computed on Linux and Windows; on macOS and BSD the card honestly says “no data”.