Per-Build WebSocket Stream
sparki intermediate 4 min read
ELI5
Each build has its own radio channel. You may tune in only if you belong to the workspace that owns the build. The station never waits for a slow listener: if your radio cannot keep up, you are switched off, and you catch up by reading the transcript, which is kept in the database.
Technical Deep Dive
Source: sparki-tools/api-engine pkg/api/server.go:304-332, pkg/api/handlers/builds.go:680-691, pkg/websocket/manager.go:14-240,282-290, pkg/api/handlers/worker.go:549-633 @0b0309f, read 2026-10-25. The previous revision described an internal/websocket hub with global broadcast, rooms and direct messages; the deployed engine has only per-build fan-out.
Who may connect
builds.Use("/:id/stream", AuthorizeBuildStream, UpgradeMiddleware) runs before the upgrade. AuthorizeBuildStream parses the build id and checks that the caller is a member of the build’s workspace (builds.go:682-691). The route sits under /api/v1, so the airlock session check has already run.
sequenceDiagram participant B as Browser participant E as api-engine participant M as websocket.Manager participant K as build-worker B->>E: GET /api/v1/builds/ID/stream with session cookie E->>E: airlock session, then workspace membership E->>M: register client under build ID K->>E: POST /internal/worker/builds/ID/logs E->>E: store new lines in build_logs E->>M: BroadcastLog per new line M-->>B: JSON log frame K->>E: POST complete E->>M: BroadcastStatus M-->>B: JSON status frameShape
classDiagram class Manager { -map buildClients -chan register 10 -chan unregister 10 -chan broadcast 100 +BroadcastLog(buildID, LogMessage) +BroadcastStatus(buildID, StatusMessage) } class Client { +string ID +UUID BuildID +chan Send 256 } class LogMessage { +string build_id +time timestamp +string line +int sequence +string stream +string type } class StatusMessage { +string build_id +string status +time timestamp +string message } Manager --> Client : per build Manager --> LogMessage Manager --> StatusMessageBack-pressure
BroadcastLogandBroadcastStatuswait at most 100 ms for room on the 100-slot broadcast channel, then drop the message and return nil (manager.go:212-238).- When a client’s 256-slot
Sendbuffer is full, the manager unregisters that client rather than blocking (manager.go:195-201). - The manager is in memory, per API process. Railway runs one engine replica (UNVERIFIED for the current service; the engine’s
railway.tomlsets no replica count). A second replica would not see the first one’s subscribers.
The live stream is best-effort. The durable record is build_logs, written once per (build_id, seq) before the broadcast, and served by GET /api/v1/builds/:id/logs.
Key Terms
- buildClients → map from build UUID to that build’s connected clients.
- stream →
stdout,stderrorsystem; system lines are prefixed[sparki]. - AuthorizeBuildStream → pre-upgrade workspace membership check.
Q&A
Q: Who is allowed to open /api/v1/builds/:id/stream, and when is that checked?
A: A signed-in member of the workspace that owns the build. The session is checked by the /api/v1 auth middleware and membership by AuthorizeBuildStream, both before the WebSocket upgrade.
Q: What happens to a log line when a client or the broadcast channel is slow? A: If the broadcast channel stays full for 100 ms the message is dropped; if one client’s send buffer is full, that client is disconnected. Neither case blocks the worker’s log upload.
Q: Where does a client get the lines it missed?
A: From GET /api/v1/builds/:id/logs, which reads build_logs. Every accepted line is stored there before it is broadcast.
Examples
A user opens a build page on a slow connection. Live frames arrive until the send buffer fills, then the manager drops the connection. The correct recovery for any client is to reconnect, fetch /logs to fill the gap by sequence number, and resume the stream. Whether web-app does exactly this was not checked for this card (UNVERIFIED).
neighbors on the map
- WebSocket Session Lifecycle adding a new privileged WS handler
- NATS Subject Taxonomy wiring a new consumer to the right stream
- Presence Broadcast Channels diagnosing missing cursor updates in the editor