mqtt

star 101

Use when working with Synchronet's MQTT integration — discovering whether MQTT is enabled (the `[MQTT]` section of `ctrl/main.ini`, **not** `sbbs.ini`), reading broker address/port/credentials/TLS settings, connecting with `mosquitto_sub`/`mosquitto_pub` (anonymous, user+pass, or one of four TLS modes: CA, mTLS, PSK, SBBS-internal-CA), or interacting with the topic tree for **monitoring** (server/node status, node-output spying, `action/#`, retained `login_attempts/<IP>` and `max_concurrent/<IP>`, log streams `…/log[/<N>]`) or **controlling** the BBS (production-impacting topics that recycle/pause/resume servers, clear failed-login lists, set node flags, inject input or messages into a node — plus the local-filesystem equivalents in `ctrl/`). Trigger on "is MQTT enabled", "what's the broker address", "subscribe to all sbbs logs", "pause/recycle via MQTT", "inject a message into node N", "clear failed-login for an IP", or "spy on a node remotely".

SynchronetBBS By SynchronetBBS schedule Updated 6/2/2026

name: mqtt description: Use when working with Synchronet's MQTT integration — discovering whether MQTT is enabled (the [MQTT] section of ctrl/main.ini, not sbbs.ini), reading broker address/port/credentials/TLS settings, connecting with mosquitto_sub/mosquitto_pub (anonymous, user+pass, or one of four TLS modes: CA, mTLS, PSK, SBBS-internal-CA), or interacting with the topic tree for monitoring (server/node status, node-output spying, action/#, retained login_attempts/<IP> and max_concurrent/<IP>, log streams …/log[/<N>]) or controlling the BBS (production-impacting topics that recycle/pause/resume servers, clear failed-login lists, set node flags, inject input or messages into a node — plus the local-filesystem equivalents in ctrl/). Trigger on "is MQTT enabled", "what's the broker address", "subscribe to all sbbs logs", "pause/recycle via MQTT", "inject a message into node N", "clear failed-login for an IP", or "spy on a node remotely".

Synchronet MQTT — monitor and control

Reference (authoritative):

MQTT in Synchronet is two-way. Most use cases are read-only monitoring, but the same broker also carries control-plane topics that recycle servers, pause/resume listeners, clear filter lists, set node flags, and inject input. Read the Control plane — production-impacting operations section before publishing anything.

Step 1 — discover whether MQTT is enabled and how to connect

