Presence¶
Presence is the two signals that make a location-aware product feel live: is the user online right now, and is the user actively sharing their location? Both are intentionally tiny, both are Redis-first, and both are designed so the user never has to think about turning them off.
The story¶
A friends-on-a-map product fails the second its "online" indicator
lies. Either it lies in the green direction (we say a friend is online,
the friend isn't, the user pings them and gets nothing) or it lies in
the dim direction (we say a friend is offline, they're actually around,
the user doesn't bother and misses the meet-up). We picked a model that
fails to grey rather than to green: a short-TTL Redis key
(presence:{userID}) refreshed by a client heartbeat every couple of
minutes, with a 3-minute TTL. If the heartbeat stops, the user goes
offline within three minutes. There is no presence/stop endpoint —
going offline is implicit, by TTL expiry. The cost is that going
offline isn't instant; the benefit is that the signal is always at
most three minutes stale and we don't need a reliable disconnect
notification.
Live location is the second signal, and it's deliberately separate. Online means "the app is open and reachable". Live location means "I have explicitly opted in to share my coordinates with my friends, for a finite window." The user picks a window — 30, 60, 360, or 720 minutes — and a typed session begins. The session's coordinates write into Redis on every update; the session's metadata is written to a Postgres row as well so a restart doesn't silently end the session. When the window expires (or the user stops it), the keys are dropped and the row is marked inactive.
The reason for the two-signal model is that the two questions are asked at different times. "Should I show this friend's avatar in the header of our chat?" is the online question. "Should I show this friend as a pin on the map?" is the live-location question. A user can be online without sharing location, or they can be offline-but-recent with their live-location session still ticking. Mixing the two would make the wrong thing visible on at least one of those surfaces.
Two privacy guardrails sit on top. The user's own activity_status
chat preference can make them appear offline to everyone even when
their presence key is fresh — a "show I'm online" master switch. And
the friend's own is_location_shared user-level toggle controls
whether they appear on others' maps at all. Both are checked on every
read, on every surface.
What we're optimising for¶
- Two distinct signals. Online ≠ sharing live location.
- Redis-first, Postgres as fallback. Presence is hot; persistence is for restart safety.
- Implicit offline. No explicit disconnect; the TTL handles it.
- Typed live-location windows. 30 / 60 / 360 / 720 minutes — a deliberate finite set, not a free text input.
- Privacy is layered.
activity_statusandis_location_sharedare both honoured at every read site. - Stay-duration preserved within 50 m. Live location updates
within 50 m of the previous reading preserve the
arrived_attimestamp so the UI can show "been here X minutes".
What presence is not¶
- It is not "last seen". We don't surface a "last seen 3 hours ago" timestamp on profiles; we surface "online" or "offline" and that's it.
- It is not background-only location. The user has to be in the app (or have an active-location session running) to broadcast.
- It is not a check-in. Tomoda does not have a "I'm at X" button separate from the map. Live location is the check-in.
See also¶
- Domain spec: Domains → Presence
- Backend implementation: Backend → Presence service
- Where presence is read from: Bible → Friends, Bible → Discovery
- UX for the activity-status toggle, location-share start/stop:
../ux/presence.md(coming soon)