Skip to content
bitzorcas
中EN

Tutorial

Integrating Real-Time Notifications: Server-Sent Events (SSE) High-Performance Streaming

Adhering to the architectural invariant 'Persistence First, Streaming Second,' leverage Server-Sent Events (SSE) to deliver high-performance streaming alerts for litigation approvals and conflict of interest warnings with automatic reconnection.

Last updated

In the early stages of enterprise platform development, engineering teams frequently commit a critical architectural error: treating the notification delivery transport as persistent storage. In an effort to achieve real-time responsiveness, backends broadcast notifications via ephemeral in-memory queues or raw WebSockets directly to clients without writing to a relational database. When a user switches browser tabs, experiences a transient network drop, or the backend restarts, unpersisted alerts are lost forever. In a law firm setting, a missed adversary conflict warning or statutory filing deadline leads directly to malpractice exposure.

BitzOrcas.Modern strictly enforces the “Persistence First, Streaming Second” architectural invariant. Every business notification must first be committed to the database as an immutable entity, establishing audit records and mailbox states. Only post-commit does the system stream updates via HTTP/2 Server-Sent Events (SSE) to active sessions. Even if a client is completely offline, re-establishing a connection allows immediate hydration of unread notifications from persistent storage.

This tutorial guides you through implementing a resilient, tenant-isolated notification stream for Matter Intake Approvals and Conflict of Interest Warnings.

Real-Time Notification Pipeline Sequence

"SQL Server Notification Table""INotificationRepository""CAP Outbox Event Consumer""Tenant Notification Hub (NotificationChannelHub)""SSE Streaming Endpoint (/api/notifications/stream)""SQL Server Notification Table""INotificationRepository""CAP Outbox Event Consumer""Tenant Notification Hub (NotificationChannelHub)""SSE Streaming Endpoint (/api/notifications/stream)"Keep-alive HTTP/2 stream active (15s :ping heartbeat)Phase 1: Persistence First InvariantPhase 2: High-Performance SSE Dispatch"Partner Browser Client (SPA)"1. Establish SSE stream (GET /api/notifications/stream with JWT)12. Register persistent session channel (Channel<NotificationPayload>)23. Consume matter event, construct Notification aggregate34. SaveAsync commits record to database table45. Trigger PublishToUserAsync(tenantId, userId, payload)56. Write to bounded Channel, notifying active StreamWriter67. Dispatch SSE payload (event: matter_alert\ndata: {...}\n\n)7"Partner Browser Client (SPA)"

Architectural Trade-offs: Why SSE Over WebSocket?

For administrative enterprise SaaS platforms, the vast majority of real-time interactions consist of unidirectional server-to-client broadcasts (such as task approvals, report generation status, and compliance alerts):

Evaluation MetricServer-Sent Events (SSE)WebSocketBitzOrcas Architectural Rationale
Protocol FoundationStandard HTTP/1.1 & HTTP/2Custom ws:// / wss:// protocol upgradeSSE integrates directly with existing ASP.NET Core pipelines without protocol-switching overhead
Proxy & Gateway TraversalNative support across YARP, Nginx, CloudflareRequires explicit Connection: Upgrade tuningZero reverse proxy reconfiguration required; seamlessly preserves distributed tracing context
Reconnection SemanticsNative browser EventSource automatic retryRequires complex manual retry and backoff logicEliminates frontend state machine complexity; transparently recovers from network drops
Corporate Firewall TraversalOperates over standard port 443Frequently blocked by corporate firewallsIdeal for secure enterprise deployments across law firms and banking environments

Step 1: Implement the Tenant-Isolated Notification Channel Hub

Leverage .NET’s lock-free high-throughput System.Threading.Channels.Channel<T> primitive to partition streaming channels by tenant and user:

src/Platform/Notifications/BitzOrcas.Platform.Notifications.Infrastructure/Channels/NotificationChannelHub.cs
using System.Collections.Concurrent;
using System.Threading.Channels;
namespace BitzOrcas.Platform.Notifications.Infrastructure.Channels;
/// <summary>
/// Tenant-isolated real-time notification streaming hub.
/// </summary>
public sealed class NotificationChannelHub
{
// Nested dictionary enforcing tenancy boundary: TenantId -> (UserId -> Channel)
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, Channel<NotificationPayload>>> _tenantChannels = new();
/// <summary>
/// Subscribes an authenticated user to their dedicated event channel.
/// </summary>
public ChannelReader<NotificationPayload> Subscribe(string tenantId, string userId)
{
var userMap = _tenantChannels.GetOrAdd(tenantId, _ => new ConcurrentDictionary<string, Channel<NotificationPayload>>());
// Bounded channel with backpressure dropping oldest items if reader lags
var channel = userMap.GetOrAdd(userId, _ => Channel.CreateBounded<NotificationPayload>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.DropOldest
}));
return channel.Reader;
}
/// <summary>
/// Unsubscribes and purges an active session channel upon disconnect.
/// </summary>
public void Unsubscribe(string tenantId, string userId)
{
if (_tenantChannels.TryGetValue(tenantId, out var userMap))
{
userMap.TryRemove(userId, out _);
}
}
/// <summary>
/// Publishes a payload to a target user within a specific tenant partition.
/// </summary>
public async ValueTask PublishToUserAsync(string tenantId, string userId, NotificationPayload payload, CancellationToken cancellationToken = default)
{
if (_tenantChannels.TryGetValue(tenantId, out var userMap) &&
userMap.TryGetValue(userId, out var channel))
{
// Writes to channel, waking the active HTTP response writer
await channel.Writer.WriteAsync(payload, cancellationToken);
}
}
}
public sealed record NotificationPayload(
string NotificationId,
string EventType,
string Title,
string Content,
DateTimeOffset CreatedAtUtc);

