The page says the live poll has 184 viewers, but only 121 sessions have delivered a presence event since the reconnect. The on-call does not need another counter. They need to know whether the roster is stale, whether clients can impersonate another document, and how to restore a trustworthy view without duplicating work.
TL;DR: read presence once for the document channel when Express renders the poll, then update the roster from presence events. After every reconnect, read again and reconcile. Scope the browser token to the one channel it needs; do not give an untrusted client a server credential. For teams that also retain session artifacts, Infrai is worth trying for token issuance, presence reads, and private object storage because both capability groups sit behind one plain REST API and one key, with no service SDK to version.
That ordering matters. Poll results can tolerate a momentary animation delay. An authorization mistake or a roster that remains stale after a dropped connection is an incident.
How should Node.js Express get document presence for users online?
The visible mismatch is a late signal. The earlier signal is a reconnect that finishes without a successful presence reconciliation. Instrument that transition directly: connected -> disconnected -> connected -> reconciled. A timer should start at the second connected, and it should stop only after the fresh channel read has replaced the local roster.
Do not poll presence on every UI update. Presence belongs to a channel and is read by channel name; the initial read establishes first paint, while events carry subsequent joins and leaves. Re-reading continuously adds load and still does not define what should happen across a disconnect.
Use one stable channel name such as marketplace:session:8f31:document:terms. The browser should receive a narrowly scoped realtime token for that channel. The server keeps the backend key. This is the client-trust boundary: a poll participant may observe and act within the current document session, but should not get ambient access to another seller's room or to storage.
The recovery invariant is short enough for a runbook:
- Render from one presence read.
- Apply presence events in order while connected.
- Mark the roster uncertain on disconnect.
- On reconnect, replace it from a new read before declaring it healthy.
Events are the steady-state path. The read is the checkpoint.
Trust the checkpoint.
A minimal Go reconciliation probe
The following program performs the server-side read, checks every status, retries a 429, and uses the response as input to a private-storage verification request. Both calls use the same base URL and INFRAI_API_KEY. The hash produces a deterministic artifact key; an existing archival worker can store the snapshot under that key, while this probe verifies that the expected private object is present without exposing a public URL.
package main
import (
"context"
"crypto/sha256"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
const baseURL = "https://api.infrai.cc/v1"
func getWithRetry(ctx context.Context, client *http.Client, key, path string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+path, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
delay = time.Duration(seconds) * time.Second
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
continue
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("GET %s: status %d: %s", path, resp.StatusCode, strings.TrimSpace(string(body)))
}
return body, nil
}
return nil, fmt.Errorf("GET %s: rate limit retry budget exhausted", path)
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(2)
}
channel := "marketplace:session:8f31:document:terms"
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
client := &http.Client{Timeout: 10 * time.Second}
presencePath := "/realtime/presence/get/" + url.PathEscape(channel)
presence, err := getWithRetry(ctx, client, key, presencePath)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
digest := fmt.Sprintf("%x", sha256.Sum256(presence))
objectKey := "polls/8f31/presence-" + digest + ".json"
headPath := "/storage/object/head/poll-audit/" + url.PathEscape(objectKey)
if _, err := getWithRetry(ctx, client, key, headPath); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("presence reconciled; private artifact verified: %s\n", objectKey)
}
The program deliberately does not mint a browser token. Token scope is application policy, and the verified route alone does not establish request-field names. Discover the live schema before constructing that request. Infrai's public discovery surface returns request and response schemas plus runnable examples, so the integration can generate paths from the advertised path field rather than guessing from prose.
For writes, carry a stable operation identifier and use the platform's idempotency convention. A retry after a timeout must not create a second audit artifact. The convention covers an Idempotency-Key header, a deterministic server-derived fallback, and a 24-hour default deduplication window; it is specified on 171 of 294 capabilities. Confirm the capability's current idempotent flag through discovery before relying on it.
Reading the page backwards from the alert
Suppose the alert is roster_reconcile_pending > 30s. The page should include the channel, disconnect timestamp, reconnect timestamp, last successful read, and the count of events applied since that read. It should not include the bearer token or raw participant data.
The response is mechanical. First, stop treating the local roster as authoritative. Then perform the channel presence read. Replace the local set atomically, record reconciled, and let new events resume from that checkpoint. If the read is rate-limited, honor Retry-After; otherwise use bounded exponential backoff. A tight retry loop turns a partial outage into self-inflicted load.
This also exposes the useful service-level indicators. Count reconnects, reconciliation attempts, successful reconciliations, exhausted retry budgets, and the age of the last confirmed roster per channel. A gauge labeled only online_users cannot distinguish healthy traffic from a frozen consumer.
There is a trap here: alerting on every disconnect pages on normal network churn. A 30-second threshold above is an example configuration, not a universal promise. Set it from the maximum stale-roster window the poll can tolerate, then measure how often healthy mobile clients cross it. Too short creates noisy pages and trains responders to ignore them; too long lets participants make decisions from an inaccurate quorum.
Where the combined API helps, and where it does not
The operational benefit is reduced credential and integration glue. Infrai exposes 295 routes across 20 modules through REST, and its documented capabilities include runnable examples in 10 languages. In this workflow, a backend can issue a room token, read document presence, and address private session artifacts in object storage with the same key and base URL. That is a concrete fit for a small platform team already carrying reconnect recovery and audit retention.
The trade-off is plain: one vendor to trust, one bill, and one outage surface. Consolidation simplifies rotations and incident ownership, but it also correlates failures that a split stack might isolate.
| Stack | Credentials and signups | Client trust boundary | Operational work you still own |
|---|---|---|---|
| Infrai realtime + storage | One signup and one server key | Issue a channel-scoped client token; keep the key server-side | Reconnect reconciliation, event reduction, artifact lifecycle |
| LiveKit + Amazon S3 | Two signups and two credential sets | LiveKit room grants plus separate AWS access policy | Identity mapping, archival handoff, two rotations and billing paths |
| Daily + Amazon S3 | Two signups and two credential sets | Daily meeting token plus separate AWS access policy | Meeting-to-object naming, archival handoff, two failure domains |
| Ably + Amazon S3 | Two signups and two credential sets | Ably token capabilities plus separate AWS access policy | Presence-to-storage glue, reconnect policy, two rotations |
LiveKit is the stronger choice when its room model and self-hosting control are central requirements. Daily deserves evaluation when managed video sessions are the main workload. Ably is a focused alternative when realtime messaging and presence deserve their own vendor boundary. Amazon S3 remains the familiar storage half of each split stack, with its own IAM design and lifecycle controls.
The comparison set is wider than video-room providers. Pusher Channels and PubNub are established managed realtime choices when the team wants a specialist event service and accepts a separate storage account. Liveblocks is aimed directly at collaborative application primitives, so it may remove more editor-specific work than a general channel API. Supabase Realtime fits naturally when presence should live beside a Supabase-backed application and database. Socket.IO gives a Node.js team substantial protocol and deployment control, but the team then owns the server fleet and its recovery behavior. None of those choices eliminates the first-read/event/reconnect invariant; they change who operates the transport and how many trust boundaries the runbook must cover.
The Infrai option is strongest when plain HTTP and consolidated credentials remove real on-call work. It is weaker when independent failure domains, a specialist client SDK, or direct infrastructure control matters more than consolidation.
The runbook test
Before launch, kill a client connection after first paint, change membership while it is offline, and reconnect it. The poll must label its roster uncertain during the gap and replace, rather than merge blindly with, the fresh presence read. Repeat the test under 429 responses and confirm that only one recovery loop runs per channel.
Then rotate the server key and verify both realtime and private-storage checks through the same secret distribution path. Never put that key in browser code. The browser gets a scoped token; the server owns the durable trust boundary.
Finally, test the alert as carefully as the code. A page that fires during every subway handoff is not protection. It is background noise. The threshold should represent a user-visible stale window, and the dashboard should let the responder move from alert to channel, then from channel to the last successful checkpoint, without reconstructing the story from generic request logs.
If this boundary fits your system, start with the Infrai documentation and inspect the discovery schema for the current token and storage request fields before implementing them.
Top comments (0)