DEV Community

CrimsonWave9361502
CrimsonWave9361502

Posted on

Realtime Membership Explained: What Presence Can and Cannot Tell Fintech Editors

TL;DR: Treat presence as a short-lived claim about a session, not proof that a person is attentive. In a fintech collaborative editor, typing indicators can be ephemeral and lossy, while read receipts need a durable statement tied to a specific document revision. A transport connection is useful evidence, but it cannot tell you that a reviewer saw, understood, or approved a change. Keep those meanings separate from the first notebook sketch through production.

That split is the main design decision. It prevents a green dot from becoming an accidental audit record, and it gives each signal a lifetime that matches what it actually knows.

Accuracy comes first.

What can realtime membership presence tell you, and what cannot it tell you?

Presence can report that a named session joined a collaboration scope and refreshed its lease recently enough to remain a member. For example, reviewer-17 may have an active session in wire-transfer-4821, with a last heartbeat timestamp and a declared client state. This supports useful interface hints: show who may be in the document, route transient cursor updates, and decide which sessions should receive a typing event.

It cannot prove that the person is looking at the relevant paragraph. A browser can be connected in a background tab. A laptop can sleep before the server notices. Mobile connectivity can disappear between two packets. One user may also have two tabs or devices, so counting sessions as people inflates the room.

The boundary gets sharper in financial workflows. "Online" must never mean "reviewed," and "read" must never mean "approved." Approval belongs to a separate, authenticated business action with its own authorization and audit semantics. Presence is softer evidence. Use it to improve coordination, not to infer consent.

Signal Defensible meaning Meaning it cannot support
Active membership A session has an unexpired lease in this document The user is watching the document
Typing A recent input hint arrived from an active session A complete edit will be submitted
Read receipt A user reported rendering one named revision The user understood or approved it

WebRTC makes the same architectural separation visible from another angle: its specification defines peer-connection state and media/data transport behavior, while signaling is outside its scope. An application still has to define identity, room membership, and the meaning of its own events. A healthy peer connection therefore does not create a trustworthy user-level presence model by itself.

Start with the event flow

The client authenticates, opens a realtime transport, and joins one document with a unique session ID. The membership service records a lease. Heartbeats extend that lease; disconnect events may remove it early, but lease expiry remains the fallback because clean disconnects are not guaranteed. Typing events fan out without becoming business records. A read receipt takes a different path: the client names the exact revision rendered, and the server stores that assertion only after checking document access.

Consider the sequence around a transfer review. At time 100, one browser tab joins revision 9; at 101, it emits a typing hint; at 102, it reports revision 9 as rendered. If the typing lease ends at 104, the UI should remove the indicator while the member remains present. If revision 10 arrives at 106, the old receipt still describes revision 9 and nothing more. Finally, if the membership lease expires, the green dot disappears, but the durable receipt remains. No single boolean can represent those transitions without discarding meaning.

This is a small model, which is useful. Notebook-to-prod work gets risky when one vague status field is asked to carry transport health, human attention, and workflow completion at once. I would rather make three narrow records than build an evaluation harness around an ambiguous one.

A runnable membership model

The example below uses an injected clock so expiry tests stay deterministic. It models sessions rather than people, then derives a deduplicated user view. Typing has a shorter lease than membership. Read receipts are revision-specific and do not expire with presence.

from dataclasses import dataclass


@dataclass
class Session:
    user_id: str
    document_id: str
    expires_at: float
    typing_until: float = 0.0


