Skip to main content

πŸ”Œ 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:

DirectionEventFields
TS β†’ PYstart_scanservice_uuid
TS β†’ PYstop_scanβ€”
TS β†’ PYconnect_and_writeaddress, service_uuid, characteristic_uuid, payload
PY β†’ TSreadyβ€”
PY β†’ TSdiscoveredaddress, name, rssi, service_uuids[], service_data{}
PY β†’ TSscan_started / scan_stoppedβ€”
PY β†’ TSwrite_resultaddress, ok, optional error

scripts/ble_bridge.py β†— β€” pure transport, no business logic.

Why Python

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 patternHandler
station/+/device/+/handshakedeviceService.onHandshake() β†’ publish handshake/ack
station/+/device/+/statedeviceService.onStateReported()
station/+/device/+/eventdeviceService.onEvent()
station/+/device/+/heartbeatdeviceService.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 REST
  • automation deviceCommand node executor β€” automation-driven commands
  • cloud-sync.station_reset handler β€” factory_reset command on all devices

Cloud Integration​

Two modules cooperate:

  • cloud/ β€” owns cloud_config table (single-row: station_token, claimed_at); exposes /api/cloud/status; manages active event channels
  • cloud-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():

MethodParamsReturnsEffect
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 typeEffect on station
identity_syncBulk upsert all members into local users table; rename station; set owner
station_renamedUpdate station.name
member_addedUpsert one user (linked by cloud_user_id)
member_updatedUpdate local users row by cloud_user_id
member_removedDelete local user by cloud_user_id
station_password_changedUpdate station_password_hash for that user
station_resetWipe 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 contain userId and role
  • On success sets request.user = { userId, role }
  • On failure: 401 Unauthorized
  • Skipped for OPTIONS and HEAD

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 fallback Device-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 with env.AGENT_TOKEN
  • Used by station-agent endpoints (network config, restart, etc.)

When to use which​

CallerHook
Mobile app via Cloud (relayed)n/a β€” cloud handles auth, station receives via JSON-RPC
Web SPA on LANverifyToken (+ authorize or requireStationRole)
ESP32 deviceverifyDeviceToken
station-agent (RPi)verifyAgentToken

Reference​