🔌 Backend Integrations
External boundaries: BLE provisioning (Python bleak), MQTT broker, Cloud WSS (JSON-RPC), and auth.
BLE Device Provisioning
The device-bootstrap/ module — Node side of BLE provisioning. Talks to scripts/ble_bridge.py (Python bleak) via stdin/stdout JSON, broadcasts candidates over WS to connected frontend clients, performs the BLE write to provision a found ESP32 with Wi-Fi creds + station info.
Files
modules/device-bootstrap/
bleBridge.ts — spawns Python subprocess; createBleBridge(logger)
bleProvisionService.ts — orchestrates scan + provision; emits candidates
provisioningManager.ts — WS client registry; broadcast candidates; provision()
bleProvisionRoutes.ts — REST endpoint POST /provisioning/add
deviceBootstrapRoutes.ts — REST endpoints for the agent (Wi-Fi config, etc.)
deviceBootstrapService.ts — agent-related logic
deviceBootstrapSchemas.ts
Lifecycle
Python Bridge Protocol
bleBridge.ts ↔ ble_bridge.py exchange newline-delimited JSON over stdio:
| Direction | Event | Fields |
|---|---|---|
| TS → PY | start_scan | service_uuid |
| TS → PY | stop_scan | — |
| TS → PY | connect_and_write | address, service_uuid, characteristic_uuid, payload |
| PY → TS | ready | — |
| PY → TS | discovered | address, name, rssi, service_uuids[], service_data{} |
| PY → TS | scan_started / scan_stopped | — |
| PY → TS | write_result | address, ok, optional error |
scripts/ble_bridge.py ↗ — pure transport, no business logic.
Node BLE bindings on Linux (Bluez) are unreliable. bleak is the most stable cross-platform BLE library. Subprocess + JSON-over-stdio gives clean isolation — the rest of the backend stays Node.
Scan Lifecycle
- First WS client connects →
provisioningManager.addClient()→ starts scan - Last WS client disconnects → stops scan, clears candidate cache
- Candidates accumulate while scan runs; new clients receive replay of current candidates
MQTT Bridge
Backend connects to the local Mosquitto broker (same Docker stack). The bridge is in mqtt/mqttClient.ts plus the device-core/adapters/mqtt.adapter.ts that wires topic handlers to the device service.
Topic Subscriptions
The MQTT adapter (registered during device-core/index.ts wiring) subscribes to:
| Topic pattern | Handler |
|---|---|
station/+/device/+/handshake | deviceService.onHandshake() → publish handshake/ack |
station/+/device/+/state | deviceService.onStateReported() |
station/+/device/+/event | deviceService.onEvent() |
station/+/device/+/heartbeat | deviceService.onHeartbeat() |
See 📡 MQTT Protocol for the full topic and payload reference.
Outbound Commands
publishCommand(externalId, payload) from mqtt/mqttClient.ts — used by:
device.service.sendCommand()— user-initiated commands via RESTautomationdeviceCommandnode executor — automation-driven commandscloud-sync.station_resethandler —factory_resetcommand on all devices
Cloud Integration
Two modules cooperate:
cloud/— ownscloud_configtable (single-row:station_token,claimed_at); exposes/api/cloud/status; manages active event channelscloud-sync/— handles inbound legacy messages from Cloud (identity sync, member events, station reset)
The transport is in ws/cloudClient.ts — a JSON-RPC peer wrapping a WebSocket to wss://<CLOUD_HOST>/ws/station.
Connection Modes
JSON-RPC Methods Registered (Cloud → Station)
After auth_ok, the station registers handlers callable by Cloud via peer.call():
| Method | Params | Returns | Effect |
|---|---|---|---|
device.command | { deviceId, command } | { status: 'ok' } | Forwards to MQTT via devicesService.sendCommandDirect() |
devices.snapshot | — | { devices: Device[] } | Returns all local devices |
device_types.list | — | { deviceTypes: DeviceType[] } | Returns the registry |
station.setActiveChannels | { channels: EventChannel[] } | { ok: true } | Filters which event categories Cloud receives |
Outbound (Station → Cloud)
The cloud adapter in device-core/adapters/cloud.adapter.ts calls peer.notify(...) for state changes (see Domain → device-core adapters).
Heartbeat
Backend pings the WSS every 30s; if no pong within the next interval — terminate() and reconnect with exponential backoff (2s → 60s, 20% jitter).
cloud-sync Message Handlers
The Cloud sends "legacy" plain-JSON messages (not JSON-RPC) for identity changes — peer.onLegacyMessage forwards them to processCloudMessage:
| Message type | Effect on station |
|---|---|
identity_sync | Bulk upsert all members into local users table; rename station; set owner |
station_renamed | Update station.name |
member_added | Upsert one user (linked by cloud_user_id) |
member_updated | Update local users row by cloud_user_id |
member_removed | Delete local user by cloud_user_id |
station_password_changed | Update station_password_hash for that user |
station_reset | Wipe cloud_config, broadcast factory_reset to all devices, exit process |
Auth Hooks Deep Dive
hooks/authHooks.ts ↗ and hooks/deviceAuthHooks.ts ↗.
verifyToken — JWT user
fastify.addHook('preHandler', verifyToken);
// or per-route:
fastify.post('/path', { preHandler: verifyToken }, ...);
- Reads
Authorization: Bearer <jwt> jwt.verify(token, env.JWT_SECRET)— must containuserIdandrole- On success sets
request.user = { userId, role } - On failure:
401 Unauthorized - Skipped for
OPTIONSandHEAD
authorize(roles) — global role
{ preHandler: [verifyToken, authorize(['owner', 'admin'])] }
Pure check against request.user.role. Global, not per-station. 403 on mismatch.
requireStationRole(...) — station-membership-scoped
{ preHandler: [verifyToken, requireStationRole('owner')] }
Stronger check — fetches the station's owner from station table; if user is owner → role = owner; otherwise looks up station_members for the user's role. 403 if user is not in station_members and not the owner.
This is the right hook for station-scoped operations (e.g. inviting members).
verifyDeviceToken — ESP32 → backend
{ preHandler: verifyDeviceToken }
- Reads
X-Device-Token(or fallbackDevice-Token) header SELECT id FROM devices WHERE device_token = $1- On success sets
request.deviceId - Used by routes the firmware calls (e.g. event reporting outside MQTT)
verifyAgentToken — station-agent → backend
{ preHandler: verifyAgentToken }
- Reads
Authorization: Bearer <token>, compares plain-equality withenv.AGENT_TOKEN - Used by station-agent endpoints (network config, restart, etc.)
When to use which
| Caller | Hook |
|---|---|
| Mobile app via Cloud (relayed) | n/a — cloud handles auth, station receives via JSON-RPC |
| Web SPA on LAN | verifyToken (+ authorize or requireStationRole) |
| ESP32 device | verifyDeviceToken |
| station-agent (RPi) | verifyAgentToken |