Appearance
Presence without persistence
A room showed six people online. Only one browser was open.
The application had stored online: true in the shared document database. Mobile browsers had been suspended, laptops had lost power, and no client had written online: false. The durable record had outlived the fact it claimed to represent.
Presence is an estimate based on recent connection evidence. It should usually disappear when the server loses that evidence.
Presence map
text
socket connects
|
v
room membership in memory
|
client announces display state
|
server broadcasts current snapshot
|
heartbeat proves liveness
|
close, timeout, or server restart removes memberPersisted documents and presence messages may share a WebSocket, but they have different lifetimes and recovery rules.
Terms
Presence. A short-lived view of who or what appears active in a shared context.
Connection. One live transport from one browser tab, device, or process.
Heartbeat. A periodic ping and response used to detect dead connections.
Lease. Authority or state that expires unless renewed.
Awareness. Ephemeral collaborative state such as cursor position, selection, or typing status.
Grace period. A short delay before declaring a temporarily disconnected participant gone.
Ghost. A presence entry left behind after its connection no longer exists.
Model connections before people
One person can have a phone, laptop, and two tabs open. One shared capability can be used by several people. A display name may be duplicated.
The server's reliable primitive is a connection, not a person.
A useful internal record looks like:
ts
type LiveClient = {
connectionId: string;
actorKey: string | null;
displayName: string | null;
lastSeenAt: number;
writable: boolean;
};connectionId belongs to the server-side socket. actorKey may be an authenticated user ID, a registered device ID, or null for an unverified capability holder. displayName is presentation, not identity.
The public presence list can group connections by actor when the actor is trustworthy. If it is not, show names as self-reported labels or simply show a connection count.
Deduplicating only by display name is compact but misleading. Two people named Sam collapse into one. A person can also open two tabs under different names.
Keep presence out of durable documents
Room membership can live in memory when one service process owns each room. On socket join, add the client. On leave or heartbeat failure, remove it. Broadcast a fresh snapshot.
ts
function presenceSnapshot(room: Room) {
return {
type: "presence",
members: [...room.clients].map(client => ({
id: client.publicId,
name: client.displayName,
role: client.writable ? "editor" : "viewer",
})),
};
}
function leave(room: Room, client: LiveClient) {
room.clients.delete(client);
broadcast(room, presenceSnapshot(room));
}If the server restarts, every connection closes and clients reconnect. Presence rebuilds from live sockets. That reset is correct.
Several server instances need a shared ephemeral registry or room ownership. Redis pub/sub alone fans out messages but does not provide reliable membership. Add expiring keys, a presence service, or route one room to one owner. Every record needs a lease.
Heartbeats detect half-open connections
A close event is not guaranteed. Networks disappear without a clean TCP shutdown. Browsers freeze in the background.
Have the server send a WebSocket ping on a fixed interval. Mark the client unresponsive before the ping, and mark it alive on pong. If the next interval finds it still unresponsive, terminate the socket and remove presence.
Thirty seconds is a common starting point, not a law. Faster detection costs more traffic and creates more churn on unstable mobile networks. Slower detection leaves ghosts longer.
Application-level heartbeats may be needed when intermediaries do not expose protocol ping and pong. Include sequence or time information only for diagnostics. Do not trust a client-provided timestamp as proof of current liveness.
Typing and cursors are even shorter lived
Typing indicators, pointer positions, selected cards, and drag previews are awareness signals. They should:
- Be rate-limited.
- Carry small payloads.
- Expire within seconds.
- Avoid the durable change log.
- Avoid triggering offline outbox writes.
- Be dropped freely under load.
A reconnect may resend the current selection. It should not replay every cursor move made while offline.
Use coalescing. If a client sends pointer positions faster than the network can deliver them, keep only the latest position. Reliable ordered delivery is wasted work for stale motion.
Presence is not authorization
A read-only visitor may appear in a room but still cannot write documents. A disconnected editor may remain authorized even though absent from presence.
Never grant access because a name appears in the room list. Never revoke durable membership because a phone went offline. Presence answers "who seems connected now?" Authorization answers "who may act?"
Likewise, presence is not a billing metric or a count of unique people unless authenticated identities and grouping rules support that claim.
Reconnect without flicker
Mobile connections flap. Removing a participant immediately can make the room list flash.
For a polished product, retain a short disconnected state keyed by an authenticated actor or reconnect token:
- Mark the actor reconnecting for three to ten seconds.
- Replace the old connection if a valid reconnect arrives.
- Remove it after the grace period.
Do not use a self-asserted display name as the reconnect key. For anonymous capability rooms, brief flicker may be more honest than pretending identity continuity.
The client should expect complete presence snapshots. Applying only join and leave deltas can leave ghosts after a missed message. A snapshot on connect and after membership changes keeps the model simple.
Rejected approaches
Store online in the document database. Crashes make it stale, and every heartbeat creates durable writes.
Replay presence from the offline outbox. Old awareness has no value.
Count names. Names are neither unique nor verified.
Assume a socket open event means a human is active. A background tab can keep a connection while nobody looks at it.
Guarantee exact presence. Distributed systems and network delay make presence approximate. Use product copy such as "connected" or "in this room," not "actively viewing this second."
Failure modes
- A socket error is logged but the client remains in the room set.
- A server subscribes to room pub/sub and never unsubscribes after the last client leaves.
- Presence broadcasts expose private account identifiers to capability guests.
- A client can send an unbounded display name or payload.
- Heartbeats continue after shutdown and keep the process alive.
- Background tabs reconnect in a tight loop.
- Two server instances both claim the same connection.
- A reconnect creates a second member because the old lease has not expired.
- Presence updates are persisted in the same stream as document changes.
Field checklist
Presence gives shared apps a live social layer. Games add timing, simulation, fairness, and recovery. Chapter 20, Tiny game engines, shared worlds shows where a general sync engine still works and where it stops. Identity boundaries are covered in Chapter 18.