/

Developer Guide

Socket Service

Implementation reference for the socket-service. For its architectural role, see Socket Service Architecture.

A small Node.js service built on Socket.IO and a Redis subscriber. It is the bridge between the broker and the browser: it relays alerts and per-building telemetry to connected clients over WebSockets. It has no database and no domain model. The code follows a functional core / imperative shell split: pure relay helpers and token verification in core/ and auth.ts, the broker- and socket-event handlers in handlers/, the metrics registry in config/, and a thin composition root in index.ts that wires them together.

Connections are authenticated: a Socket.IO handshake middleware decodes the mesh-verified claims header before any event is processed, so the service knows which account and domains each socket belongs to.

graph TD
    IDX["index.ts\ncomposition root"] --> AUTH["auth.ts\nauthenticateClaimsHeader"]
    IDX --> CONN["handlers/connection.ts"]
    IDX --> TEL["handlers/telemetry.ts"]
    IDX --> NOTIF["handlers/notification.ts"]
    CONN --> REL["core/relay.ts\npure: roomForBuilding, roomForDomain, buildingIdFromChannel"]
    TEL --> REL
    NOTIF --> REL
    IDX --> METRICS["config/metrics.ts"]

Configuration

VariableDefaultPurpose
REDIS_URLemptyBroker connection for the subscriber.
FRONTEND_URLhttp://localhost:5173Allowed CORS origin for the Socket.IO server (credentials enabled).

The HTTP/WebSocket server listens on port 3000. A GET /health endpoint backs the Kubernetes probes.


Authentication

A Socket.IO handshake middleware (io.use) runs once per connection, before any event handler:


Broker Subscriptions and Emissions

At startup the service connects a Redis subscriber and binds two channels to two Socket.IO events:

Redis subscriptionSocket.IO emissionAudience
notificationsemit notification to room domain:{name}, or to every socketThe targeted domain’s members; unscoped messages broadcast to all.
telemetry:filtered:* (pattern)emit telemetry to room building:{id}Only clients in that building’s room.

For the pattern subscription, the building id is recovered from the channel name (stripping the telemetry:filtered: prefix) and used to target the room. Each incoming message is JSON-parsed before emission.

Notifications are domain-scoped: if the payload carries a domainName, only that domain’s room receives it, so members of one tenant never see another’s alerts. A payload without a domainName (a system-wide message) is broadcast to every client.


Client Subscription Protocol

Domain rooms are not client-controlled: on connection the service reads the verified identity and joins the socket to a domain:{name} room for each of the account’s memberships. The client only chooses which buildings to follow:

Client eventHandler effect
subscribe_building(buildingId)Joins room building:{buildingId} to start receiving that building’s telemetry. Ignored if the account has no domain memberships.
unsubscribe_building(buildingId)Leaves room building:{buildingId} to stop receiving it.
sequenceDiagram
    participant UI as Vue client
    participant WS as socket-service
    participant R as Redis
    UI->>WS: connect (authentication_token cookie)
    Note over WS: Istio verifies the JWT, injects x-gateway-claims — WS decodes it, joins domain:{...} rooms
    UI->>WS: subscribe_building(B)
    Note over WS: member? → socket joins room building:B
    R-->>WS: telemetry:filtered:B message
    WS->>UI: emit "telemetry" (room building:B only)
    R-->>WS: notifications message (domainName=D)
    WS->>UI: emit "notification" (room domain:D only)

Telemetry is room-scoped so each browser receives only the buildings it is displaying; notifications are scoped to the recipient’s domain so tenants stay isolated. Only a system-wide notification with no domainName is broadcast to everyone.

⚠️ Per-building authorization (verifying the account’s domain actually owns the requested building) needs a twin-service lookup and is deferred; today the guard is domain-membership presence.


Scaling Considerations

The service runs as a single Socket.IO process. Two changes are required to run multiple replicas:

For where this service sits in the end-to-end real-time path, see Communication & Data Flow.