Socket.io overview Live
Real-time fan-out runs over Socket.io with two namespaces. The dashboard subscribes to /agent with a workspace JWT. The widget bundle subscribes to /widget anonymously, identified by workspace plus a local visitor ID. Both deliver sub-100ms latency in our internal benchmarks.
Socket.io is a thin wrapper over WebSockets with reconnect logic, fallback transports, and packet acks. Read the Socket.io v4 docs for transport details. We pin Socket.io v4 server-side and ship Socket.io v4 in the widget bundle.
Base URL
The same hostname serves the REST API and the Socket.io upgrade:
wss://app.message.com/socket.io/The path is fixed at /socket.io/ (we do not allow custom paths). The namespace is appended via the io(URL) call.
Namespaces
| Namespace | Who connects | Auth |
|---|---|---|
/agent | Dashboard, human agents, server-side automations acting as a workspace | JWT in auth.token |
/widget | Embed bundle, customer browsers | Anonymous handshake with workspaceId + optional visitorId |
Detailed event lists per namespace: Agent namespace and Widget namespace.
Agent connection
Get a workspace JWT from POST /api/v1/auth/login (see Authentication). Pass it under auth.token on connect. The server verifies the JWT, attaches the agent to the workspace room, and replays any events the agent missed while disconnected (up to a 5-minute window).
import { io } from "socket.io-client";
const socket = io("wss://app.message.com/agent", {
auth: { token: "<workspace JWT>" },
transports: ["websocket"], // skip the long-poll upgrade
reconnectionDelayMax: 5000
});
socket.on("connect", () => {
console.log("connected as", socket.id);
});
socket.on("conversation:new", (conv) => {
// new chat just landed in your queue
});Widget connection
The widget bundle handles this automatically. The handshake passes the public workspace ID plus a locally-cached visitor ID (if any). The server may issue a new visitor ID and persist it in local storage.
// The widget bundle does this for you. Shown for reference.
const socket = io("wss://app.message.com/widget", {
auth: {
workspaceId: "abc123",
visitorId: "v_local_uuid"
},
transports: ["websocket"]
});Presence
Each connected socket counts toward presence. Agents go “online” when they connect with an available status flag; they go “away” on disconnect. Visitors are “active” while their socket is open. See Authentication and presence for the full state machine.
Reconnection
The Socket.io client reconnects automatically with exponential backoff. Most failures (network blip, server restart) self-heal. A few cases need explicit handling:
socket.on("disconnect", (reason) => {
// 'io server disconnect' = server kicked us, manual reconnect needed
if (reason === "io server disconnect") {
socket.connect();
}
// any other reason: auto-reconnect handles it
});
socket.io.on("reconnect_attempt", (n) => {
console.log("reconnect attempt", n);
});If the JWT has expired between connect attempts, the server rejects the handshake. Re-issue the token from your auth flow and reconnect.
Event payload shapes
All events carry JSON payloads. Resource references use UUIDs (string). Timestamps are ISO 8601 strings with a Z suffix (UTC). Money amounts are integer cents (no floats).
Per-event payload shapes are documented on the namespace pages. Most match the REST resource shape one-for-one. For example, the conversation:new payload is identical to the conversation object returned by GET /api/v1/conversations.
Acknowledgements
Client-emitted events can optionally request a server ack. The ack callback fires with either the resource or an error. Use this for take-next (atomic claim of the next chat), message:send, and other state-changing emits.
socket.emit("take-next", {}, (err, conv) => {
if (err) {
if (err === "already_taken") return; // another agent got it
if (err === "queue_empty") return;
throw new Error(err);
}
// conv is the conversation you just claimed
});Common pitfalls
- Hard-coding
https. Usewssin the URL. Socket.io will negotiate the upgrade, but pinninghttpsblocks the WebSocket transport on some proxies. - Polling for events. Do not also poll the REST API for new messages while subscribed. You will get duplicates and waste rate limit. Pick one source.
- Skipping the ack on
take-next. Without the ack, you cannot tell whether you actually got the conversation. Two agents emitting at the same time is the whole reason the ack exists. - Listening for every event. Subscribe only to events your UI actually renders. Idle subscribers still cost a function pointer per packet.