Skip to content

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_status and is_location_shared are 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_at timestamp 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