AbernathyCross6857Use a short-lived, server-authorized lease for every realtime channel, and make reconnect plus replay...
Use a short-lived, server-authorized lease for every realtime channel, and make reconnect plus replay part of the protocol. That is the dependable choice for a collaborative whiteboard because a browser tab can disappear without saying goodbye, while a whiteboard still needs one ordered view of the document.
The deciding constraint is trust. A client may draw a cursor, but it must not mint a channel token, widen its scope, or declare its own version current. A reconnect race can turn a harmless laptop sleep into a forked canvas. The fix is protocol work, not a longer timeout.
Treat a channel as a lease with explicit states: requested, active, draining, and closed. The authorization service issues a token scoped to one board and one role, with an expiry measured in minutes. The realtime gateway checks that token on connect and on every publish; it does not trust a board ID supplied by the client message.
Every accepted operation carries a monotonically increasing server sequence. Clients keep the last sequence they applied. On reconnect, the client presents that cursor, receives a bounded replay, then a snapshot if the cursor has fallen outside the retention window. This gives an honest boundary: a live stream is an optimization, while the snapshot is the recovery contract.
Here is the critical path in Python-like pseudocode. The storage and transport are intentionally generic.
def open_channel(user, board_id, presented_token, last_sequence):
claims = auth.verify(presented_token)
if claims.subject != user.id:
raise PermissionError("subject mismatch")
if board_id not in claims.boards:
raise PermissionError("board scope missing")
channel = gateway.accept(board_id, claims.role)
oldest = event_log.oldest_sequence(board_id)
if last_sequence is None or last_sequence < oldest:
channel.send(snapshot_store.latest(board_id))
last_sequence = snapshot_store.sequence(board_id)
for event in event_log.after(board_id, last_sequence):
channel.send(event)
return channel
The replay loop must be idempotent from the client's perspective. A duplicated event is less dangerous than a missing one, so the reducer ignores a sequence it has already committed. Keep presence and cursor updates on a separate, lossy channel; losing a cursor is acceptable, losing a shape mutation is not.
The useful boundaries are visible in operations, not in a diagram. A lease expires while a tab is offline. The server then rejects writes with an authorization result, and the client refreshes the lease before reopening. During draining, new publishes stop, but already accepted events finish fan-out. Closure records a reason and the final sequence so support can distinguish a normal idle timeout from a revoked session.
Short tokens reduce blast radius, but they create clock and refresh edge cases. Allow a small server-side grace period for skew, never for an unbounded retry. Rate-limit refresh by user and board. A token scoped only to board:read should be unable to submit an operation even if the UI accidentally exposes the button. It's a small distinction with a large incident-radius payoff: authorization belongs at the write boundary, not only in the button handler.
One line matters in logs: board_id, channel_id, token subject, scope, lease state, last sequence, and close reason. Do not log the bearer token. Correlate the client operation ID with the server sequence so a support engineer can answer “was this applied?” without reading packet captures.
| Design | Strength | Failure boundary | Best fit |
|---|---|---|---|
| Long-lived socket with session cookie | Minimal reconnect code | Revocation and tab duplication are hard to reason about | Trusted internal tools |
| Short lease plus replay cursor | Clear trust boundary and predictable recovery | Requires refresh and event retention | Multi-user whiteboards |
| Peer mesh with data channels | Low relay traffic for small rooms | Membership, ordering, and mobile churn become application problems | Small, explicitly trusted rooms |
WebRTC data channels can be useful for low-latency peer traffic, but they do not remove the need for application authorization or a durable document authority. The W3C recommendation describes transport behavior; it does not define your board's conflict policy or audit trail.
Property tests should generate sleep, duplicate connect, token expiry, out-of-order delivery, and a snapshot taken between two replays. The invariant is simple: after convergence, every authorized client has the same document hash and no unauthorized operation appears in the log.
The long case is worth spelling out. Imagine client A commits sequence 410, then the laptop sleeps while client B commits 411 through 418. A wakes with an expired lease and sends its stale cursor. The gateway must authenticate a fresh lease, notice that 410 is older than the retained stream, and choose a snapshot or a replay from the exact cursor. If A publishes before that repair completes, the server assigns a new sequence after 418; it never accepts A's local counter as authority. That ordering rule keeps conflict resolution in one place and makes the audit trail explainable weeks later.
Small detail.
I start with a deterministic fake clock. Then I run a browser test that suspends a tab for longer than the lease, edits offline, and reconnects twice. The first implementation usually passed ordinary reconnects and failed the second connect because both sockets believed they owned the same presence record. Presence is ephemeral; document authority is not.
Your mileage may vary with mobile background limits, so measure reconnect duration and replay size in production. Alert on gaps in sequence numbers, refresh denial spikes, and snapshots that are repeatedly larger than the retention budget.
I would reject a single, non-expiring channel token for a public support whiteboard. It makes revocation slow, encourages overly broad scopes, and turns a leaked browser credential into a durable write capability. The catch is that lease refresh adds moving parts and a dependency on the authorization service.
That rejected design is still valid for a kiosk on a private network where identities are fixed, the board is disposable, and operators can revoke the whole session at the network boundary. For a customer-facing board, stick with scoped leases, replay, and a snapshot authority; the extra protocol states buy a much smaller incident radius.