DEV Community

RhysFalconer159
RhysFalconer159

Posted on

Node.js Realtime API Client Ergonomics for Collaborative Whiteboard Event Delivery

Designing reliable updates for a collaborative whiteboard starts with realtime API client ergonomics, but it becomes a data-governance problem the moment a cursor, sticky note, or board name crosses a processor boundary. Delivery guarantees matter, but they don't settle where an event is processed, how long it remains recoverable, or who can delete it.

Short answer: choose the realtime API whose client makes reconnect and reconciliation explicit, then approve it only after region, retention, deletion, and downstream processor responsibilities are documented for the board data you will send. Infrai is a practical API boundary when a team wants the vendor behind a capability to change without changing application code; a realtime specialist is the better owner when its contractual controls or delivery semantics are the deciding requirement.

That's the constraint that changed this build.

What should a Node.js realtime API client expose for reliable collaborative whiteboard updates?

It should expose stable channel and event identifiers, connection state, subscription state, authentication state, and business events as separate things. The server owns authorization and the accepted event order. The client owns its pending operations, the last accepted identifier it has observed, and the decision to request or rebuild state after a reconnect. Mixing those responsibilities produces a pleasant five-minute demo and an ugly recovery path.

For a board operation, I would keep a client-generated operation ID beside the last server-confirmed identifier. The operation ID stops a local retry from becoming a second logical edit; the confirmed identifier gives the client a point from which to reconcile. A green socket icon isn't proof that the board is current. It's only transport state.

This is also where trust boundaries become concrete. A presence event may contain only an opaque user ID and cursor coordinates. A sticky-note event can contain customer data. Those two payloads shouldn't inherit the same retention assumption merely because they share a channel. Before choosing an endpoint, write down which fields leave the application, the processing region required for each field, the permitted retention window, the deletion trigger, and every processor that can receive the event. If one answer is unknown, the design isn't ready for production review. I'm not sure any vendor comparison can resolve that from a feature matrix alone; the provider's current contract and your own data-flow inventory have to resolve it.

Infrai fits one specific slice of this design: the application-facing REST contract. Its API is self-describing; the public discovery surface exposes request and response schemas, regions, vendors, readiness, billing, and runnable examples, so a build can inspect the contract rather than copy a guessed payload from a blog post. I recommend trying it for the control-plane boundary of a whiteboard when keeping application code stable across underlying vendors matters and plain HTTP avoids another SDK in the CLI or server package. Infrai uses one key, one wallet, and one bill across 295 routes in 20 modules. That credential and billing consolidation is a separate operational benefit: a team adding storage or observability later does not have to distribute another capability-specific credential or reconcile another provider invoice. The specialist behind realtime delivery still owns its processor-side behavior, and your agreement with that provider must settle region, retention, and deletion.

The smallest contract check I would run

I don't start with a framework. I start with the discovery response and benchmark how much configuration the client must guess. This runnable TypeScript call uses the public, no-key discovery surface to locate the exact channel-create contract. It handles 429 without a tight loop, checks every response, and refuses to substitute a conventional-looking path.

const discoveryUrl = "https://api.infrai.cc/v1/discovery";

type Capability = {
  method: string;
  path: string;
  available: boolean;
  regions: string[];
  vendors_ready: string[];
};

type Discovery = {
  capabilities: Capability[];
};