The [MQTT] section lives in ctrl/main.ini, not ctrl/sbbs.ini. (This trips a lot of people; the rest of Synchronet's server/daemon config is in sbbs.ini, but MQTT belongs to BBS-level system settings in main.ini.) Read it:

grep -A 20 '^\[MQTT\]' <sbbs>/ctrl/main.ini

Keys (defaults shown; full source of truth is scfglib1.c::read_main_cfg):

Key Default Meaning
Enabled false master switch; if false, no topics are published or accepted
InternalBroker false if true, Synchronet runs its own embedded broker (no external mosquitto needed); otherwise connect to the address below
Broker_addr 127.0.0.1 IPv4/IPv6/hostname of the MQTT broker
Broker_port 1883 (IPPORT_MQTT) broker TCP port (typically 1883 plain, 8883 TLS)
Username empty MQTT username; empty = anonymous
Password empty MQTT password; plain text in the file — restrict file permissions
Keepalive 60 (seconds; range 5–INT_MAX) client keepalive interval
Protocol_version 4 (range 3–5; 3=v3.1, 4=v3.1.1, 5=v5) MQTT protocol version. v5 unlocks per-message user properties (Synchronet uses these for log-level tagging on …/log without level suffix).
Publish_QOS 0 (range 0–2) QoS for messages Synchronet publishes
Subscribe_QOS 2 (range 0–2) QoS for subscriptions Synchronet places
LogLevel Info minimum severity of console lines republished to MQTT
Verbose true publish per-node status, per-server status, and full log streams. false reduces to terminal status only. (Mirrors SCFG's "Publish Verbosity = High" vs "Low".)
TLS_mode 0 (disabled) see TLS table below
TLS_cafile empty path to a CA cert/bundle (used by modes 1, 2)
TLS_certfile empty path to client certificate (mode 2)
TLS_keyfile empty path to client private key (mode 2)
TLS_keypass empty passphrase for the client key, if encrypted
TLS_psk empty pre-shared key, hex-encoded (mode 3)
TLS_identity empty PSK identity string (mode 3)

TLS_mode enumeration (from mqtt.h):

Value Name What it means What you need
0 MQTT_TLS_DISABLED plain TCP nothing
1 MQTT_TLS_CA TLS, broker cert verified against a CA bundle TLS_cafile
2 MQTT_TLS_CERT mTLS — client also presents a cert TLS_cafile, TLS_certfile, TLS_keyfile (+ TLS_keypass if encrypted)
3 MQTT_TLS_PSK pre-shared-key TLS TLS_psk (hex), TLS_identity
4 MQTT_TLS_SBBS use Synchronet's own internal CA and the BBS's web/server cert nothing extra; the BBS handles it

Security note: ctrl/main.ini contains Password= and possibly TLS_keypass= in plain text. The default install on a typical Linux host is world-readable; tighten with chmod 600 ctrl/main.ini (and chown to the BBS user) if your threat model includes other local users.

Step 2 — connect with mosquitto_sub / mosquitto_pub

Once you know broker/port/auth/TLS from main.ini, plain-TCP (TLS_mode=0) anonymous:

mosquitto_sub -h <broker> -p <port> -v -t 'sbbs/#'

With username/password:

mosquitto_sub -h <broker> -p <port> -u <user> -P <pass> -v -t 'sbbs/#'

With TLS mode 1 (CA-verified broker, anonymous):

mosquitto_sub -h <broker> -p <port> --cafile <ca-bundle> -v -t 'sbbs/#'

With TLS mode 2 (mTLS):

mosquitto_sub -h <broker> -p <port> \
  --cafile <ca> --cert <client-cert> --key <client-key> \
  -v -t 'sbbs/#'

With TLS mode 3 (PSK):

mosquitto_sub -h <broker> -p <port> --psk <hex-key> --psk-identity <identity> \
  -v -t 'sbbs/#'

TLS mode 4 (MQTT_TLS_SBBS) uses the BBS's own CA + cert internally — an external client (mosquitto_sub) doesn't get that bundle automatically; you'd need the broker's CA file out-of-band to verify it, or just use --insecure for quick diagnostics (skips hostname verification — fine for a local test broker, not for production).

MQTT protocol versionmosquitto_sub/-pub default to v3.1.1 (4). Match the broker's expectation; for v5 add -V 5. Note that the level-as-user-property convention on Synchronet's bare …/log topics only works on v5.

Native access from Synchronet JavaScript (the MQTT object)

mosquitto_sub/-pub are right for shell diagnostics, but inside Synchronet (a JS module, service, or a chat-bot tool) use the built-in MQTT object — it reads the broker address / port / credentials / TLS straight from the [MQTT] section of main.ini, so there are no hardcoded broker details or secrets in your script, and it speaks the same config the BBS itself uses.

if (!system.mqtt_enabled)            // gate first -- false if [MQTT] Enabled=false
    return;
var mqtt = new MQTT;
if (!mqtt.connect()) {               // no args: auto-configured from main.ini
    log(LOG_WARNING, "MQTT connect failed: " + mqtt.error_str);
    return;
}
mqtt.subscribe("sbbs/" + system.qwk_id + "/host/+/server/+");
var msg;
while ((msg = mqtt.read(1000, /* object: */ true)) !== false) {
    // msg.topic, msg.data   (retained messages arrive immediately on connect)
    print(msg.topic + " => " + msg.data);
}

Key members (the object extends Socket, so socket props apply too):

Member Purpose
system.mqtt_enabled bool — is MQTT configured/on? Gate every MQTT path on this.
system.qwk_id the <BBSID> for building topics ("sbbs/" + system.qwk_id + …)
new MQTT / mqtt.connect() connect using main.ini's broker/auth/TLS (no args)
mqtt.subscribe(topic [,qos]) / mqtt.subscribe_qos subscribe (wildcards + / # allowed)
mqtt.read(timeout_ms, object) next message; object=true{topic,data}, else raw; returns false on timeout
mqtt.publish(retain, qos, topic, data) publish (see control-plane warnings before using)
mqtt.broker_addr / broker_port / username / password the resolved connection settings (defaults from main.ini)
mqtt.error / error_str / library last-call result + library name/version

Stock JS examples in exec/ (read these before writing your own):

  • mqtt_sub.js — generic subscriber (the connect/subscribe/read loop above).
  • mqtt_pub.js — generic publisher.
  • mqtt_spy.js — subscribe to a node's …/output and render the live screen.
  • mqtt_stats.jspublishes system.stats to sbbs/<BBSID>/stats[/<field>].

Local object model vs. MQTT — pick the right source. For this instance's own state, the JS object model is simpler and needs no broker: system.stats (logons/posts/uploads today, totals), system.node_list / the Node-status files, bbs/client in a session. Reach for MQTT specifically when you need what the local objects cannot give you: the cross-host, whole-BBS live picture (every member host's server states and client counts under one BBSID), or a push stream of action/# events. Don't round-trip through the broker for a number system.stats already has.

Read-only unless you mean it. Everything under Control plane (recycle, pause, node/+/input, node/+/intr, …) is a publish() away. A tool exposed to untrusted callers (an IRC bot, a public command) must subscribe only and never publish to a control topic. Treat MQTT data as read-only situational awareness there.

Topic hierarchy at a glance

All topics start with sbbs/<BBSID> where <BBSID> is the QWK BBS ID (SCFG → Message Options). Under that:

sbbs/<BBSID>                            ← BBS name (retained payload)
sbbs/<BBSID>/node                       ← total node count
sbbs/<BBSID>/node/<N>                   ← human-readable status (verbose mode)
sbbs/<BBSID>/node/<N>/status            ← tab-delimited node status fields (see exec/load/nodedefs.js)
sbbs/<BBSID>/node/<N>/terminal          ← tab-delimited last-connected terminal (cols, rows, syncterm, ANSI, CP437, …)
sbbs/<BBSID>/node/<N>/output            ← live terminal output (for spying)
sbbs/<BBSID>/action/<KIND>/<KEY>        ← BBS-wide client actions (see Action topics below)
sbbs/<BBSID>/host/<HOSTNAME>            ← public host name (retained)
sbbs/<BBSID>/host/<HOSTNAME>/login_attempts/<IP>  ← retained failed-login aggregate per IP
sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>      ← server status line (state + counters)
sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>/state/<STATE>  ← retained per-state flag (e.g. .../state/ready)
sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>/served   ← total clients served since start (retained)
sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>/client   ← "<N> total\t<M> max" current/limit (retained)
sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>/client/list   ← tab-delimited rows, one per connected client (retained)
sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>/log      ← console log (level in v5 user property)
sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>/log/<N>  ← console log filtered to level N
sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>/client/action/<KIND>  ← per-server client events (connect/disconnect/update)
sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>/error_count           ← total errors since server start (retained)
sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>/max_concurrent/<IP>   ← retained term concurrent-connection strikes
sbbs/<BBSID>/host/<HOSTNAME>/event/log[/<N>]      ← event-thread log

<SERVER> is one of: term (Terminal), mail (Mail), ftp (FTP), web (Web), srvc (Services).

One BBSID spans every host. All hosts that share a QWK ID publish under the same sbbs/<BBSID> tree, each as its own host/<HOSTNAME>. So a single subscription to sbbs/<BBSID>/host/+/server/+ gives the live, cross-host state of the whole BBS (e.g. Vertrauen's vert, git, plus other member hosts) — something the in-process JS object model can't see, since it only knows its own instance. The server status line is tab-delimited: <state>\t[<cur>/<max> clients]\t<served> served\t[<n> errors].

Action topics (BBS-wide client events, non-retained)

sbbs/<BBSID>/action/hack/<METHOD>       ← suspected hack attempt
sbbs/<BBSID>/action/spam/<ACTION>       ← suspected SPAM
sbbs/<BBSID>/action/error/<LEVEL>       ← unexpected condition
sbbs/<BBSID>/action/exec/<PROGCODE>     ← external program executed
sbbs/<BBSID>/action/login/<PROTOCOL>    ← successful auth
sbbs/<BBSID>/action/login_fail/<PROTOCOL>   ← failed auth

These fire one message per event, not retained — a fresh subscriber sees only events from connect-time forward. Pair with the retained login_attempts/<IP> snapshot for "current state + live updates".

Server state strings

sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER> carries a status string whose first field is one of:

State Meaning
stopped server not running
initializing starting up
ready accepting clients
paused running but rejecting new connections (see pause control)
reloading re-reading config
stopping shutting down
disconnected MQTT client lost the broker; published from a will-message

Retention semantics

Most Synchronet MQTT messages are published retained. Retained messages persist on the broker until either the broker restarts (without persistence configured) or someone publishes an empty payload to the same topic to clear it. Consequence: a fresh mosquitto_sub connection replays the latest retained value of every matched topic immediately — useful for dashboards, surprising if you expect an event stream.

The non-retained topics are: action/#, node/+/output, and the various log topics (those flow live).

Control plane — production-impacting operations

Anything below this line affects users connected to the BBS in real time. Read the warnings before publishing.

Synchronet listens for control messages on the same broker. Most accept any payload (empty is fine) — they trigger a flag rather than passing data. Where payload matters it's noted.

Host-wide controls (affect every server on a host)

sbbs/<BBSID>/host/<HOSTNAME>/recycle     ← restart ALL servers on that host
sbbs/<BBSID>/host/<HOSTNAME>/pause       ← stop accepting new connections on ALL servers
sbbs/<BBSID>/host/<HOSTNAME>/resume      ← undo pause
sbbs/<BBSID>/host/<HOSTNAME>/clear       ← clear failed-login attempt list + term max-concurrent strike list
                                            payload: empty = clear everything; "<IP>" = clear only that IP

recycle will drop in-flight connections during the restart window. pause keeps existing users connected but new logins fail until resume. Don't publish to these unless you mean it — there's no confirmation prompt.

The same effects can be triggered locally (no MQTT needed) by creating a flag file in ctrl/:

File Equivalent topic
ctrl/recycle (or recycle.term, recycle.web, …) host/+/recycle (or server/+/recycle)
ctrl/clear (or clear.term, clear.ftp, …) host/+/clear (or server/+/clear)

The flag file's first line is read as an optional IP filter (for clear); empty content means "clear all". The file is removed by Synchronet once consumed.

Per-server controls

sbbs/<BBSID>/host/<HOSTNAME>/server/<SERVER>/clear   ← clear that server's view of the failed-login list
                                                       (and, for term, max-concurrent strikes)
                                                       payload: empty or "<IP>"

Per-server recycle / pause / resume follow the same pattern (one server only, instead of all).

Node controls (Terminal Server)

sbbs/<BBSID>/node/<N>/input             ← inject keyboard input into the connected user's session (payload = keys to send)
sbbs/<BBSID>/node/<N>/msg               ← send a short text message to the node (payload = text, terminate with \n)
sbbs/<BBSID>/node/<N>/set/status        ← set node status integer; "0"=waiting, "5"=offline
sbbs/<BBSID>/node/<N>/set/errors        ← set error counter ("0" to clear)
sbbs/<BBSID>/node/<N>/set/misc          ← set misc attribute flags; integer ("0x"-prefixed = hex)
sbbs/<BBSID>/node/<N>/set/lock          ← only sysop can login; "0" to clear
sbbs/<BBSID>/node/<N>/set/intr          ← interrupt (disconnect) the user; "0" to clear
sbbs/<BBSID>/node/<N>/set/down          ← mark node unavailable (status = 5); "0" to clear
sbbs/<BBSID>/node/<N>/set/rerun         ← reload config on next connection; "0" to clear

input is essentially remote keystrokes — anything you send is interpreted by the user's current screen as if typed. intr disconnects the user immediately. down prevents anyone from getting that node until cleared. All four affect the live user; use with care.

Safety patterns

  • Subscribe before you publish. Run mosquitto_sub -v -t 'sbbs/+/host/+/server/+/log' -t 'sbbs/+/action/#' -t 'sbbs/+/host/+/server/+' in another terminal so you can see what your control message did.
  • Prefer the narrowest scope. A per-server recycle is less disruptive than a host-wide one; a single-IP clear/<IP> is much less than a wholesale clear.
  • Test with non-destructive ops first. Status snapshots (retained) and error_count reads are free of side effects; use them to confirm the topics work before issuing a pause or intr.
  • Beware retained control topics. If a sysop has accidentally retained a pause message in the past, the next time the BBS reconnects to the broker it could replay the retained message and re-pause itself. Clear stale retained control messages by publishing an empty payload to the same topic with -r.

Recipes

"Is MQTT enabled on this host, and where's the broker?"

awk '/^\[MQTT\]/,/^\[/' <sbbs>/ctrl/main.ini

If Enabled=false (or the section is missing), no further work is possible until SCFG → Networks → MQTT is set up. If InternalBroker=true, the broker is Synchronet itself (default port Broker_port); otherwise connect to Broker_addr:Broker_port.

"Subscribe to every log line across every host on the broker."

mosquitto_sub -h <broker> -p <port> -V 5 -v \
  -t 'sbbs/+/host/+/server/+/log' \
  -t 'sbbs/+/host/+/event/log'

(-V 5 so you get the level user-property on the bare …/log topic.)

"What's happening across the BBS right now — actions only, terse."

mosquitto_sub -h <broker> -p <port> -v -t 'sbbs/+/action/#'

"Show the current per-IP failed-login state (retained dashboard)."

mosquitto_sub -h <broker> -p <port> -v -t 'sbbs/+/host/+/login_attempts/+'

A fresh subscriber sees every tracked IP's last-attempt payload immediately (retained replay), then updates live.

"Spy on what node 3 is sending to the user (remote screen capture)."

mosquitto_sub -h <broker> -p <port> -t 'sbbs/<BBSID>/node/3/output'

The payload is the terminal byte stream (CP437 by default, including ANSI control sequences) — pipe through a terminal-capable viewer if you want it rendered.

"Clear a single offending IP from the failed-login list, host-wide."

mosquitto_pub -h <broker> -p <port> -t 'sbbs/<BBSID>/host/<HOSTNAME>/clear' -m '203.0.113.42'

(Or, locally: echo 203.0.113.42 > <sbbs>/ctrl/clear.)

"Recycle just the web server on host X."

mosquitto_pub -h <broker> -p <port> -t 'sbbs/<BBSID>/host/<HOSTNAME>/server/web/recycle' -m ''

Verify the result by watching the matching …/server/web status topic — you should see stoppinginitializingready within seconds.

Common mistakes

  • Looking for [MQTT] in sbbs.ini. It's in main.ini. sbbs.ini has the per-server (term/web/mail/ftp/services) settings; MQTT is a BBS-level concern and lives with the rest of the system config in main.ini.
  • Forgetting -V 5. Without it, mosquitto_sub defaults to v3.1.1 and you don't see the MQTT v5 user property carrying the log level on bare …/log topics — you'd have to subscribe to all eight …/log/<N> subtopics instead.
  • Reading the password from main.ini and pasting it into a shell history. Use -P "$(awk -F= '/^[[:space:]]*Password=/{print $2}' <sbbs>/ctrl/main.ini)" or set MOSQUITTO_PWD and read with -P "$MOSQUITTO_PWD". Better still, configure a dedicated read-only MQTT user for diagnostics.
  • Treating action topics as retained. They aren't — a fresh subscriber sees only new events. For pre-connect state, use the retained login_attempts/<IP> and max_concurrent/<IP> topics in parallel.
  • Publishing to a control topic without -r '' cleanup. If you publish a one-shot control with -r set (retained), Synchronet may re-consume it on reconnect; clear by publishing an empty retained payload to the same topic afterwards.
  • Subscribing to node/+/output and dumping straight to a terminal. The payload contains ANSI CSI sequences that may reposition your cursor, clear your screen, change colours, etc. Pipe to a file or a controlled viewer for analysis.
  • Forgetting Verbose=true. In false/"Low" mode, per-server status and full log streams are suppressed; you'll only see terminal node status. Bump it in SCFG → Networks → MQTT → Publish Verbosity if you need the rest.
  • Sending a node/+/msg without a trailing newline. Per the wiki, the message payload must end with \n to be delivered cleanly; use -m $'<text>\n' in bash.
  • Using TLS mode 4 (MQTT_TLS_SBBS) without the BBS's CA out-of-band. That mode uses Synchronet's internal CA and the BBS's own cert; an external client can't validate it without the CA file. For local diagnostics use --insecure; for production, distribute the CA properly or use TLS mode 1.

Cross-references

  • For the file/console log streams (per-category files in data/, syslog/journalctl on *nix, the WS*.LOG sbbsctrl-on-Windows trap, and the ip.can field format) — see the logs skill.
  • For low-level config parsing in scfglib1.c (the source of truth for the [MQTT] defaults and ranges quoted here) — search for read_main_cfg in src/sbbs3/scfglib1.c.
  • For MQTT-related JS host bindings (MQTT object, mqtt.publish(), etc.) — see the javascript skill.
Install via CLI
npx skills add https://github.com/SynchronetBBS/sbbs --skill mqtt
Repository Details
star Stars 101
call_split Forks 18
navigation Branch main
article Path SKILL.md
More from Creator
SynchronetBBS
SynchronetBBS Explore all skills →