Skip to content

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.

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.

  1. Read current presence from Peer.online and Peer.last_seen_at in a first-page inbox load (GET /v1/conversations) or in GET /v1/conversations/{id}.
  2. Subscribe to each peer you show, with that peer’s presence_pass.
  3. Apply each presence.changed event: {user_id, online, last_seen_at}.

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.

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.

  • 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_at and Peer.presence_pass are present only when presence is visible to the viewer.
  • last_seen_at is null when the user has last_seen: nobody (set with PATCH /v1/me) or presence.show_last_seen is off. online stays visible.
  • last_seen: nobody works both ways: that user sees nobody’s last_seen_at either. Their own presence.changed events carry last_seen_at: null. Events from other users still carry it, so a client whose own Me.last_seen is nobody must ignore last_seen_at in presence.changed.

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.

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.

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.

  • 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.
  • A catch-up does not recover presence: presence takes no seq and 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.