Real-Time Updates
This document defines the standard for real-time server-to-client updates across the ASP.NET Core API and the Next.js frontend.
1. Default Choice
Use ASP.NET Core SignalR when the product needs server-pushed updates. SignalR is appropriate for notifications, dashboards, collaborative workflows, and status changes where waiting for the next page load is not acceptable.
Use polling only when the update interval is coarse, the data is not user-facing, or the deployment environment cannot support persistent connections.
2. Event Flow
Real-time updates are notifications that data changed. They are not the source of truth.
sequenceDiagram participant Client participant Api as WebApi participant Command as Application.Write participant Domain participant Reactions as Application.Reactions participant Hub as SignalR Hub
Client->>Api: POST /tickets/{id}/assign Api->>Command: AssignTicketCommand Command->>Domain: ticket.Assign(...) Domain-->>Command: TicketAssigned event Command-->>Api: committed by pipeline Reactions->>Hub: publish TicketChanged notification Hub-->>Client: ticketChanged(ticketId) Client->>Api: refetch ticket queryThe SignalR message carries enough information for the frontend to invalidate or refetch. It does not carry full domain state unless a project ADR approves that payload shape.
3. Backend Rules
Hubs live in WebApi. Hub-facing notification services live in Infrastructure and implement narrow interfaces defined in Application.Reactions.
// GOOD: Reactions defines intent through a narrow interfaceinternal interface ITicketChangedPublisher{ Task PublishTicketChangedAsync( TicketId ticketId, TenantId tenantId, CancellationToken cancellationToken);}// BAD: Application.Reactions depends on SignalR directlyinternal sealed class PublishTicketChangedEventHandler : IEventHandler<TicketAssigned>{ private readonly IHubContext<TicketHub> _hubContext;}SignalR hubs MUST:
- Require authorization unless explicitly public.
- Use groups for tenant, team, or resource scoping.
- Never trust client-sent group names without server-side authorization.
- Emit correlation IDs in logs for connection, group join, and publish operations.
- Avoid sending PII in event names or group names.
4. Frontend Rules
The frontend subscribes to real-time notifications in a client component or client hook, then invalidates TanStack Query keys or calls router.refresh().
"use client"// Needs a browser SignalR connection and TanStack Query invalidation.
import { HubConnectionBuilder } from "@microsoft/signalr"import { useQueryClient } from "@tanstack/react-query"import { useEffect } from "react"import { ticketQueryKeys } from "./queryKeys"
export function useTicketRealtime() { const queryClient = useQueryClient()
useEffect(() => { const connection = new HubConnectionBuilder() .withUrl("/hubs/tickets") .withAutomaticReconnect() .build()
connection.on("ticketChanged", (ticketId: string) => { queryClient.invalidateQueries({ queryKey: ticketQueryKeys.byId(ticketId), }) })
void connection.start()
return () => { void connection.stop() } }, [queryClient])}Do not store real-time server state in Zustand. Use SignalR as an invalidation channel and keep TanStack Query or server components as the data source.
5. Scaling
Single-instance deployments may use in-process SignalR. Multi-instance deployments require a backplane or managed SignalR service, documented in a project ADR.
When real-time updates are required for correctness, pair SignalR with the Outbox pattern. SignalR alone is not durable. Disconnected clients must recover by refetching current state.