9.5 KiB
How-To: MQTT Broker + Live Map
This guide covers two parts: stand up a MeshCore MQTT broker and point the live map at it.
Current version: 1.8.4 (see VERSIONS.md).
1) MQTT broker (meshcore-mqtt-broker)
Use the broker repo from Michael Hart.
Install dependencies:
apt install git curl npm
Clone the repo:
git clone https://github.com/michaelhart/meshcore-mqtt-broker
cd meshcore-mqtt-broker
Create the data folder:
mkdir data
Create your .env:
cp .env.example .env
Edit .env and update the database path (use an absolute path that matches your checkout). While you are there, fill out the rest of the .env to your liking, and make sure AUTH_EXPECTED_AUDIENCE is set to the hostname of your MQTT server:
# Abuse Detection - Persistence
ABUSE_PERSISTENCE_PATH=/home/user/meshcore-mqtt-broker/data/abuse-detection.db
For the live map, the easiest broker auth setup is a dedicated read-only
subscriber account. Add at least one SUBSCRIBER_N entry to the broker
.env:
# Format: SUBSCRIBER_N=username:password:role
# Role 1 = admin (includes /internal + $SYS)
# Role 2 = full_access (recommended for most maps)
# Role 3 = limited (filtered payload fields)
SUBSCRIBER_1=meshmap:change-this-password:2
Recommended role:
- Use role
2for most live maps. - Use role
1only if you explicitly want/internaltopics or admin-only broker visibility.
Important:
- The live map does not generate MeshCore signed auth tokens on its own.
- Do not put a node-style username like
v1_<PUBLIC_KEY>in the map unless you are also handling JWT token generation and rotation elsewhere. - For normal map deployments against
meshcore-mqtt-broker, use aSUBSCRIBER_Nusername/password pair instead.
Build and run:
npm install
npm run build
npm start
Run it as a systemd service (optional)
Create /etc/systemd/system/meshcore-mqtt.service:
[Unit]
Description=MeshCore MQTT Broker
After=network.target
[Service]
Type=simple
User=user
WorkingDirectory=/home/user/meshcore-mqtt-broker
# Start the broker with node + tsx directly
ExecStart=/home/user/.nvm/versions/node/v22.21.1/bin/node /home/user/meshcore-mqtt-broker/node_modules/.bin/tsx src/server.ts
Restart=always
RestartSec=5
Environment=NODE_ENV=production
# Optional logging to files (you can drop these if you prefer journalctl only)
StandardOutput=append:/var/log/meshcore-mqtt.log
StandardError=append:/var/log/meshcore-mqtt-error.log
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable --now meshcore-mqtt
Cloudflare Tunnel for MQTT over TLS (optional)
Follow the broker docs for Cloudflare Tunnel setup:
https://github.com/michaelhart/meshcore-mqtt-broker/blob/main/docs/cloudflare-tunnels.md
Note: You can run plain TCP on port 1883, but using a tunnel (or another TLS front) avoids exposing the port on the host. If you do use TCP, make sure you lock it down.
2) Configure the live map
Clone the repo and enter it:
git clone https://github.com/yellowcooln/meshcore-mqtt-live-map
cd meshcore-mqtt-live-map
Copy .env.example to .env and update MQTT settings. The map expects MQTT over WebSockets by default:
MQTT_HOST=your-mqtt-host
MQTT_PORT=443
MQTT_USERNAME=meshmap
MQTT_PASSWORD=change-this-password
MQTT_TRANSPORT=websockets
MQTT_WS_PATH=/
MQTT_TLS=true
# Comma-separated list supported, e.g. meshcore/BOS/#,meshcore/CON/#
MQTT_TOPIC=meshcore/#
# MQTT online presence tuning (v1.5+)
MQTT_ONLINE_SECONDS=300
MQTT_ONLINE_STATUS_TTL_SECONDS=300
MQTT_ONLINE_INTERNAL_TTL_SECONDS=300
MQTT_ACTIVITY_PACKETS_TTL_SECONDS=300
MQTT_STATUS_OFFLINE_VALUES=offline,disconnected
Those MQTT_USERNAME / MQTT_PASSWORD values should match the subscriber
account you created in the broker .env, for example:
SUBSCRIBER_1=meshmap:change-this-password:2
Authentication summary:
meshcore-mqtt-brokersupports signed MeshCore node auth for publishers.- This live map is normally a read-only subscriber, so it should use a broker subscriber account instead.
- If you use subscriber role
3, some metadata fields are filtered out. Role2is the recommended default for maps.
Presence behavior:
/status+/internaldetermine whether a node is shown as MQTT online./packetsis tracked as feed activity and does not, by itself, mark a node online.
Optional coordinate overrides (for fixed node placement):
DEVICE_COORDS_FILE=/data/device_coords.json
Optional neighbor override controls (manual + auto):
NEIGHBOR_OVERRIDES_FILE=/data/neighbor_overrides.json
AUTO_NEIGHBOR_OVERRIDES_ENABLED=false
AUTO_NEIGHBOR_OVERRIDES_FILE=/data/neighbor_overrides.auto.json
AUTO_NEIGHBOR_ACTIVE_DAYS=7
AUTO_NEIGHBOR_MIN_EDGE_COUNT=3
AUTO_NEIGHBOR_REFRESH_SECONDS=60
Optional channel secrets file (for decrypting sender names from supported group-text packets):
CHANNEL_SECRETS_FILE=/data/channel_secrets.json
Copy channel_secrets.example.json to your chosen path and keep only the channels you want to ship.
Optional automatic backups:
BACKUP_ENABLED=true
BACKUP_INTERVAL_SECONDS=43200
BACKUP_DIR=/backup
BACKUP_RETENTION_DAYS=7
Notes:
- backups are written as timestamped
.tar.gzarchives likemeshmap-backup-2026-03-28T21-15-00.tar.gz - only files that currently exist are included
- the default compose setup mounts
./backup:/backup - live state stays under
/data; backups stay under/backup
Optional packet analyzer link base (used for Route Details hashes):
PACKET_ANALYZER_URL=https://analyzer.letsmesh.net/packets?packet_hash=
QR_CODE_BUTTON_ENABLED=false
PEERS_DEFAULT_LIMIT=8
MAP_BOUNDARY_MODE=radius
MAP_BOUNDARY_FILE=/data/map_boundary.json
MAP_BOUNDARY_SHOW=false
LOS_CURVATURE_ENABLED=true
LOS_CURVATURE_FACTOR=1.333333
ROUTE_ALLOW_AMBIGUOUS_ONE_BYTE_FALLBACK=false
LOS note:
- The live map LOS tool now includes Earth curvature by default.
- Leave
LOS_CURVATURE_ENABLEDunset to keep the defaulttrue. - Leave
LOS_CURVATURE_FACTORunset to keep the default1.333333.
Optional polygon boundary mode:
MAP_BOUNDARY_MODE=polygon
MAP_BOUNDARY_FILE=/data/map_boundary.json
MAP_BOUNDARY_SHOW=true
Boundary files:
- Use
map_boundary.example.jsonas the schema reference. - Open
tools/map-boundary-builder.htmldirectly in a browser, or use the hosted copy at https://yellowcooln.com/map-boundary-builder/, to click out a polygon and exportmap_boundary.json.
Optional: enable the coverage layer by setting COVERAGE_API_URL (the Coverage button hides itself when blank):
COVERAGE_API_URL=https://coverage.example.com
# The envs below are MeshMapper-only. The legacy coverage map does not use them.
# Optional for MeshMapper coverage.php requests
COVERAGE_API_KEY=
# Show only the last N days on the map. Default 30. Set 0 to disable age filtering.
COVERAGE_MAX_AGE_DAYS=30
# MeshMapper only: fallback cooldown after HTTP 429 if the API does not report resets_in_hours
COVERAGE_RATE_LIMIT_COOLDOWN_SECONDS=3600
# MeshMapper only: local file used to serve cached coverage to users
COVERAGE_CACHE_FILE=/data/coverage_cache.json
# MeshMapper only: server-side refresh interval; default hourly
COVERAGE_SYNC_INTERVAL_SECONDS=3600
#
# Routing note: on large meshes, ambiguous 1-byte prefixes are handled
# conservatively by default. If your network lost routes after v1.7.0 and you
# need the older behavior back, set:
# ROUTE_ALLOW_AMBIGUOUS_ONE_BYTE_FALLBACK=true
# That restores the legacy closest/time-based fallback for colliding 1-byte
# prefixes.
#
# MeshMapper uses the documented domain:
# COVERAGE_API_URL=https://meshmapper.net
WEATHER_RADAR_COUNTRY_BOUNDS_ENABLED=true
# Optional override. Default is /weather/radar/country-bounds on this app.
# This is an HTTP URL path, not a filesystem directory.
WEATHER_RADAR_COUNTRY_LOOKUP_URL=/weather/radar/country-bounds
WEATHER_WIND_ENABLED=true
WEATHER_WIND_API_URL=https://api.open-meteo.com/v1/forecast
WEATHER_WIND_GRID_SIZE=3
WEATHER_WIND_REFRESH_SECONDS=180
Optional: configure Weather (Radar + Wind) behavior:
# Master radar feature flag.
WEATHER_RADAR_ENABLED=true
# Keep radar clipped to the active country bounds around map center.
WEATHER_RADAR_COUNTRY_BOUNDS_ENABLED=true
# Keep default unless you run your own lookup endpoint.
WEATHER_RADAR_COUNTRY_LOOKUP_URL=/weather/radar/country-bounds
# Enable/disable wind overlay in the Weather panel.
WEATHER_WIND_ENABLED=true
# Open-Meteo compatible wind API endpoint.
WEATHER_WIND_API_URL=https://api.open-meteo.com/v1/forecast
# Wind sampling density (1-5): higher = more arrows + more API load.
WEATHER_WIND_GRID_SIZE=3
# Wind refresh interval in seconds (minimum 30).
WEATHER_WIND_REFRESH_SECONDS=180
If both WEATHER_RADAR_ENABLED=false and WEATHER_WIND_ENABLED=false, the Weather button is hidden.
If you are using plain TCP MQTT, set:
MQTT_TRANSPORT=tcp
MQTT_TLS=false
MQTT_PORT=1883
3) Run the map
Background (detached):
docker compose up -d --build
Foreground (watch logs):
docker compose up --build
Open the UI at http://localhost:8080.
4) Verify data flow
/snapshotshould show devices once adverts arrive./statsshows ingest totals.
Example:
curl -s http://localhost:8080/stats | jq
Notes:
- The map only places nodes once it decodes adverts that include a valid location.
- If no nodes appear, verify the MQTT topic and that the broker is sending
/packetsdata with raw hex payloads.