Presence subscriptions
Presence is whether a user is online and when they were last seen. Each user’s changes go to one channel of their own, and a client subscribes to the peers it shows. A peer’s channel needs a presence pass, which the API hands out only where the viewer may see that peer’s presence.
How it works
Section titled “How it works”Where presence comes from
Section titled “Where presence comes from”A user is online while their client sends heartbeats. The frame posts POST /v1/presence at once and every 25 s while the chat is open and visible, and again when the browser fires online. When the chat closes, or the page has been hidden for 30 s (at once in the installed app), it sends POST /v1/presence/offline with fetch(..., {keepalive: true}) and the body {session_token, hidden_ms}. hidden_ms is how long the page has been hidden, so last_seen_at becomes the moment the user stopped looking.
When heartbeats stop and no offline call arrives, a timer marks the user offline once their last heartbeat is more than 60 s old and publishes presence.changed. The timer fires 60 s after it starts and, while heartbeats keep coming, re-arms itself for 60 s after the latest one, so offline follows the last heartbeat by 60 s or a little more, not at once.
Current state, then live changes
Section titled “Current state, then live changes”- Read current presence from
Peer.onlineandPeer.last_seen_atin a first-page inbox load (GET /v1/conversations) or inGET /v1/conversations/{id}. - Subscribe to each peer you show, with that peer’s
presence_pass. - Apply each
presence.changedevent:{user_id, online, last_seen_at}.
Subscribing with a pass
Section titled “Subscribing with a pass”Peer.presence_pass is a token valid for 1 hour that authorizes a subscribe to /presence/{peer.id}. Subscribe on the socket you already have, with the pass in place of the session token:
{"type": "subscribe", "id": "<your subscription id>", "channel": "/presence/{peer_id}", "authorization": {"host": "<http_host>", "Authorization": "<presence_pass>"}}Unsubscribe with {type: "unsubscribe", id} when the peer leaves the screen. Subscribe again on every new socket, including the one you open after a token refresh.
The frame subscribes to a peer while its online dot or last seen is on screen, at most 100 peers, and unsubscribes once none is on screen. It keeps the newest pass for each peer.
When a pass is refused
Section titled “When a pass is refused”A pass expires after 1 hour. A refused subscribe answers subscribe_error for that subscription and leaves the socket open. Do not retry with the same pass. Fetch fresh passes by reloading the inbox or the conversation. The frame never retries a refused pass and reloads at most once per 10 minutes for this reason.
Privacy
Section titled “Privacy”- A pass is handed out only where the viewer may see the peer’s presence. Visibility, policy and blocks are decided when the pass is issued. A peer without a pass shows no presence.
Peer.online,Peer.last_seen_atandPeer.presence_passare present only when presence is visible to the viewer.last_seen_atis null when the user haslast_seen: nobody(set withPATCH /v1/me) orpresence.show_last_seenis off.onlinestays visible.last_seen: nobodyworks both ways: that user sees nobody’slast_seen_ateither. Their ownpresence.changedevents carrylast_seen_at: null. Events from other users still carry it, so a client whose ownMe.last_seenisnobodymust ignorelast_seen_atinpresence.changed.
presence.fanout
Section titled “presence.fanout”The setting presence.fanout (default true) also pushes each change to every online conversation peer allowed to see it, on that peer’s own /users/{user_id} channel. Turn it off for your app once all your clients subscribe with passes. Until then both paths run.
Order and duplicates
Section titled “Order and duplicates”With presence.fanout on, a client on both paths gets the same change twice, with the same envelope id. Deduplicate on id. Presence events carry no seq. Ordering and limits has the rules.
Do not rebuild presence from webhooks
Section titled “Do not rebuild presence from webhooks”The presence.changed webhook goes to your server, not to users. It carries the real last_seen_at whatever the users’ privacy choices, so forwarding it to browsers would leak what last_seen: nobody hides. It also knows nothing of who may see whom: the API decides visibility, policy and blocks per viewer when it issues a pass. Give clients presence from the API and the socket as above.
Limits
Section titled “Limits”- At most 200 subscriptions per socket, which is AppSync’s quota. The session’s own
/users/{id}channel is one of them, so presence passes have 199. The frame keeps at most 100 presence subscriptions. - A presence pass lasts 1 hour.
- Each subscribe is one AppSync operation and one authorizer call.
Where it ends
Section titled “Where it ends”- A catch-up does not recover presence: presence takes no
seqand the socket does not replay. After a gap, reload current presence as in step 1, then follow live events. - A viewer blocked after receiving a pass keeps getting the blocker’s online changes until its socket closes, and can subscribe again until the pass expires.
- A pass is a bearer token. Anyone who holds it can subscribe until it expires. Never hand it to anyone but the viewer it was issued to.
- A privacy change reaches peers on their next inbox load or presence event, not live.