Skip to main content

Friendships

Overview

Friendships in CommonPlace are strictly mutual and bilateral. There are no followers, no asymmetric relationships, and no public follower counts. When two people become friends, it is because both explicitly agreed to the connection. This design upholds the platform's commitment to human-scale social contexts where relationships are intentional and reciprocal.

The friendship system supports the full lifecycle of a connection: sending a request, accepting or declining, canceling a pending request, and removing an existing friendship. All operations are rate-limited to prevent spam. The system also integrates with the "stepped back" feature, which allows users to reduce contact with a friend without fully removing them.

Multiple hooks provide different levels of access to friendship data throughout the application. The core useFriendship hook manages the relationship between the current user and a specific other user, while useFriends provides the full friends list with search and filtering capabilities used in messaging, circle invites, and other features.

Relevant Invariants

  • Invariant #7: "Human-Scale Social Contexts" -- Only mutual friendships exist. No followers, no asymmetric connections.
  • Invariant #6: "No Public Comparative Metrics" -- Friend counts are never displayed publicly.
  • Invariant #1: "Participation Is Always Voluntary" -- No guilt-tripping for declining requests. No "X is waiting for your response" nudges.

User Experience

User Flow

  1. Discovering a user: Navigate to their HomeRoom at /room/:username.
  2. Sending a request: Click the Friend Button on their profile. The button changes to "Request Sent".
  3. Receiving a request: The addressee sees "Accept" and "Decline" options.
  4. Accepting: Both users become friends. The button changes to "Friends".
  5. Declining: The request is silently declined. No notification to the requester.
  6. Canceling a pending request: The requester can cancel before the other user responds.
  7. Removing a friend: Either user can remove the friendship at any time.

Friend Button States

StateDisplayedAvailable Actions
No relationship"Add Friend"Send request
Pending (sent by current user)"Request Sent"Cancel request
Pending (received by current user)"Accept / Decline"Accept or decline
Accepted"Friends"Remove friend

Technical Implementation

Key Files

FilePurpose
src/hooks/useFriendship.jsCore hook for managing the friendship state between the current user and one specific other user (293 lines)
src/hooks/useFriends.jsHooks for fetching friends lists: useFriends, useFriendsWithSearch, useFriendsExcluding, useFriendSelection (210 lines)
src/components/friends/FriendButton.jsxUI component that renders the appropriate button state
src/components/friends/BlockedUsersList.jsxDisplays blocked users list
src/components/friends/FriendSearchInput.jsxSearch input for filtering friends by name/username

useFriendship(otherUserId) Hook

This hook manages the full lifecycle for a relationship between the current user and one other user.

Returned State:

  • friendship -- Raw friendship record (or null)
  • friendshipStatus -- 'pending', 'accepted', or null
  • isRequester / isAddressee -- Role in the relationship
  • isFriend -- Boolean shortcut for status === 'accepted'
  • isPendingSent / isPendingReceived -- Directional pending checks
  • hasNoRelationship -- Boolean for no existing connection

Returned Actions:

  • sendRequest() -- Creates a pending friendship (or reactivates a canceled one). Rate-limited.
  • cancelRequest() -- Sets a pending request to 'canceled' (requester only).
  • acceptRequest() -- Sets a pending request to 'accepted' (addressee only).
  • declineRequest() -- Sets a pending request to 'declined' (addressee only).
  • removeFriend() -- Deletes the friendship record (either user).

useFriends(): Fetches all accepted friendships for the current user, normalizing each record to extract the "other person" with their profile data (display_name, username, avatar_url, bio).

useFriendsWithSearch(): Wraps useFriends with client-side search filtering by display_name and username.

useFriendsExcluding(excludeIds): Filters out specific user IDs from the friends list. Used for "invite more friends" flows where some are already included.

useFriendSelection({multiSelect, maxSelections, initialSelection}): Manages selection state for friend picker UIs with toggle, select all, and clear capabilities.

Database Tables

TablePurpose
friendshipsStores friendship records with requester_id, addressee_id, status (pending/accepted/declined/canceled), and responded_at

Rate Limiting

Friend requests are rate-limited via the useRateLimit hook using the RATE_LIMIT_ACTIONS.FRIEND_REQUEST action. Both client-side and server-side checks are performed. If a rate limit is hit, a descriptive error message is returned.

Edge Cases

ScenarioBehavior
User tries to friend themselvesHook returns early (no fetch) when currentUser.id === otherUserId
Canceled request re-sentThe existing canceled record is reactivated to 'pending' rather than creating a duplicate
Friendship query finds declined/canceled recordsThese statuses are excluded via .not('status', 'in', '("declined","canceled")')
Blocked user sends friend requestBlock checks happen at a different layer (useBlock hook); the friendship system itself does not enforce blocks
Rate limit exceededsendRequest() returns { error, rateLimited: true } and the error is displayed in the UI
  • Circles -- Only friends can be invited to circles
  • Messaging -- Conversations are typically between friends
  • Feed -- "Friends Only" filter uses the friends list
  • HomeRoom -- Friend button appears on user profiles

Last updated: 2026-02-07