class CollaborationState:
    def __init__(self, membership_ttl: float = 30.0, typing_ttl: float = 3.0):
        self.membership_ttl = membership_ttl
        self.typing_ttl = typing_ttl
        self.sessions: dict[str, Session] = {}
        self.reads: set[tuple[str, str, str]] = set()

    def join(self, session_id: str, user_id: str, document_id: str, now: float) -> None:
        self.sessions[session_id] = Session(
            user_id=user_id,
            document_id=document_id,
            expires_at=now + self.membership_ttl,
        )

    def heartbeat(self, session_id: str, now: float) -> None:
        self.sessions[session_id].expires_at = now + self.membership_ttl

    def start_typing(self, session_id: str, now: float) -> None:
        session = self.sessions[session_id]
        if session.expires_at <= now:
            raise ValueError("expired session")
        session.typing_until = now + self.typing_ttl

    def mark_read(self, session_id: str, revision_id: str, now: float) -> None:
        session = self.sessions[session_id]
        if session.expires_at <= now:
            raise ValueError("expired session")
        self.reads.add((session.user_id, session.document_id, revision_id))

    def snapshot(self, document_id: str, now: float) -> dict[str, set[str]]:
        active = [s for s in self.sessions.values()
                  if s.document_id == document_id and s.expires_at > now]
        return {
            "present_users": {s.user_id for s in active},
            "typing_users": {s.user_id for s in active if s.typing_until > now},
        }


state = CollaborationState()
state.join("tab-a", "reviewer-17", "wire-transfer-4821", now=100.0)
state.start_typing("tab-a", now=101.0)
state.mark_read("tab-a", "revision-9", now=102.0)
assert state.snapshot("wire-transfer-4821", now=105.0) == {
    "present_users": {"reviewer-17"},
    "typing_users": set(),
}
assert ("reviewer-17", "wire-transfer-4821", "revision-9") in state.reads
Enter fullscreen mode Exit fullscreen mode

The numbers are example policy values, not protocol guarantees. Tune them against observed heartbeat delay, reconnect behavior, and the amount of stale UI your workflow tolerates. A three-second typing lease may feel responsive, but the correct threshold is the one your own traces and usability checks support. Prompt-cost awareness applies here too: compact, typed events are easier to replay into an AI evaluation fixture than prose-shaped activity logs, and they avoid sending noisy presence history into a model context.

Two details matter in production. First, authorize every join and receipt against the document; knowing an identifier is not permission. Second, define idempotency. Repeating mark_read for the same user, document, and revision should not create a new semantic fact. The set makes that property obvious, though a production store would also retain timestamps and the authenticated actor required by application policy.

Failure modes should drive the evaluation

A happy-path socket test is too weak. Build a replayable harness that advances a fake clock and checks meaning at boundaries: two tabs for one reviewer count as one present user; an expired lease removes both presence and typing; a late typing event from an expired session is rejected; reconnecting creates a new session without erasing a prior revision receipt; and reading revision 9 says nothing about revision 10.

Then test disorder. Deliver duplicate heartbeats. Delay an old typing event until after a newer expiry decision. Drop the clean disconnect. Race a document-permission revocation against a receipt. These cases expose whether the server owns state transitions or merely repeats whatever a client claims.

Metrics should follow the same semantics. Track active sessions separately from unique users, lease expirations separately from explicit leaves, and rejected events by reason. Measure heartbeat delay distributions before changing the membership lease. For read receipts, watch revision mismatches and authorization failures rather than folding them into a generic realtime error counter. This produces evidence an on-call engineer can act on without pretending to measure attention.

There is a cost trade-off. Faster heartbeats reduce the stale-presence window but increase traffic, writes, and wakeups; longer leases do the reverse. Choose the lease from the acceptable false-present window, then verify its operational cost. Do not pick it because a sample used 30 seconds.

That trade-off is deliberate.

Ship the semantics, then the UI

Before release, write the state definitions beside the event schema: a member is an unexpired authorized session; a present user is the deduplicated projection of those sessions; typing is an expiring hint; read means a specific authenticated user reported rendering a specific revision; approval is outside this subsystem. Make the client labels reflect those definitions. "Active in document" is more honest than "watching you edit."

Run deterministic clock tests in continuous integration, then add integration coverage for reconnects, permission changes, process restarts, and delayed delivery. Verify that ephemeral events can be dropped without damaging durable records. Confirm logs avoid document contents and other sensitive payloads, while retaining correlation IDs needed to trace one session through join, heartbeat, receipt, and expiry. Finally, rehearse a degraded mode: editing should remain understandable when typing indicators disappear, and a receipt should never be synthesized from transport activity.

Presence is valuable precisely because its claim is narrow. Keep that claim visible in schemas, tests, dashboards, and copy, and the editor can feel lively without turning connectivity into compliance evidence.

References

Top comments (0)