Skip to content

SignalR Integration

CritterWatch uses SignalR to push real-time updates from the server to connected browser clients. All incoming telemetry from monitored services is relayed to browsers immediately after being processed by Wolverine handlers.

CommunicationHub

The CommunicationHub is a SignalR hub mounted at /api/messages (configurable):

csharp
public class CommunicationHub : Hub
{
    // Clients connect and receive all service updates pushed by the server
    // No client-to-server SignalR calls — all commands go through the HTTP API
}

The hub is a receive-only connection from the browser's perspective. Browsers connect and receive push messages. Commands are sent via HTTP POST, not via the hub.

Relay Pattern

The relay pattern bridges Wolverine message processing to SignalR clients:

csharp
// This handler runs after each ServiceUpdates is processed
public class RelayToWebSocketMessageHandler
{
    private readonly IHubContext<CommunicationHub> _hub;

    public async Task Handle(ServiceUpdates updates)
    {
        await _hub.Clients.All.SendAsync("serviceUpdated", updates);
    }
}

Every message type that needs to reach the browser has a corresponding relay handler. This keeps the SignalR relay separate from the Marten projection logic.

Message Types Relayed

The following message types are relayed to connected browsers via SignalR:

MessageBrowser EventDescription
ServiceUpdatesserviceUpdatedFull service state snapshot
AgentHealthReportagentHealthUpdatedAgent health changes
AlertRaisedalertRaisedNew alert created
AlertElevatedalertElevatedAlert severity increased
AlertResolvedalertResolvedAlert auto-resolved
AlertClearedalertClearedAlert manually cleared
ShardStatesChangedshardStatesUpdatedProjection shard status
PersistenceCountsChangedpersistenceCountsUpdatedInbox/outbox counts
BackPressureTriggeredMessagebackPressureTriggeredBack pressure activated
CircuitBreakerTrippedMessagecircuitBreakerTrippedCircuit breaker tripped
TimelineEntrytimelineEntryActivity timeline event

CloudEvents Envelope

All SignalR messages use the Wolverine CloudEvents envelope format:

json
{
  "type": "service_updated",
  "data": {
    "serviceName": "trip-service",
    "nodes": { ... },
    "agents": { ... }
  }
}

The type field uses snake_case naming derived from the C# message type name. The data field contains the message payload with camelCase property names.

JSON Serialization

SignalR messages use specific serialization settings that differ from the default ASP.NET Core JSON:

  • JsonNamingPolicy.CamelCasePropertyNamepropertyName
  • JsonStringEnumConverter — enums as string names ("Query", not 1)

This is configured in the Wolverine.SignalR transport and applies to all messages sent through the hub.

TypeScript Code Generation

The TypeScript message types generated by dotnet run generate must use camelCase property keys in their init() and toJSON() methods. The code generator may produce PascalCase keys — always verify generated code after running the generator.

See the CLAUDE.md file for the full serialization conventions.

Frontend Connection

The Vue frontend connects to the SignalR hub using the @microsoft/signalr package:

typescript
// src/stores/connection-store.ts
const connection = new HubConnectionBuilder()
  .withUrl('/api/messages')
  .withAutomaticReconnect()
  .build()

connection.on('serviceUpdated', (data) => {
  // relay to appropriate Pinia store
})

await connection.start()

The relayToStore.ts module maps each incoming message type to the appropriate Pinia store update action, maintaining a clean separation between the WebSocket transport and the Vue store layer.

Connection Lifecycle

  • Automatic reconnect — the SignalR client retries with exponential backoff on disconnect
  • Connection state — a connection-store.ts Pinia store tracks the connection state and exposes it to UI components
  • Reconnect recovery — on reconnect, the client requests a full state snapshot via HTTP to catch up with any missed updates during the disconnection

Released under the MIT License.