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¶
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.
sequenceDiagram
participant User A as User A (Browser)
participant API as API Host
participant SSE as SSE Manager
participant User B as User B (Browser)
User A->>API: POST /api/events (edit principle title)
API->>API: Process event
API->>SSE: Broadcast notifications
API-->>User A: 200 OK (result)
SSE-->>User A: event: card-changed
SSE-->>User B: event: card-changed
User A->>API: GET /api/principles/{id}
User B->>API: GET /api/principles/{id}
Architecture¶
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.
graph TB
EP[Event Processing] -->|BroadcastNotification| CM[SseConnectionManager]
CM --> C1[Channel - Client 1]
CM --> C2[Channel - Client 2]
CM --> C3[Channel - Client 3]
C1 -->|Read loop| R1[HTTP Response Stream]
C2 -->|Read loop| R2[HTTP Response Stream]
C3 -->|Read loop| R3[HTTP Response Stream]
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¶
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¶
Indicates that a specific entity's data was updated.
Triggered by: field edits (title, description, progress, name), 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¶
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.
Cascade notifications
The processor emits notifications for all affected entities, not just the direct target. When a principle is deleted, it emits card-changed for the principle and for every objective that had it assigned. This way clients only re-fetch cards that actually changed, rather than doing a full view reload for every edit.
Admin Site simplification
The Admin Site treats all notifications identically: any card-changed or view-reload triggers a data refresh for whichever page is active. The card-level optimization is specific to the Strategy Site.
Wire Format¶
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-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¶
The SseHeartbeatService is a hosted service that sends a comment line to all connections every 15 seconds:
The 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
Why staleness detection matters
SSE connections can silently die — network changes, load balancers timing out idle connections, etc. In a collaborative tool where someone else might be editing simultaneously, the user needs to know when they might be looking at stale data.
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¶
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¶
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¶
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¶
| 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.StrategySite/Services/SseClient.cs |
Strategy Site SSE client |
src/TechStrat.StrategySite/wwwroot/js/sse-client.js |
Browser EventSource wrapper |