Every entity change is delivered in real time to connected clients. The mechanism differs for users (frontend) and services (background processors).
WebSocket protocol
All real-time communication happens over WebSocket using a JSON protocol with a type discriminator field.
Events (Server → Client)
| Type | Recipient | Description |
|---|
| Entity Change | Users | Entity changed matching your subscription |
| Entity Change (with task) | Services | Entity change requiring acknowledgment |
| Entity Change Batch | Services | Batch of changes requiring acknowledgment |
| Subscription Result | Both | Subscribe success/failure |
| Subscription Refresh | Users | Entity left your subscription — refetch your list |
| Heartbeat | Both | Connection alive |
| Log Broadcast | Admins | Service log message |
Commands (Client → Server)
| Command | Description |
|---|
| Subscribe | Subscribe to entities matching a query |
| Unsubscribe | Stop receiving changes |
| Task Completion | Acknowledge processing complete (services only) |
| Lock / Unlock | Distributed lock management (services only) |
| Heartbeat | Keep-alive ping |
User data flow
Entity committed
A change is persisted and a checkpoint is created.
Broadcast
The change is broadcast to all API nodes via pub/sub.
Subscription evaluation
Each node evaluates the change against every connected user’s subscriptions. If the entity matches the query, an entity change event is sent. If the entity was being watched but no longer matches, a refresh event is sent telling the client to refetch its list.
User changes are fire-and-forget. Users don’t acknowledge receipt. If a user is offline when a change happens, they’ll get the current state when they reconnect and subscribe.
Subscription refresh
When an entity leaves a user’s subscription (e.g., an account’s workstream changes from “DenialManagement” to “None” while a user is viewing the denial queue), a refresh event is sent. This tells the client: “an entity you were watching no longer matches your query — refetch your list.”
Service data flow
Entity committed
A change is persisted and a checkpoint is created.
Query matching
The change is evaluated against all background service queries.
Enqueue
Matching changes are enqueued to the service’s dedicated message queue.
Delivery
The service receives the change event via its WebSocket connection.
Acknowledge
The service processes the change and sends an acknowledgment (success or failure).
Key difference from users:
- Durable — messages persist even if the service is offline
- Acknowledged — services must confirm processing
- Retried — unacknowledged messages are re-delivered after a timeout
Multi-node architecture
Our API runs across multiple nodes behind a load balancer. The challenge: a change committed on Node A must reach a user connected to Node B.
Changes are broadcast to all nodes via a pub/sub bus. Each node evaluates the change against its locally connected users’ subscriptions and delivers matching events. This ensures every user gets real-time updates regardless of which node they’re connected to.
Change event payload
An entity change event includes:
{
"change": {
"entityId": "entity-uuid",
"previous": { /* full entity before change, null if creation */ },
"current": { /* full entity after change */ },
"createdAt": "2026-02-19T14:30:00Z",
"createdBy": "user-uuid"
}
}
Both the previous and current state are included, so clients can compute what changed without an additional API call.