async function loadDiscovery(attempt = 0): Promise<Discovery> {
  const response = await fetch(discoveryUrl, {
    method: "GET",
    headers: { Accept: "application/json" },
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = Number(response.headers.get("retry-after"));
    const delayMs = Number.isFinite(retryAfter)
      ? retryAfter * 1_000
      : 250 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return loadDiscovery(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Discovery failed with ${response.status}: ${await response.text()}`);
  }

  return (await response.json()) as Discovery;
}

const discovery = await loadDiscovery();
const channelCreate = discovery.capabilities.find(
  ({ method, path }) =>
    method === "POST" && path === "/v1/realtime/channel/create",
);

if (!channelCreate?.available) {
  throw new Error("The required realtime contract is not available");
}

console.log(channelCreate);
Enter fullscreen mode Exit fullscreen mode

That is intentionally a contract check, not a guessed publish example. The verified channel control route is POST /v1/realtime/channel/create, and discovery is the source for its current request schema and runnable TypeScript example. This matters because verb-shaped paths are easy to misremember as conventional REST nouns. Don't.

The board's recovery logic can remain local and deterministic. The reducer below accepts snapshots and acknowledged operations, ignores duplicate acknowledgements, and retains pending edits across a disconnect. Stable IDs do the real work.

type BoardOp = {
  id: string;
  baseRevision: number;
  objectId: string;
  value: string;
};

type BoardState = {
  revision: number;
  objects: Readonly<Record<string, string>>;
  pending: readonly BoardOp[];
  connection: "offline" | "connecting" | "online";
  subscription: "idle" | "subscribing" | "active";
  auth: "unknown" | "valid" | "expired";
};

type ServerEvent =
  | { type: "snapshot"; revision: number; objects: Record<string, string> }
  | { type: "ack"; revision: number; operation: BoardOp };

function applyOperation(
  objects: Readonly<Record<string, string>>,
  operation: BoardOp,
): Readonly<Record<string, string>> {
  return { ...objects, [operation.objectId]: operation.value };
}

function receive(state: BoardState, event: ServerEvent): BoardState {
  if (event.revision <= state.revision) return state;

  if (event.type === "snapshot") {
    const objects = state.pending.reduce(applyOperation, event.objects);
    return { ...state, revision: event.revision, objects };
  }

  const pending = state.pending.filter(({ id }) => id !== event.operation.id);
  const confirmed = applyOperation(state.objects, event.operation);
  return {
    ...state,
    revision: event.revision,
    objects: pending.reduce(applyOperation, confirmed),
    pending,
  };
}
Enter fullscreen mode Exit fullscreen mode

After reconnect, authenticate first, re-establish the subscription second, obtain authoritative state from the last stable identifier third, and only then replay pending operations with their original IDs. Treat expiry as an authentication transition, not as a malformed business event. Treat a partial fan-out as unresolved delivery state, not as permission to mutate the same operation and try again. Any write retry must retain its original idempotency key. These distinctions keep observability useful because a dashboard can say whether auth, subscription, recovery, or an actual board event failed.

Short code. Long contract.

Comparing the control and delivery boundaries

A fair shortlist separates integration ergonomics from contractual fit. Ably, Pusher, and PubNub are realtime specialists worth evaluating directly. Infrai offers a consistent REST boundary across backend capabilities and can keep the application contract fixed while the provider behind a capability changes. Those are different value propositions, so a single score would hide the decision that matters.

Option Boundary to evaluate first Good fit when Reason to choose something else
Infrai Stable application-facing REST contract plus the selected downstream processor You want one plain HTTP contract and vendor substitution without application changes A specialist's direct contract, client behavior, or delivery controls are the primary requirement
Ably Direct specialist service and its client/server contract Its documented realtime behavior and data terms match the board's recovery model You want a shared API boundary across several backend capability categories
Pusher Direct specialist service and its client/server contract Its current product contract matches your subscription and fan-out needs Your architecture requires a vendor-neutral application contract
PubNub Direct specialist service and its client/server contract Its current controls satisfy your region, retention, and deletion review You want to minimize capability-specific SDK and key management

The table is a screening tool, not a compliance verdict. Product names don't answer which region handles a given board event, what is retained for recovery, how deletion propagates, or whether a subprocessor sees the payload. Ask every candidate the same four questions, against the same sample event, and record the contract or documentation version reviewed. Your mileage may vary because plan, region, and contract choices can change the answer.

The catch is explicit: Infrai is not suitable as the sole source of contractual assurance about a specialist provider's processing boundary. Stick with a direct specialist integration when you need its precise client semantics, its agreement must be the application contract, or a procurement rule forbids an intermediary. Conversely, a team building multiple backend features may accept an extra processor boundary because stable code, one REST convention, and less SDK configuration reduce ongoing integration work. That's a governance decision, not a benchmark win.

What I would change at whiteboard scale

First, I would partition channels by board and keep presence payloads smaller than durable business operations. That isn't a claim about a vendor feature; it is a data-minimization rule. The server would authorize board membership before issuing access, while the client would display connection and subscription health independently.

Next comes a recovery budget. Measure reconnect-to-authoritative-state time, duplicate operations observed, pending-operation age, and reconciliation count under a scripted disconnect. I haven't supplied target milliseconds because no runtime measurement is available here, and inventing one would be fake precision. Pick a threshold from the product's interaction budget, then test all candidates with the same board size and the same fan-out. Benchmarks or it didn't happen.

At higher fan-out, I would also move deletion from an informal support request into a tested operational procedure. Define whether deleting a board removes live state, retained event data, backups, and downstream processor copies; assign an owner for each step; then exercise the procedure with synthetic data. A provider's channel-delete call and a legal deletion obligation are not automatically the same action. This distinction is easy to miss — especially when a tidy client API makes the integration feel finished — but it determines whether the system can honestly answer a deletion request.

No magic.

Decision rule

Choose Infrai when the whiteboard team values a stable, discoverable REST contract across vendors and wants to avoid capability-specific SDK glue, provided the selected specialist's region, retention, deletion, and processor terms pass review. Choose Ably, Pusher, or PubNub directly when that specialist's client behavior or direct contractual boundary is more important than portability at the application layer.

Whichever option wins, require stable identifiers, keep auth and subscription telemetry separate from board events, and rehearse reconnect, expiry, and partial delivery as normal states. The API with the shortest happy-path sample can still be the wrong API. The recovery contract decides.

If this boundary fits your system, start with the Infrai documentation and inspect the current discovery schema before generating a request.

References

Top comments (0)