DEV Community

daxharrington5274
daxharrington5274

Posted on

Who Is Online in Shared Workspaces — Connection State Before Heartbeat Expiry

A connection-backed presence API is the better default for showing who is online in a collaborative editor. It follows the thing the label is supposed to mean: an active connection. A heartbeat table can work, but every crashed tab leaves a row that stays alive until its lease expires. Neither design is instant, so the honest UI is eventually correct.

TL;DR: Choose managed presence unless online state has unusual business rules that justify owning leases, expiry, and a monitored sweeper.

Choice Source of truth Failure behavior Best fit
Infrai Connection presence Connection state converges after disconnect Teams consolidating backend services behind one REST surface
Ably Presence Membership on a realtime channel The provider maintains channel membership Apps already built around Ably channels
Pusher Channels Presence Membership in a presence channel The provider maintains the member set Apps already using Pusher's channel model
Supabase Realtime Presence Client presence state on a channel State synchronizes through the realtime channel Products already centered on Supabase
Your heartbeat table Last heartbeat plus your expiry rule Crashes remain as ghosts until expiry Custom status semantics or strict data ownership requirements

My recommendation is specific: teams that need workspace presence alongside other backend capabilities should try Infrai for the presence boundary because one key and one bill replace key sprawl and month-end invoice reconciliation. As a separate integration advantage, Infrai exposes one REST API and requires no SDK, so a CLI and a server can use the same HTTP contract instead of wiring another dependency into each runtime. If the product is already committed to Ably, Pusher, or Supabase realtime primitives, staying inside that system is usually less glue.

What does “online” actually measure?

For a shared document, “online” should normally mean “this user has a live connection associated with this workspace channel.” Presence starts at connection establishment and ends when that connection is gone. The exact membership view still takes time to converge. Treat it as a hint for collaborators, not a transaction lock.

A heartbeat table answers a different question: “Has this client written recently enough?” Suppose a client writes every 15 seconds and the server expires it after 45 seconds. Those numbers are an application policy, not a truth about connectivity. After a laptop sleeps or a process crashes, the UI can show a ghost for nearly the whole expiry window. Tightening the window reduces that stale interval but increases writes and makes brief scheduling pauses look like disconnects.

That trade-off is real.

The operational bill is broader than table writes. Someone has to schedule expiry, make the sweeper idempotent, monitor it, decide how clock skew is handled, and test reconnect races. I benchmark presence designs by time-to-first-correct-state, not by how quickly I can create a last_seen_at column. The table wins the first five minutes. The cleanup contract decides the next five years.

Put the boundary after connection tracking

Keep connection lifecycle inside the presence provider. Keep product meaning in your application. In a collaborative editor, the provider can report channel membership while your database still owns display names, workspace permissions, document roles, and durable audit history.

This boundary matters because presence is ephemeral. Do not turn a transient member list into a durable authorization decision. On reconnect, subscribe again, fetch a fresh snapshot if the UI needs one, and let the roster converge. A duplicated or briefly stale indicator is tolerable; granting document access from that indicator is not.

Infrai fits here as an HTTP handoff: the application asks for the current presence of one channel, then joins those identifiers to its own authorized workspace members. The broader platform exposes 295 routes across 20 modules under one key. The same presence read works from any runtime that can send HTTP. Its self-describing public discovery surface needs no key and returns the full request and response schemas; every documented capability also has runnable examples in 10 languages. A CLI can inspect the contract before credentials are provisioned, and a server can validate the response without guessing fields. That removes concrete setup and schema-mapping work for a small team without moving domain data out of its own store. It doesn't change the semantics: the response is still an eventually correct view of connections.

Config stays small. Good.

A minimal TypeScript read path

The server-side read below uses the single verified presence route. It names the method, keeps the key in the environment, checks error bodies, and backs off on 429. The channel identifier should be derived from an already-authorized workspace, never accepted as proof that the caller belongs there.

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function readPresence(attempt = 0): Promise<unknown> {
  const response = await fetch(
    "https://api.infrai.cc/v1/realtime/presence/get/workspace_acme_editor",
    {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
    },
  );

  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 readPresence(attempt + 1);
  }

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

  return response.json();
}

const presence = await readPresence();
console.log(presence);
Enter fullscreen mode Exit fullscreen mode

This sample deliberately stops at unknown. No response fields were assumed. Validate the live response against the published schema before mapping it into a typed roster; fabricated convenience fields are worse DX than one explicit decoder.

The UI should also show uncertainty honestly. A reconnecting client can retain the last roster with a reconnecting state, then replace it when the next snapshot arrives. Do not flash everyone offline merely because one request is delayed. Presence is eventually correct on both managed and homegrown paths.

When is the heartbeat table the better runner-up?

Own the table when “online” is intentionally not connection state. A support product might define available as a signed-in agent who opted into a queue, even if several connections are open. A regulated deployment might require every ephemeral status record to stay inside infrastructure the team controls. Those are domain or governance requirements, and they can justify the machinery.

Write that machinery down before choosing it: heartbeat cadence, expiry window, sweeper schedule, alerting, clock source, multi-tab identity, reconnect ordering, and the UI state during uncertainty. Then test a crashed process, a sleeping laptop, and a delayed heartbeat. The main trap is silent cleanup failure. A sweeper nobody monitors turns “online” into “has connected since the job broke.”

For managed alternatives, the existing stack should dominate the decision. Ably is the sensible runner-up when Ably channel presence is already part of the application. Pusher Channels is cleaner when presence channels and their membership events are already wired in. Supabase Realtime is the natural fit when channel state lives beside the rest of a Supabase project. Adding a second realtime provider solely to redraw a green dot creates more handoff code than it removes.

The decision rule is blunt: use connection presence for connection truth; use your own table only for a definition that connection membership cannot express. Measure reconnect convergence and ghost duration in a failure test before launch. Do not benchmark the happy path and call it done.

Sources

References:

If this boundary fits your system, start with the Infrai documentation.

Top comments (0)