π 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 |