Skip to content

Real-Time Updates (SSE)

The Tech Strategy Tool uses Server-Sent Events (SSE) to push real-time notifications to connected clients. This page describes the SSE architecture, notification types, wire format, and client reconnection behavior.

When an event is processed on the server, the affected entities are broadcast to all connected clients via SSE. Clients then fetch fresh data for only the changed entities, keeping the UI in sync without polling.

The SseConnectionManager is a singleton service that maintains a ConcurrentDictionary of active connections. Each connection is backed by a Channel<string> that decouples the write (broadcast) side from the read (HTTP response stream) side.

Diagram
  1. Client connects to GET /api/sse (requires ViewerOrAbove authorization)
  2. SseConnectionManager.AddConnection() creates a channel for the connection
  3. A read loop writes from the channel to the HTTP response stream
  4. On disconnect (client closes tab, network failure), the connection is cleaned up

Two notification types are used, each triggering different client behavior. The distinction exists for performance: most edits are field changes on individual cards, and doing a full view reload for every keystroke-level edit would be wasteful and cause visible flicker.

Indicates that a specific entity’s data was updated.

Triggered by: field edits (name, description, progress), principle assignment/removal, initiative creation/deletion, Jira key changes.

Client behavior: Re-fetch only the affected card from the API. If the entity is in the current view, replace the local data. If the entity is not in the current view but is within scope (same team and view type), treat it as a new card. If outside scope entirely, ignore.

Indicates a structural change that affects the layout of the view.

Triggered by: group creation/renaming/deletion/reordering, team deletion, objective reordering, principle reordering, objective-to-group assignment changes.

Client behavior: Re-fetch the entire current view (all principles or all objectives for the current team). This is necessary because structural changes affect layout — card ordering, which group an objective belongs to, whether groups exist at all.

SSE messages follow the standard EventSource specification:

event: card-changed
data: {"entityType":"principle","entityId":"a1b2c3d4-..."}
event: view-reload
data: {"entityType":"team","entityId":"e5f6g7h8-..."}

Each message includes:

  • event: — the notification type (card-changed or view-reload)
  • data: — a JSON object with entityType and entityId

Notifications carry no payload data — only the identity of what changed. Clients are responsible for fetching the updated data.

The SseHeartbeatService is a hosted service that sends a comment line to all connections every 15 seconds:

: heartbeat

The heartbeat serves two purposes:

  1. Keep-alive — prevents intermediate proxies and load balancers from closing idle connections
  2. Staleness detection — clients monitor the time since the last received data; if no data arrives within 30 seconds, the connection is considered stale

The SSE stream does not use Last-Event-ID. Messages are ephemeral; the server maintains no replay state. This is a deliberate simplification:

  • The EventSource browser API handles reconnection automatically — when the connection drops, the browser reestablishes it
  • When reconnection succeeds, the client performs a full bootstrap reload of the current view (the same code path as initial page load and team/view switching)
  • Missed notifications during a disconnection are harmless — the bootstrap reload brings the client up to date

There is deliberately one way to get data: fetch it from the server. This eliminates any risk of the client holding a partially-updated local state.

SseClient uses JavaScript interop (wwwroot/js/sse-client.js) to create a browser EventSource:

  • On card-changed: check if the entity is in the current view; if yes, re-fetch that specific card from the API and update StrategyState
  • On view-reload: re-fetch the entire current view
  • On reconnection: trigger a full bootstrap reload of the current view
  • Staleness: a 10-second timer checks if the last message was received within 30 seconds. If not, ConnectionStatus is set to stale and the UI indicator changes.
  • Timeout fallback: if a field is in “Persisting” state and no SSE confirmation arrives within ~30 seconds, the client re-fetches the card from the API as if the notification had arrived

AdminSseClient uses the same JavaScript interop pattern but with simpler handling — any card-changed or view-reload notification triggers a single OnDataChanged event. The EventLog and Teams pages subscribe to this event and refresh their data.

The client tracks three connection states, displayed as a visual indicator in the UI header:

StatusIndicatorMeaning
ConnectedGreenReceiving data normally
StaleOrange (pulsing)No data received within 30 seconds — data may be outdated
DisconnectedRedEventSource is closed or in error state
FilePurpose
src/TechStrat.Api/Sse/SseConnectionManager.csConnection tracking and broadcast
src/TechStrat.Api/Sse/SseHeartbeatService.cs15-second heartbeat timer
src/TechStrat.Api/Endpoints/SseEndpoints.csGET /api/sse endpoint
src/TechStrat.Web/Services/SseClient.csStrategy Site SSE client
src/TechStrat.Web/wwwroot/js/sse-client.jsBrowser EventSource wrapper