docs
/
Client Integration

Realtime & chat

The WebSocket surface — connecting, authenticating, the event vocabulary, and why this is the one case that talks to AppEngine directly.

Chat is the exception to the rule in Overview. A WebSocket carries a long-lived, bidirectional stream; proxying it per-message through your own server buys none of the safety a request proxy buys, and costs a hop on every keystroke. The chat client connects to AppEngine directly and authenticates over the socket.

The reference implementation is the chat-client project — a React widget embedded into a host page.

Connecting

Socket.IO, on the /chat namespace:

import io from 'socket.io-client';

const socket = io(`${appengineHost}/chat`, {
  transports: ['websocket'],
  reconnection: true,
  reconnectionAttempts: 10,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 10000,
  auth: {
    token,
    orgId,
    configId,
    deviceId: getDeviceId(),
    context: {
      currentUrl: window.location.href,
      device: /mobile/i.test(navigator.userAgent) ? 'mobile' : 'desktop',
      language: navigator.language,
      deviceId: getDeviceId(),
      chatSessionId: existingChatSession,
    },
  },
});

transports: ['websocket'] skips the HTTP long-polling handshake. Credentials go in auth, which Socket.IO sends on the connection handshake — not in the URL, where they would end up in proxy logs and browser history.

chatSessionId is persisted locally and replayed on reconnect so a visitor who refreshes rejoins the same conversation rather than starting a new one.

Authenticating

Connection and authentication are separate. connect means the socket opened; the server then emits authenticate with the verdict:

socket.on('connect', () => {
  // transport is up — not yet authorised
});

socket.on('authenticate', (res) => {
  if (res?.success) {
    onAuthenticated(socket, res);   // res.chatSessionId
  } else {
    socket.close();
    clearSession();
  }
});

Do not send messages on connect. Wait for a successful authenticate — anything emitted before it is unauthenticated and will be rejected.

Event vocabulary

Session and transport

EventMeaning
authenticateAuth verdict; carries chatSessionId
connect_error, errorTransport or server error
token_expiredCredential expired — re-authenticate
session-expiring, session-expiredThe conversation is ageing out
disconnectSocket closed; reconnection is automatic

Conversation

EventMeaning
messageA complete message
chat-streamAn incremental token from an AI reply
status, updateTyping and state changes

Routing to a human

EventMeaning
queued, queue-updateWaiting for an agent, and position
agent-assigned, agent-changedAn agent picked the conversation up
chat-transferredHanded to another agent
chat-endedConversation closed
presence-changeAgent availability changed

Proactive engagement and broadcast

EventMeaning
engageServer-initiated outreach
broadcast-started, broadcast-endedA live broadcast
webrtc-offer, ice-candidateWebRTC negotiation for audio/video

Client to server: authenticate, close-chat, broadcast-join.

Streaming replies

AI answers arrive token by token on chat-stream and must be accumulated, with message as the settled version:

socket.on('chat-stream', (chunk) => appendToDraft(chunk));
socket.on('message', (msg) => commitMessage(msg));

Reconnection

Reconnection is automatic and bounded — ten attempts with a growing delay, capped at ten seconds. Two things are your responsibility:

  • Identity is re-sent on every reconnect. The auth payload is evaluated again, so a token that expired mid-session fails on reconnect; handle token_expired by refreshing before the next attempt.
  • Attempts are finite. After ten failures the socket stops trying. Surface that to the visitor rather than leaving a dead widget that looks live.

Embedding

The widget mounts into a host page and keeps its own state, so it does not require the host to be React. Point it at your AppEngine host and pass orgId and configId — configId selects which chat configuration (routing rules, AI persona, availability) the conversation runs under.