SignalR Connection

How to establish a real-time WebSocket connection to ZapTicket's SignalR hub for live messaging.

Overview

ZapTicket uses ASP.NET Core SignalR for real-time communication between the backend, agent dashboard, and visitor widget. The hub enables instant message delivery, typing indicators, presence updates, and conversation lifecycle events.

Two types of clients connect to the hub:

  • Agents — authenticated via workspace JWT, join the tenant-wide group
  • Visitors — authenticated via public site key, join their specific conversation group

Connection URL

Hub Endpoint
wss://api.zapticket.app/hubs/chat

Authentication

Agents (JWT)

Agents authenticate by passing their workspace token as the access_token query parameter during the connection handshake:

Agent Connection (TypeScript)
import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr";

const connection = new HubConnectionBuilder()
  .withUrl("https://api.zapticket.app/hubs/chat", {
    accessTokenFactory: () => workspaceToken,
  })
  .withAutomaticReconnect()
  .configureLogging(LogLevel.Information)
  .build();

await connection.start();
console.log("Connected as agent");
💡The SignalR client library passes the token as an access_token query parameter on the WebSocket upgrade request. This is the standard SignalR auth pattern — do not pass it as a header.

Visitors (Site Key)

Visitors authenticate with the public site key. The widget passes it as a site_key query parameter:

Visitor Connection (TypeScript)
const connection = new HubConnectionBuilder()
  .withUrl(`https://api.zapticket.app/hubs/chat?site_key=${siteKey}`, {
    // No JWT — site key handles auth
  })
  .withAutomaticReconnect()
  .build();

await connection.start();
console.log("Connected as visitor");

Transport Negotiation

SignalR automatically negotiates the best transport in this order:

  1. WebSockets — preferred, full-duplex, lowest latency
  2. Server-Sent Events (SSE) — fallback for environments that block WebSockets
  3. Long Polling — last resort, works everywhere

In production, WebSockets are used 99% of the time. The fallbacks exist for corporate proxies and restrictive firewalls.

⚠️If deploying behind a reverse proxy (Nginx, Caddy, Cloudflare), ensure WebSocket upgrade headers are forwarded. Without this, clients will fall back to long polling.

Auto-Reconnect

The .withAutomaticReconnect() configuration handles temporary disconnections (network hiccups, server restarts) automatically. The default retry intervals are:

Default Retry Schedule
Attempt 1: immediately (0ms)
Attempt 2: after 2 seconds
Attempt 3: after 10 seconds
Attempt 4: after 30 seconds
After 4 failures: stops reconnecting

You can customize the retry policy:

Custom Retry Policy
const connection = new HubConnectionBuilder()
  .withUrl(hubUrl, { accessTokenFactory: () => token })
  .withAutomaticReconnect([0, 1000, 5000, 10000, 30000, 60000])
  .build();

connection.onreconnecting((error) => {
  console.log("Reconnecting...", error);
  // Show "reconnecting" UI state
});

connection.onreconnected((connectionId) => {
  console.log("Reconnected:", connectionId);
  // Re-join groups if needed
});

connection.onclose((error) => {
  console.log("Connection closed permanently", error);
  // Show "disconnected" UI state
});

Connection Lifecycle

Connection States
Disconnected → Connecting → Connected → Reconnecting → Connected
                                         ↓
                                    Disconnected (if all retries fail)

When an agent connects, the backend automatically adds them to the tenant:{tenantId} group. When a visitor connects, they're added to their conversation group upon starting or joining a conversation.

💡After reconnection, clients should re-subscribe to any conversation-specific groups. The tenant group is re-joined automatically based on the JWT claims.