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.
Overview
Section titled “Overview”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.
Architecture
Section titled “Architecture”SseConnectionManager
Section titled “SseConnectionManager”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.
Connection lifecycle
Section titled “Connection lifecycle”- Client connects to
GET /api/sse(requiresViewerOrAboveauthorization) SseConnectionManager.AddConnection()creates a channel for the connection- A read loop writes from the channel to the HTTP response stream
- On disconnect (client closes tab, network failure), the connection is cleaned up
Notification Types
Section titled “Notification Types”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.
card-changed
Section titled “card-changed”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.
view-reload
Section titled “view-reload”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.
Wire Format
Section titled “Wire Format”SSE messages follow the standard EventSource specification:
event: card-changeddata: {"entityType":"principle","entityId":"a1b2c3d4-..."}
event: view-reloaddata: {"entityType":"team","entityId":"e5f6g7h8-..."}Each message includes:
event:— the notification type (card-changedorview-reload)data:— a JSON object withentityTypeandentityId
Notifications carry no payload data — only the identity of what changed. Clients are responsible for fetching the updated data.
Heartbeat
Section titled “Heartbeat”The SseHeartbeatService is a hosted service that sends a comment line to all connections every 15 seconds:
: heartbeatThe heartbeat serves two purposes:
- Keep-alive — prevents intermediate proxies and load balancers from closing idle connections
- Staleness detection — clients monitor the time since the last received data; if no data arrives within 30 seconds, the connection is considered stale
Reconnection Strategy
Section titled “Reconnection Strategy”The SSE stream does not use Last-Event-ID. Messages are ephemeral; the server maintains no replay state. This is a deliberate simplification:
- The
EventSourcebrowser 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.
Client Implementation
Section titled “Client Implementation”Strategy Site
Section titled “Strategy Site”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 updateStrategyState - 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,
ConnectionStatusis 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
Admin Site
Section titled “Admin Site”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.
Connection Status
Section titled “Connection Status”The client tracks three connection states, displayed as a visual indicator in the UI header:
| Status | Indicator | Meaning |
|---|---|---|
| Connected | Green | Receiving data normally |
| Stale | Orange (pulsing) | No data received within 30 seconds — data may be outdated |
| Disconnected | Red | EventSource is closed or in error state |
Key Files
Section titled “Key Files”| File | Purpose |
|---|---|
src/TechStrat.Api/Sse/SseConnectionManager.cs | Connection tracking and broadcast |
src/TechStrat.Api/Sse/SseHeartbeatService.cs | 15-second heartbeat timer |
src/TechStrat.Api/Endpoints/SseEndpoints.cs | GET /api/sse endpoint |
src/TechStrat.Web/Services/SseClient.cs | Strategy Site SSE client |
src/TechStrat.Web/wwwroot/js/sse-client.js | Browser EventSource wrapper |