SignalR Groups

How ZapTicket organizes real-time event routing using SignalR groups for tenant-wide and conversation-specific messaging.

Overview

ZapTicket uses SignalR groups to control which clients receive which events. Groups are logical channels that clients join and leave — events sent to a group are delivered to all members of that group.

There are two types of groups:

  • Tenant groups — all agents in a workspace
  • Conversation groups — participants in a specific conversation

Tenant Group

Group Name Format
tenant:{tenantId}

Example: tenant:ws_abc123

Every agent is automatically added to their tenant group when they connect to the hub. The tenant ID is extracted from the workspace JWT claims — no client action needed.

Events delivered to tenant group:

  • ConversationCreated — new conversation started by a visitor
  • ConversationUpdated — status change, assignment change
  • MessageReceived (internal notes only) — internal notes are never sent to conversation groups
  • PresenceChanged — agent online/offline
💡The tenant group gives agents a "bird's eye view" of the inbox — they see new conversations appear and status changes without being subscribed to every individual thread.

Conversation Group

Group Name Format
conversation:{conversationId}

Example: conversation:conv_def456

Clients join a conversation group explicitly by calling JoinConversation(conversationId). Visitors are automatically joined when they start a conversation.

Events delivered to conversation group:

  • MessageReceived — new messages (public only, not internal notes)
  • TypingChanged — typing indicators
  • ConversationUpdated — status/assignment changes for this conversation

Group Membership Rules

Who Joins What
┌─────────────────────────────────────────────────────────────────┐
│ Client        │ tenant:{id}  │ conversation:{id}                │
├───────────────┼──────────────┼──────────────────────────────────┤
│ Agent         │ Auto-joined  │ Joined via JoinConversation()    │
│ Visitor       │ Never        │ Auto-joined on StartConversation │
└─────────────────────────────────────────────────────────────────┘
  • Agents can join any conversation group in their tenant
  • Visitors can only be in their own conversation group
  • Visitors are never added to the tenant group
  • Agents are removed from conversation groups when they navigate away or disconnect

Internal Notes Routing

Internal notes are a special case. When an agent sends a message with isInternal: true, the MessageReceived event is only broadcast to the tenant group, not the conversation group. This ensures visitors never receive internal notes, even if they're in the conversation group.

Message Routing Logic
SendMessage(isInternal: false)
  → Broadcast to conversation:{id}  (all participants see it)
  → Broadcast to tenant:{id}        (inbox preview updates)

SendMessage(isInternal: true)
  → Broadcast to tenant:{id} ONLY   (agents see it in sidebar)
  → NOT sent to conversation:{id}   (visitor never receives it)
⚠️This is a security boundary. The routing logic is enforced server-side — clients cannot opt-in to receiving internal notes. Even if a visitor's client is modified, the server never sends internal note events to non-tenant groups.

Presence Tracking

Agent presence (online/offline status) is tracked at the tenant group level. The system monitors connection lifecycle events:

  • Connect — Agent marked online, PresenceChanged(online) sent to tenant group
  • Disconnect — 15-second grace period for reconnection
  • Grace period expires — Agent marked offline, PresenceChanged(offline) sent
  • Reconnect within grace — No event, agent stays online
Presence State Machine
Connect ─────────────────→ Online
                                  │
                            Disconnect
                                  │
                                  ▼
                         Grace Period (15s)
                           │            │
                     Reconnect      Timeout
                           │            │
                           ▼            ▼
                        Online       Offline
💡The 15-second grace period prevents flickering when agents refresh the page or experience brief network interruptions. The widget uses presence data to show "X agents online" in the chat header.