Step 2: Implement the High-Performance SSE Minimal API Endpoint

Expose an authenticated streaming endpoint that writes directly to the HTTP response stream:

src/Platform/Notifications/BitzOrcas.Platform.Notifications.Application/Endpoints/NotificationStreamEndpoint.cs
using System.Text.Json;
using BitzOrcas.Application.Abstractions.Tenancy;
using BitzOrcas.Application.Security;
using BitzOrcas.Platform.Notifications.Infrastructure.Channels;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace BitzOrcas.Platform.Notifications.Application.Endpoints;
public static class NotificationStreamEndpoint
{
public static void MapNotificationStream(this IEndpointRouteBuilder app)
{
app.MapGet("/api/notifications/stream", async (
HttpContext context,
ICurrentUser currentUser,
ICurrentTenant currentTenant,
NotificationChannelHub channelHub,
CancellationToken cancellationToken) =>
{
// 1. Set standard Server-Sent Events protocol headers
context.Response.Headers.ContentType = "text/event-stream";
context.Response.Headers.CacheControl = "no-cache";
context.Response.Headers.Connection = "keep-alive";
var tenantId = currentTenant.Tenant.EffectiveTenantId;
var userId = currentUser.UserId;
var reader = channelHub.Subscribe(tenantId, userId);
try
{
// Dispatch initial connection handshake event
await context.Response.WriteAsync("event: connected\ndata: {\"status\":\"ok\"}\n\n", cancellationToken);
await context.Response.Body.FlushAsync(cancellationToken);
// 2. Consume events from bounded channel
while (!cancellationToken.IsCancellationRequested)
{
// 15-second timeout to emit ping heartbeats preventing intermediary proxy disconnects
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromSeconds(15));
try
{
var payload = await reader.ReadAsync(cts.Token);
var json = JsonSerializer.Serialize(payload);
// Format according to standard SSE specification: event: <name>\ndata: <json>\n\n
await context.Response.WriteAsync($"event: {payload.EventType}\n", cancellationToken);
await context.Response.WriteAsync($"data: {json}\n\n", cancellationToken);
await context.Response.Body.FlushAsync(cancellationToken);
}
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
{
// Emit SSE comment line as heartbeat
await context.Response.WriteAsync(":ping\n\n", cancellationToken);
await context.Response.Body.FlushAsync(cancellationToken);
}
}
}
finally
{
// 3. Clean up subscription upon client disconnection
channelHub.Unsubscribe(tenantId, userId);
}
})
.RequireAuthorization()
.WithTags("Notifications")
.WithSummary("Establish tenant-isolated SSE real-time notification stream");
}
}

Step 3: Frontend TypeScript Native Client Integration

In the frontend application, connect to the streaming endpoint using standard browser EventSource:

frontend/packages/platform-sdk/src/notifications/useNotificationStream.ts
import { useEffect } from 'react';
export function useNotificationStream(accessToken: string) {
useEffect(() => {
if (!accessToken) return;
// Connect via standard HTTP SSE endpoint
const eventSource = new EventSource(`/api/notifications/stream?access_token=${encodeURIComponent(accessToken)}`);
// Listen for incoming legal conflict and approval alerts
eventSource.addEventListener('matter_alert', (event: MessageEvent) => {
const data = JSON.parse(event.data);
console.info('[SSE] Received real-time alert:', data);
// Dispatch browser custom event to update badges and trigger toast UI
window.dispatchEvent(new CustomEvent('bitz:notification:received', { detail: data }));
});
// Native browser automatic exponential backoff error handling
eventSource.onerror = (err) => {
console.warn('[SSE] Connection interrupted, browser retrying automatically...', err);
};
return () => {
eventSource.close();
};
}, [accessToken]);
}

Key Architectural Invariants Enforced

  1. Zero Message Loss: Persistence comes first. Even during client disconnects, records remain accessible via /api/notifications/inbox.
  2. Extreme Concurrency Efficiency: Utilizing asynchronous Channel<T> and ValueTask allows a single API Host to manage tens of thousands of idle connections with negligible memory overhead.
  3. Transparent Proxy Compatibility: Fully compliant with YARP and HTTP/2 multiplexing without custom server infrastructure.

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%