Skip to content

Presence — Domain Spec

Presence is two independent signals: online (a short-TTL heartbeat key) and actively sharing live location (a typed, finite-duration session). This page is the rules for both and how they compose with the privacy toggles that gate them.

Mental model

Two shapes:

  1. Heartbeat → online. Client posts POST /presence/heartbeat, server sets presence:{userID} = "1" with a 3-minute Redis TTL. If the heartbeat stops, the user goes offline implicitly within three minutes.
  2. Typed session → live location. Client calls POST /presence/active/start with one of four allowed intervals. Server writes a session blob to Redis (TTL = interval) and a row to active_location_sessions (for restart recovery).

Heartbeat semantics

Property Value
Key presence:{userID}
Value "1"
TTL 3 minutes
Endpoint POST /presence/heartbeat
Recommended cadence Every ≤2 minutes from an active client
Going offline Implicit, by TTL expiry. No presence/stop endpoint

Active-location intervals

Allowed value (minutes) UX label (TBD)
30 Short
60 One hour
360 Six hours
720 Twelve hours

Anything else is rejected. The session writes a Redis blob to active_location:{userID} with {interval_minutes, started_at, expires_at} and a row to active_location_sessions.

Two-signal model

Question Signal
"Is the user online right now?" presence:{userID} exists
"Is the user actively sharing live coordinates?" active_location:{userID} exists, OR active_location_sessions has an active row

Both can be true. Either can be true independently. Both are checked separately at the read sites (friend feed, radar, map overlay).

Privacy guardrails (composed with presence)

Setting Owner Effect
User.ChatPreferences.ActivityStatus The user If false, the user appears offline to everyone even when presence: key is fresh
users.is_location_shared The user If false, the user does not appear on others' maps even when sharing live coordinates
active_location_sessions.is_active Set by start/stop Restart-safe truth source

Invariants

Invariant Why
Heartbeat TTL is exactly 3 minutes Predictable offline detection
Only 30 / 60 / 360 / 720 are valid intervals Anti-arbitrary-input; UX has a fixed picker
One active session per user DeactivateAll runs before each new Create
Redis is hot path; Postgres is restart fallback GetActiveLocationStatus rehydrates Redis from DB on cache miss
ActivityStatus = false masks online indicator Honoured at every consumer read site
is_location_shared = false removes from others' maps Honoured at every consumer read site
Live-location coordinates are not persisted to Postgres Only Redis (user:location:{userID}, TTL 24 h)
Stay-duration preserved within 50 m UpdateLocation keeps arrived_at if the new coords are within 50 m of the previous reading

Lifecycle / state machine (prose)

Online lifecycle: (offline) → online → (offline). Online is entered on heartbeat; offline is entered implicitly when the 3-minute TTL on presence:{userID} expires without a refresh. There is no explicit offline transition.

Active-location lifecycle: (inactive) → active → (expired | stopped). Active is entered via POST /presence/active/start with a valid interval. Expired is entered automatically when the session window passes — both the Redis key and the row's is_active flag are dropped (the latter on next read or by the heartbeat fallback). Stopped is entered explicitly via POST /presence/active/stop, which deletes the Redis key and marks the DB row inactive immediately.

Cross-domain interactions

Other domain Reads
Friends GetFriendLocations reads presence:, active_location:, user:location: for each friend in one MGET
Discovery Friend overlay markers + radar use the same three keys
Chat Online indicator in the chat header reads presence:{userID} (gated by ActivityStatus)
Identity is_location_shared and activity_status are user-level settings

Privacy / visibility

Field Self Friend Stranger
Online status Always Yes (gated by ActivityStatus) No
Live coordinates Always Yes (gated by is_location_shared) No
Active-location session detail Always No (only the resulting coords) No
Heartbeat / endpoint surface Self only

See also