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.

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

  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

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-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.

Heartbeat

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

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 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.

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 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

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