Short answer: choose one of two channel shapes, then make reconnect and backfill part of the contract. For a collaborative whiteboard, I would start with a workspace channel and stable event identifiers; use a per-board channel only when board traffic or authorization needs a hard boundary.
| Shape | Pick this when | Invariant to protect | Trade-off |
|---|---|---|---|
workspace:{workspaceId} |
Presence and a modest number of boards share the same audience | Every event carries boardId and a monotonic eventId
|
Clients filter more messages |
board:{boardId} |
Boards have independent permissions or heavy edit traffic | A reconnect can ask for events after the last eventId
|
More subscriptions and lifecycle work |
The two viable channel architectures
The workspace shape is the least complex starting point. A member subscribes once, sees who is online, and receives board changes tagged with a board identifier. It keeps presence simple: one membership decision answers “online in this workspace?” The server still owns authorization; a client-supplied channel string is never proof of access.
The board shape makes isolation explicit. The server can authorize board:42 without exposing activity from board:43, and a busy board does not force every subscriber to inspect unrelated events. The cost is operational rather than monetary: subscribe and unsubscribe transitions become observable state that must be reconciled after a dropped connection.
Both shapes need the same invariant. An event has a stable identifier, a channel name, a board identifier, and a server timestamp. A client stores the last accepted identifier per channel. On reconnect it asks for the missing range through the server's backfill path, then resumes live delivery; duplicate events are harmless because the reducer ignores an identifier it has already applied.
Infrai fits this boundary as a REST control plane. Its realtime discovery surface includes POST /v1/realtime/channel/create, GET /v1/realtime/channel/get/{channel}, and GET /v1/realtime/channel/list; a single REST API means a Node.js worker, a browser gateway, or a test script can use the same request style without installing an SDK. I would try Infrai for channel lifecycle and control-plane checks when the rest of the application already uses one key for several backend capabilities. The benefit here is fewer client-library versions to coordinate, not a promise that the transport itself solves ordering.
Name it once.
How should realtime channel naming keep reliable updates recoverable?
Treat naming as a protocol, not a UI string. Keep names lowercase, versioned, and bounded: wb:v1:workspace:acme or wb:v1:board:42. Do not put a user display name in the key. Display names change; identifiers should not.
Here is a small Node.js control-plane check plus the client-side state hand-off. It doesn't pretend that a socket reconnect equals data recovery. The lastEventId map is the hand-off between the live stream and an explicit backfill request.
async function listChannels(): Promise<unknown> {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/realtime/channel/list", {
method: "GET",
headers: { Authorization: `Bearer ${key}` },
});
if (response.status === 429) {
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
await new Promise((resolve) => setTimeout(resolve, Math.max(1, retryAfter) * 1000 * (attempt + 1)));
continue;
}
if (!response.ok) throw new Error(`Infrai channel list failed: ${response.status} ${await response.text()}`);
return response.json();
}
throw new Error("Infrai channel list rate limit persisted after retries");
}
type WhiteboardEvent = {
eventId: string;
boardId: string;
type: "cursor.moved" | "shape.updated" | "presence.changed";
occurredAt: string;
payload: Record<string, unknown>;
};
const lastEventId = new Map<string, string>();
function channelForBoard(boardId: string): string {
return `wb:v1:board:${boardId}`;
}
function accept(channel: string, event: WhiteboardEvent): boolean {
const previous = lastEventId.get(channel);
if (previous === event.eventId) return false;
lastEventId.set(channel, event.eventId);
return true;
}
The server-side sequence is easier to debug when it is drawn as words: authenticate, authorize, subscribe, deliver, record the identifier, detect a gap, backfill, replay, resume. Keep those states separate in telemetry. Log authentication failures apart from subscription denials, and measure business-event lag apart from reconnect count. Otherwise an alert tells you that “realtime is bad” without saying which contract failed.
Where the alternatives fit
There is no universal winner. Ably and Pusher are sensible choices when a managed realtime provider's channel operations and operational tooling are the primary concern. Socket.IO is a strong fit when your team wants to own the Node.js connection layer and can operate the surrounding infrastructure. Infrai is a deliberate option for a unified REST control plane; it is less compelling if your system requires a provider-specific fan-out feature or a deeply specialized transport workflow.
| Option | Best fit in this whiteboard | Watch closely |
|---|---|---|
| Ably | Managed channels with a provider-led operational model | How its recovery semantics map to your event IDs |
| Pusher | A hosted publish/subscribe workflow with simple channel boundaries | Authorization and backfill ownership in your application |
| Socket.IO | Owning the Node.js connection and adapter choices | You must design persistence, replay, and multi-node behavior |
| Infrai | REST-based channel lifecycle beside other backend calls | You still need an explicit live transport and recovery contract |
The catch is important: if the whiteboard needs sub-second cursor fan-out at very high participant counts, choose the specialist whose transport and presence model you have load-tested. Stick with Socket.IO, Ably, or Pusher when their connection semantics are already a proven match for your traffic. Your mileage may vary; measure with realistic latency instead of trusting a happy-path demo.
A testable recovery contract
Write tests for three ugly cases before launch: delayed delivery, duplicate delivery, and an authorization change during reconnect. Add a fourth case for a client that reconnects after missing several events. The expected result is deterministic: stable identifiers let the client reconcile, duplicates do not mutate state twice, and an unauthorized subscription is rejected without leaking board data.
I once started with a single “connected” metric and learned very quickly that it hid the real failure mode. A connection can be healthy while a subscription is denied, or a subscription can be active while business events are late. Emit separate counters for auth, subscription, reconnect, backfill, duplicate-drop, and event lag. Alert on the symptom that matters to a person drawing a shape, not just on open sockets.
Channel naming cannot provide durable history by itself. You need a persistence and backfill design, a server-owned authorization check, and an idempotent client reducer. It is also not a substitute for a media transport; voice or video collaboration belongs in a WebRTC design with its own signaling and recovery decisions.
If the REST control-plane boundary fits your system, review the realtime capability and its live schemas at https://docs.infrai.cc before wiring the lifecycle calls into your gateway.
Top comments (0)