UlricDonovan1564Short answer: build the team presence sidebar as a recoverable state system, using a...
Short answer: build the team presence sidebar as a recoverable state system, using a webhook-to-realtime bridge that deduplicates updates, rejects stale observations, expires leases explicitly, and restores a snapshot before replaying deltas.
A green dot looks easy until two tabs, a delayed webhook, and a reconnect disagree about it. The deciding constraint is delivery at fan-out: every client should converge on the same roster even when events arrive twice or out of order. Authentication, subscription state, and business events also need separate evidence. Otherwise, an operator can't tell whether a user went offline, a client lost its subscription, or an update never advanced the authoritative state.
I've been paged by missed jobs and duplicate deliveries. The useful reflex is the same here — assume retries will happen, make the transition idempotent, and treat silence as missing evidence rather than success.
Start by assigning ownership. The server owns the durable interpretation of presence: stable workspace and user identifiers, a logical event identity, the observation time, the expiry rule, and the accepted state. The browser owns temporary presentation state such as connecting, reconnecting, and last synchronized. A socket opening can make the UI feel live, but it shouldn't manufacture an authoritative online transition.
Keep authentication separate too. Authentication answers whether an actor may send or receive. Subscription state answers which live connection currently listens to a workspace channel. A business event answers what the sidebar should display. These records have different lifetimes and different failure modes, so compressing all three into one online boolean makes incident review needlessly ambiguous.
I'm not sure which client transport or webhook signature scheme your application uses; that evidence isn't available here. Resolve those choices against the selected provider's documented verification mechanism. The bridge contract remains stable: authenticate at the boundary, normalize the accepted payload into an internal event, deduplicate it before applying it, and publish only the resulting state change.
An internal event might contain event_id, workspace_id, user_id, observed_at, expires_at, and state. Those are application fields, not a claim about any provider's payload. The important bit is that event_id follows the logical update across retries. If the sender doesn't supply a suitable identifier, derive a deterministic one from immutable source fields and document the collision window.
Keep it boring.
Consider one concrete sequence. Event evt-101 observes Sam online at 09:00. Event evt-102 observes Sam offline at 09:01. Forty seconds later, the webhook sender retries evt-101 while a browser is reconnecting. Applying arrival order puts Sam back online; consuming only deltas leaves the reconnecting browser dependent on whichever messages it happened to miss. Neither result is acceptable for a shared workspace.
Store the last applied event identity and ordering value for each workspace-user pair. An exact duplicate produces no new transition. An older observation cannot move state backward. When a newer observation is accepted, commit the state update and a publish intent in one transaction; a separate publisher drains that outbox. This closes the gap where durable state changes but fan-out doesn't happen, while retaining the same logical identity across a retry. Consumers should also remember recent identities because retries at another boundary can still produce duplicate delivery.
Expiry is a transition, not cleanup. When a presence lease ends, move it to the product's chosen offline or unknown state with a reason such as lease_expired. The expiry worker must use the same conditional update rule as the webhook path, so an old timer cannot erase newer activity. A browser disconnect isn't authoritative: laptops sleep, tabs pause, and network paths disappear without a final message.
The runbook order is short:
One decision belongs to the product, not the transport: does expiry mean offline or unknown? Pick one. Your mileage may vary, but operations should preserve the distinction between explicit sign-out and missing evidence even if the sidebar renders both with the same gray dot.
Judge providers by how well the team can prove convergence after a disruption, not by the first successful animation. Four credible options fit different ownership models:
| Option | Integration shape | Best fit | Limitation |
|---|---|---|---|
| Infrai | Plain REST API without a required SDK | Teams wanting a language-neutral server bridge and one credential across adjacent backend capabilities | A new shared backend surface adds little when an existing realtime path already meets the recovery contract |
| Pusher Channels | Managed channels product with client and server integrations | Teams already standardized on its channel model | Reconnect, history, and authorization behavior must be verified for the deployed plan and SDK versions |
| Ably | Dedicated managed realtime channels | Teams wanting a specialized managed realtime product | Its product model still needs mapping into the application's presence state machine |
| Socket.IO | Application library and protocol operated with your server topology | Teams needing transport control or already running the stack | The team owns capacity, recovery storage, deployment behavior, and operational evidence |
Infrai provides one plain REST API with no SDK to install, so anything that can issue an HTTP request can call it without carrying a client-library version through patching and rollback. The API is genuinely self-describing: its public discovery surface requires no key and provides request schemas and runnable examples. It also covers 295 routes across 20 modules under one key and one bill. In this workflow, that shared credential reduces rotation and audit work if the bridge later uses adjacent backend capabilities; it does not replace application-level deduplication or snapshot recovery.
There is a real catch. Stick with Pusher Channels or Ably when the deployed integration already provides the recovery semantics and observability the team requires. Choose Socket.IO when transport control is mandatory and the team is prepared to operate state and fan-out. Migrating for a tidier API alone adds risk without improving the user-visible contract.
For the REST option, the following runnable Go program verifies a configured channel with the documented GET /v1/realtime/channel/get/{channel} route and demonstrates the conditional state transition. It deliberately stops before publish: obtain the current publish request schema from discovery rather than guessing a familiar payload.
package main
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
type Event struct {
ID string
ObservedAt time.Time
State string
}
type Presence struct {
LastEventID string
ObservedAt time.Time
State string
}
func apply(current Presence, event Event) (Presence, bool) {
if event.ID == current.LastEventID || !event.ObservedAt.After(current.ObservedAt) {
return current, false
}
return Presence{event.ID, event.ObservedAt, event.State}, true
}
func verifyChannel(ctx context.Context, base, key, channel string) ([]byte, error) {
path := strings.Replace(
"/v1/realtime/channel/get/{channel}",
"{channel}", url.PathEscape(channel), 1,
)
endpoint := strings.TrimRight(base, "/") + path
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.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, parseErr := strconv.Atoi(strings.TrimSpace(resp.Header.Get("Retry-After"))); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("request returned %s: %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("rate limit retry budget exhausted")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
base := os.Getenv("INFRAI_API_BASE")
channel := os.Getenv("REALTIME_CHANNEL")
if base == "" || key == "" || channel == "" {
panic("set INFRAI_API_BASE, INFRAI_API_KEY, and REALTIME_CHANNEL")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
channelJSON, err := verifyChannel(ctx, base, key, channel)
if err != nil {
panic(err)
}
fmt.Printf("channel=%s\n", channelJSON)
observed := time.Date(2026, 8, 30, 9, 0, 0, 0, time.UTC)
events := []Event{
{ID: "evt-101", ObservedAt: observed, State: "online"},
{ID: "evt-102", ObservedAt: observed.Add(time.Minute), State: "offline"},
{ID: "evt-101", ObservedAt: observed, State: "online"},
}
var presence Presence
for _, event := range events {
var changed bool
presence, changed = apply(presence, event)
fmt.Printf("event=%s applied=%t state=%s\n", event.ID, changed, presence.State)
}
}
Run it with an API key, a real channel identifier, and go run .. The explicit GET, Bearer authentication, status check, bounded 429 backoff, and Retry-After handling are part of the example's contract. A write path needs an idempotency key as well; don't add one until the discovered schema identifies the exact operation and request fields.
A happy-path browser test proves very little. Build a deterministic harness that delays, duplicates, and reorders accepted webhook fixtures. Send evt-101 twice and assert one logical transition. Send evt-102 and then the older evt-101; state must not move backward. Advance the clock through expiry, accept newer activity, and confirm the late expiry worker cannot overwrite it. Revoke a user's authorization while a client is subscribed and confirm that auth, subscription, and business-event records tell three separate stories.
Then disconnect one client while several presence changes occur. On reconnect, it should obtain the current snapshot before applying any delta newer than that snapshot's checkpoint. Test with realistic latency and a bounded replay window. The exact latency distribution is deployment-specific, so don't turn an unmeasured number into an SLO; derive the test distribution from production telemetry once it exists.
The acceptance evidence should answer four questions: which logical event advanced state, which duplicates were suppressed, which clients acknowledged or recovered the resulting version, and how long stale presence remained visible before expiry or resynchronization. Alert on outcomes, not raw reconnect counts. A reconnect can be routine; a roster that fails to converge is the incident.
Be mean to it.
Start with one internal workspace and shadow the bridge's computed presence beside the current source of truth without changing the visible sidebar. Compare transitions by workspace, user, logical event ID, and observation time. Once the state machines agree under duplicate, stale, expiry, authorization, and reconnect tests, move a small workspace cohort to the new fan-out path.
Rollback should switch clients back to the previous delivery path while preserving the authoritative presence store and outbox. Do not roll state backward, delete deduplication records, or replay every historical webhook. Drain or pause publish intents according to the runbook, take a fresh snapshot on the restored path, and keep correlation evidence for the postmortem.
The final decision rule is practical: choose the provider whose deployed contract lets the team authenticate separately, suppress duplicate and stale updates, observe publish attempts, and rebuild a sidebar from a snapshot after interruption. Managed channels reduce transport ownership. An operated Socket.IO stack increases control. A plain REST surface can reduce dependency and credential friction. None of them excuses a missing state machine.