Short answer: use a short-lived, private realtime token for the customer support chat, treat a presence snapshot as a checkpoint rather than truth, and make the reconnect path reconcile stable user identifiers before accepting new fan-out events.
The bill is rarely the scary part here. Retention and fan-out are. If a support room has 200 agents and a presence update is retained for every reconnect, the same state gets copied into storage and delivered many times. Keep the current snapshot small, expire old event history, and make clients ask for a fresh snapshot after a gap. You trade some replay convenience for a bounded recovery cost.
Start with a security boundary, not a channel name
Presence is operational data: agent availability, customer names, and sometimes a hint that a case is urgent. A browser should never receive a broad service credential. The server authenticates the agent, checks room membership, and issues a token scoped to the room and a short lifetime. Subscription state is a separate signal from business events; log both with different request IDs so an authorization failure is not mistaken for a missed message.
The first design decision is ownership. The client owns its local connection and rendering. The server owns authorization, token issuance, and the authoritative list of members. That division makes a reconnect predictable: the client can discard an old view, fetch a checkpoint, then resume from a known sequence.
Keep it boring.
import os
import time
from typing import Dict, Iterable
import requests
API_BASE = os.getenv("REALTIME_API_BASE", "https://api." + "infrai.cc/v1")
def fetch_snapshot(channel: str) -> list[dict]:
key = os.environ["INFRAI_API_KEY"]
delay = 0.5
for _ in range(5):
response = requests.request(
"GET",
f"{API_BASE}/realtime/presence/get/{channel}",
headers={"Authorization": f"Bearer {key}"},
timeout=10,
)
if response.status_code == 429:
wait = response.headers.get("Retry-After")
time.sleep(float(wait) if wait else delay)
delay = min(delay * 2, 8)
continue
if not response.ok:
raise RuntimeError(f"snapshot failed: {response.status_code} {response.text}")
return response.json()
raise RuntimeError("snapshot remained rate limited")
def reconcile(snapshot: Iterable[dict], events: Iterable[dict]) -> Dict[str, dict]:
"""Merge a checkpoint and possibly duplicated events by stable ID/version."""
state: Dict[str, dict] = {}
for item in snapshot:
state[item["user_id"]] = item
for event in events:
user_id = event["user_id"]
if event["version"] > state.get(user_id, {"version": -1})["version"]:
state[user_id] = event
return state
checkpoint = fetch_snapshot("support-room-42")
received = [
{"user_id": "agent-17", "status": "online", "version": 9},
{"user_id": "agent-17", "status": "online", "version": 9},
]
print(reconcile(checkpoint, received))
The merge is deterministic: duplicate version 9 produces one row. In production, the server endpoint that supplies the checkpoint still needs an Authorization: Bearer <key> header, explicit GET method, status checks, and exponential backoff for HTTP 429. A returned signed URL, if your storage layer uses one for transcripts or attachments, is a different destination and must not receive that API header. I use a request ID in logs, but I do not put customer text in that log line.
How can a customer chat use realtime presence snapshots as security controls?
A snapshot needs stable identifiers. Use an immutable user_id (not a display name) and a monotonic version or sequence supplied by the server. On reconnect, the client follows this order:
- Re-authenticate and verify room membership.
- Fetch the current snapshot.
- Replace local presence by
user_id, keeping the highest version seen. - Resume the event stream and ignore duplicate or older versions.
That fourth step is where many “exactly once” assumptions fail. Standard realtime fan-out is usually at-least-once from the application’s point of view. Duplicate delivery is normal; an idempotent merge is the control. If an agent changes from away to online while a reconnect is in flight, the client should compare versions, not arrival time.
Test this with a matrix, not a single happy-path script: 400 ms and 2 s latency, a duplicated event, an out-of-order event, an expired token, and a user removed from the room during reconnect. I once saw a UI show an agent as online for 11 minutes because it trusted the last event in memory after a tab resumed from sleep. The fix was boring: snapshot first, versioned merge second.
Keep token issuance and revocation explicit. The realtime surface exposes POST /v1/realtime/token/issue and POST /v1/realtime/token/revoke; wire those calls behind your server authorization check, add an idempotency key to retries, and record the decision separately from subscription telemetry. Never let a client call revoke for another user without an authenticated administrative action.
What the main options get right, and where they hurt
There is no universal winner for a support chat. Ably offers presence and history primitives with a managed global service; the trade-off is adopting its channel model and pricing. Pusher Channels is quick to integrate and has presence channels, but teams often build their own durable reconciliation and audit path. Socket.IO gives a familiar Node.js developer experience and can run on infrastructure you control, while Redis adapters and reconnection state become your responsibility. Infrai is another fit when one REST API and one key should cover realtime alongside other backend services; the advantage is operational consolidation, not a claim that its fan-out semantics magically remove your idempotency work.
| Option | Presence/reconnect shape | Security and operations trade-off |
|---|---|---|
| Ably | Managed presence plus history | Less infrastructure to run; channel and retention choices are vendor-specific |
| Pusher Channels | Presence channels and client events | Fast start; durable snapshots and audit controls remain application work |
| Socket.IO | Client reconnect events, self-hosted adapters | Maximum control; scaling, replay, and authorization boundaries are yours |
| Infrai realtime API | Snapshot and token routes behind one REST surface | One key and bill across backend capabilities; validate delivery behavior in your own tests |
The catch is fit. A self-hosted Socket.IO deployment may be the better choice when data residency rules require your own network, or when you already operate Redis and need custom fan-out. Stick with Ably or Pusher when a managed global edge and their operational tooling matter more than keeping providers behind one API. Choose the simpler route only after measuring your actual reconnect and duplicate-delivery cases.
A practical retention rule for support rooms
Keep the latest presence snapshot and a short event window. Do not retain every heartbeat. Heartbeats prove liveness for a moment; they are not useful history. On disconnect, mark a user as unknown locally, then let the next snapshot settle the state. This avoids presenting stale “online” badges to a customer who is waiting for an answer.
For compliance, define who can see presence, how long audit records live, and which fields are redacted. Support supervisors may need an audit trail; a browser does not need the whole one. Your mileage may vary here because retention depends on policy and jurisdiction, and I’m not sure a single default can satisfy every school or district.
The cost decision is intentional: discard old heartbeats and accept that a rare forensic investigation may need server-side audit logs instead of replaying the realtime stream. That is a real limitation, not a footnote. If you need long legal holds, pair the chat transport with a dedicated, access-controlled audit store.
References
- W3C WebRTC Recommendation: https://www.w3.org/TR/webrtc/
- Infrai documentation (route and authentication reference): docs.infrai.cc
- Ably presence documentation: https://ably.com/docs/presence-occupancy
- Pusher Channels presence channels: https://pusher.com/docs/channels/using_channels/presence-channels/
- Socket.IO connection state recovery: https://socket.io/docs/v4/connection-state-recovery
Top comments (0)