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
- Discovering a user: Navigate to their HomeRoom at
/room/:username. - Sending a request: Click the Friend Button on their profile. The button changes to "Request Sent".
- Receiving a request: The addressee sees "Accept" and "Decline" options.
- Accepting: Both users become friends. The button changes to "Friends".
- Declining: The request is silently declined. No notification to the requester.
- Canceling a pending request: The requester can cancel before the other user responds.
- Removing a friend: Either user can remove the friendship at any time.
Friend Button States
| State | Displayed | Available 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
| File | Purpose |
|---|---|
src/hooks/useFriendship.js | Core hook for managing the friendship state between the current user and one specific other user (293 lines) |
src/hooks/useFriends.js | Hooks for fetching friends lists: useFriends, useFriendsWithSearch, useFriendsExcluding, useFriendSelection (210 lines) |
src/components/friends/FriendButton.jsx | UI component that renders the appropriate button state |
src/components/friends/BlockedUsersList.jsx | Displays blocked users list |
src/components/friends/FriendSearchInput.jsx | Search 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 nullisRequester/isAddressee-- Role in the relationshipisFriend-- Boolean shortcut forstatus === 'accepted'isPendingSent/isPendingReceived-- Directional pending checkshasNoRelationship-- 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() and Related Hooks
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
| Table | Purpose |
|---|---|
friendships | Stores 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
| Scenario | Behavior |
|---|---|
| User tries to friend themselves | Hook returns early (no fetch) when currentUser.id === otherUserId |
| Canceled request re-sent | The existing canceled record is reactivated to 'pending' rather than creating a duplicate |
| Friendship query finds declined/canceled records | These statuses are excluded via .not('status', 'in', '("declined","canceled")') |
| Blocked user sends friend request | Block checks happen at a different layer (useBlock hook); the friendship system itself does not enforce blocks |
| Rate limit exceeded | sendRequest() returns { error, rateLimited: true } and the error is displayed in the UI |
Related
- 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