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
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 Metric | Server-Sent Events (SSE) | WebSocket | BitzOrcas Architectural Rationale |
|---|---|---|---|
| Protocol Foundation | Standard HTTP/1.1 & HTTP/2 | Custom ws:// / wss:// protocol upgrade | SSE integrates directly with existing ASP.NET Core pipelines without protocol-switching overhead |
| Proxy & Gateway Traversal | Native support across YARP, Nginx, Cloudflare | Requires explicit Connection: Upgrade tuning | Zero reverse proxy reconfiguration required; seamlessly preserves distributed tracing context |
| Reconnection Semantics | Native browser EventSource automatic retry | Requires complex manual retry and backoff logic | Eliminates frontend state machine complexity; transparently recovers from network drops |
| Corporate Firewall Traversal | Operates over standard port 443 | Frequently blocked by corporate firewalls | Ideal 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:
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:
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:
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
- Zero Message Loss: Persistence comes first. Even during client disconnects, records remain accessible via
/api/notifications/inbox. - Extreme Concurrency Efficiency: Utilizing asynchronous
Channel<T>andValueTaskallows a single API Host to manage tens of thousands of idle connections with negligible memory overhead. - Transparent Proxy Compatibility: Fully compliant with YARP and HTTP/2 multiplexing without custom server infrastructure.