Documentation

Everything needed to run and configure the broker. The dashboard's built-in help mirrors these sections and is translated into seven languages.

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.

The dashboard administrator is not an MQTT login. Devices connect with separate accounts created in the Users section.

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.

The nginx config must carry the 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.

Settings live in 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:

FlagWhat it sets
-configpath to config.json
-datadata directory
-mqttMQTT over TCP address
-mqtt-tlsMQTT over TLS address
-webweb dashboard address
-loglog level
-versionprint 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:

FieldValues
Filteran ordinary MQTT filter: sensors/+/temp, dev/#, $u/#
Accessread, write or readwrite
Decisionallow 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.

SourceWhen it fits
internalaccounts are managed in the dashboard — the default
mysql / mariadbdevices already live in your system's database
sqlitethe same, but the database is a file
csva simple list; files are reloaded when they change
jwtthe device presents a signed token instead of a password
httpyour 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.

The signature algorithm is taken from the configuration only, never from the token header: a forgery with "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:

WhenFilterExpressionNew topic
Alwaysraw/#^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:

FieldWhat it sets
Topic patternan ordinary MQTT filter: sensors/+/temp
Actionthin down to a rate or drop entirely
Messages per secondthe target rate; fractions are allowed (0.2 is one per five seconds)
Count byclient and topic, or the topic as a whole
Thinning keeps the latest value: a message arriving early is held, the next one replaces it, and the freshest goes out on the next tick. The publisher always receives a normal acknowledgement — otherwise it would consider the delivery failed and send everything again.

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 fieldMeaning
Filterwhich topics to forward
Directionout, in or both
QoSdelivery quality across the bridge
Prefixeswhat 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.

The bridge speaks QoS 0 and 1; QoS 2 is downgraded to 1, and QoS 1 delivery is best-effort.

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.

This API manages the built-in user list. If an external source is selected, the accounts live there and must be created in the source itself.

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”.