The current repository has no production SSE endpoint or adapter. Chat uses SignalR and Notification Inbox exposes reliable state through ordinary reads. This page records requirements for introducing SSE and does not present a sample as an existing capability.
SSE suits one-way browser hints such as inbox change or long-running job progress. It is a poor fit for bidirectional interaction, binary frames, or complex chat-channel negotiation.
Critical path diagram
Follow the main path to identify responsibility handoffs, then use the prose to inspect failure branches and evidence.
Implementation requirements
- Authenticate before connection and authorize every subscribed resource by tenant.
- Use stable
idandLast-Event-IDfor reconnect; recovery comes from a persistent cursor. - Send comment heartbeats and configure proxy buffering, idle timeout, and maximum connection lifetime.
- Limit connections per caller and bound buffers so a slow client cannot block producers.
- Do not rely only on a process-local Channel; multi-instance delivery needs a shared source or replayable Store.
An SSE hint remains at least once. Clients deduplicate and query the source of truth. A future delivery requires proxy end-to-end, reconnect, capacity, and readiness tests before product material claims support.
Frames and cursors
An event contains id, event, one or more data lines, and a blank terminator. The ID must map to a durable sequence or opaque cursor; a process-local counter cannot recover after restart or across replicas.
id: 01J2M7X5event: notification.changeddata: {"notificationId":"N-1001","version":4}
: keep-aliveA heartbeat is a comment, not a business event. Keep payloads as minimal change hints and have the client fetch authorized truth from REST instead of copying full PII onto a long-lived connection.
On reconnect, validate Last-Event-ID against the current tenant/user/channel. An unknown, expired, or unauthorized cursor needs a stable error or explicit safe restart point; never replay another tenant’s stream.
Planned endpoint shape
This is a future implementation skeleton, not a shipped route. Authenticate and authorize the subscription before writing the response, and propagate RequestAborted.
// Planned endpoint: establish a trusted subscription before text/event-stream.app.MapGet("/api/notifications/stream", async ( HttpContext http, INotificationEventStream stream) =>{ http.Response.ContentType = "text/event-stream"; http.Response.Headers.CacheControl = "no-cache"; http.Response.Headers.Append("X-Accel-Buffering", "no");
// The stream validates Last-Event-ID for the current tenant and user. var cursor = http.Request.Headers["Last-Event-ID"].ToString(); await foreach (var item in stream.ReadAsync(cursor, http.RequestAborted)) { await http.Response.WriteAsync($"id: {item.Id}\nevent: {item.Type}\ndata: {item.Json}\n\n", http.RequestAborted); await http.Response.Body.FlushAsync(http.RequestAborted); }}).RequireAuthorization();Production code must encode lines safely; never concatenate arbitrary user text into an SSE frame.
Backpressure and budgets
Use a bounded buffer per connection. A policy may disconnect a slow client, drop recoverable hints, or coalesce repeated changes to one resource. Durable facts belong in a Store, not an unbounded in-memory Channel.
Budget per-user, per-tenant, and global connections; buffer length; event bytes; heartbeat; idle/maximum lifetime; and reconnect rate. Define closure for anonymous or expiring-token connections.
Multi-instance and proxy behavior
A load balancer may reconnect to another replica, so recovery needs a shared durable source. Sticky sessions reduce movement but do not replace cursor persistence.
Disable Nginx/CDN buffering, extend idle timeout, support streaming/chunking, and avoid compression/cache layers that aggregate output. Test through the actual public edge, not only Kestrel.
# After a future endpoint ships, -N exposes frames and heartbeats without buffering.curl -N \ -H 'Authorization: Bearer <test-token>' \ -H 'Last-Event-ID: 01J2M7X5' \ https://api.example.com/api/notifications/streamAuthentication and revocation
A JWT may expire while a stream remains open, and Membership/permission may be revoked. Choose periodic recheck, short maximum connection lifetime, or a server revocation signal, and prove the revocation window.
Delegation/Tenant Impersonation expires independently. SSE must not keep sending customer events under an expired support session. Audit connect/disconnect reason without storing every sensitive payload.
Client recovery
// Native EventSource cannot conveniently set Authorization; fetch streaming can.const response = await fetch("/api/notifications/stream", { headers: { Authorization: `Bearer ${accessToken}`, "Last-Event-ID": cursor }, signal: abortController.signal,});
// Deduplicate hints and retrieve the authorized latest fact from REST.await consumeSse(response.body, event => refreshFromApi(event.id));Cookie authentication needs CSRF/Origin analysis. Bearer fetch streaming needs token refresh/reconnect behavior.
Delivery tests
- unauthenticated, cross-tenant cursor, revoked Membership, and expired delegation disconnect;
- reconnect to another replica resumes without gaps or cross-tenant data;
- slow clients, bounded queue, network loss, and cancellation leak no Task/connection;
- proxy buffering is disabled and heartbeat is visible at the real edge;
- duplicate/reordered hints deduplicate while REST remains truth;
- capacity tests establish connection, memory, CPU, bandwidth, and reconnect-storm limits;
- readiness distinguishes missing event source from serviceable configuration.
See Realtime for the existing bidirectional implementation.
Production safeguards
SSE is a one-way transport for bounded event views. The server authenticates before opening the stream, filters every event by tenant and resource scope, and stops work promptly when the client disconnects.
- Send event identifiers so clients can reconnect without blind duplication.
- Use keepalive and bounded buffers; slow clients must not exhaust memory.
- Expose connection count, disconnect cause, lag, and dropped-event metrics.
Current conclusion
The repository currently has no production MapGet SSE route, event source, durable cursor, or SSE-specific operations metrics. The code and commands above are acceptance targets. Until implementation, Consumer Contract Tests, and real-proxy verification ship, SSE remains planned.