Skip to content

Friends — Domain Spec

A friendship is a single undirected row between two users with a status. This page is the rules for request flow, mutual-graph invariants, blocks, and how the graph gates location sharing.

Mental model

Three shapes:

  1. One row per pair. Friendship is modelled as a single row in friendships with user_id_1 (requester) and user_id_2 (recipient). Ordering matters only for "pending sent" vs "pending received" UI.
  2. Mutual, not asymmetric. A request needs the recipient's accept before the edge counts. There is no asymmetric follow.
  3. The graph gates everything. Live location, friends-only visibility on moments / events, mutual-friend counts on profiles all read from the same friendships table.

Friendship statuses

Status Meaning Set by
pending Request sent, not yet accepted Sender via POST /friends/request
accepted Both parties are friends Recipient via POST /friends/accept
blocked Recipient blocked the sender (one-way restriction) Block action (TBD UI surface)

Request flow

From state Action To state Caller
(none) POST /friends/request pending (requester = user_id_1) Sender
pending POST /friends/accept accepted Recipient
pending POST /friends/reject (row deleted) Recipient or sender (cancel)
accepted DELETE /friends/:id (row deleted) Either side
accepted Block blocked Either side

POST /friends/accept takes the other user's UUID, not the friendship row ID, for historical reasons. The service resolves the row via FindBetweenUsers(userID, friendID) and confirms UserID2 == userID before flipping status. POST /friends/reject and DELETE /friends/:id follow the same pattern.

Invariants

Invariant Why
One row per pair No duplicate friendship rows
accepted is symmetric Both sides see the edge; both sides can unfriend
blocked is one-way at the data layer but enforced symmetrically at the read layer Blocked users are invisible to the blocker on every read site
Friend list is cached in Redis user:friends:{userID}, 10-min TTL, invalidated on accept / unfriend
Mutual-friends count is computed in Discovery Not in FriendService — the friend service exposes only the raw graph

Location-share rules

Setting Owner Controls
users.is_location_shared The friend Whether others see this user on the map
Heartbeat (presence:{userID}, TTL 3 min) The friend's client Whether the user appears online
Active-location session The friend Whether the user has opted in to broadcast coordinates for a finite window (see Presence)

Privacy on the read site is one-way: the caller's own is_location_shared flag controls whether others see the caller. It does not restrict what the caller can see — GetFriendLocations returns visible friend locations regardless of the caller's own sharing setting. Only friends whose own is_location_shared is true appear in the result.

Block semantics

Surface Behaviour when blocked
Map / radar Blocked user does not appear
Search results Blocked user does not appear
Profile view Blocked user's profile is not loadable
Mutual-friend counts Blocked edges do not contribute
Chat Existing DMs become inaccessible; new DM cannot be created
Events Blocked user cannot see friends-visibility events of the blocker

Lifecycle / state machine (prose)

Friendship lifecycle: (none) → pending → accepted → (deleted) with blocked as an alternative branch from accepted. A pending row goes either to accepted (recipient accepts) or to (deleted) (reject or cancel). An accepted row goes either to (deleted) (unfriend) or blocked (block). A blocked row stays blocked until explicitly unblocked, which deletes the row.

No location data is persisted in Postgres. Live location lives entirely in Redis at user:location:{userID} with a 24-hour TTL.

Cross-domain interactions

Other domain Interaction
Presence GetFriendLocations reads user:location:, presence:, active_location: for each friend in a single MGET
Discovery Reads the graph to compute mutual-friend counts, friend overlay markers, radar friend results
Events friends-visibility events check accepted friendship with host
Moments friends_only visibility checks accepted friendship with poster
Chat DM creation typically requires accepted friendship (TBD policy)
Identity On user hard delete, friendship rows are removed

Privacy / visibility

Field on profile Public Friend
Display name + username + avatar Yes Yes
Bio / profile fields Yes Yes
Friend list count Yes Yes
Mutual-friends count Yes (auth-gated) Yes
Live location No Yes (if friend's is_location_shared is on)
Friends-only moments No Yes
Friends-visibility events No Yes (if host is friend)

See also