Friends API
Manage friend requests and friendships. Friendships are bidirectional — once accepted, both parties can see each other for purposes of event co-authoring and like-group formation.
See also: Friendship algorithm,
friendships table.
All endpoints require Authorization: Bearer <supabase-access-token>.
The acting user is resolved from the JWT (no userId query param).
Status
| Endpoint | Status |
|---|---|
GET /friends | ✅ Shipped |
POST /friends/requests | ✅ Shipped |
POST /friends/requests/:id/accept | ✅ Shipped |
POST /friends/requests/:id/reject | ✅ Shipped |
POST /friends/:userId/block | ✅ Shipped |
DELETE /friends/:userId/block | ✅ Shipped |
DELETE /friends/:userId | ✅ Shipped |
Source:
src/feats/friends/.
Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /friends | List friendships of the current user |
| POST | /friends/requests | Send a friend request |
| POST | /friends/requests/:id/accept | Accept a request |
| POST | /friends/requests/:id/reject | Reject a request |
| POST | /friends/:userId/block | Block another user |
| DELETE | /friends/:userId/block | Unblock a user |
| DELETE | /friends/:userId | Unfriend |
List friends
GET /friends?status=accepted
Authorization: Bearer <token>
| Query | Type | Description |
|---|---|---|
status | enum | pending | accepted | rejected | blocked |
cursor | string | Pagination cursor |
limit | number | Page size (default 20) |
{
"data": [
{
"id": "uuid",
"requesterId": "uuid",
"addresseeId": "uuid",
"status": "accepted",
"createdAt": "2026-04-01T00:00:00Z"
}
],
"nextCursor": null
}
Send friend request
POST /friends/requests
Authorization: Bearer <token>
Content-Type: application/json
{ "addresseeId": "uuid" }
| Status | Reason |
|---|---|
201 | Created |
400 | Cannot request yourself |
409 | Already friends, already pending, or you are blocked |
Accept / reject
POST /friends/requests/:id/accept
POST /friends/requests/:id/reject
Only the addressee of a pending row may call these. Returns the
updated friendship.
Block
POST /friends/:userId/block
Blocking is a unilateral action: it sets the row to blocked regardless
of who initiated it, removes any existing pending state, and prevents
future requests from the blocked user.
The response includes a blockedBy field indicating who initiated the block.
Unblock
DELETE /friends/:userId/block
Only the user who initiated the block can unblock. Unblocking deletes the friendship row entirely, allowing either party to send a new friend request.
| Status | Reason |
|---|---|
200 | Success |
403 | Only the blocker can unblock |
404 | Block not found |
Unfriend
DELETE /friends/:userId
Removes the row entirely. Either side can re-request later.
Limits
| Limit | Value |
|---|---|
| Pending outgoing requests per user | 50 |
| Friends per user | No limit |
Implementation
Source:
src/feats/friends/.
src/feats/friends/
├── friends.router.ts # POST/GET/DELETE handlers, auth + zod middleware
├── friends.service.ts # Business rules: bidirectional check, block, etc.
├── friends.schemas.ts # Zod request/response schemas
└── friends.constants.ts # Status enum, limits
Key service functions
| Function | Description |
|---|---|
areFriends(a, b) | Check if two users are friends (status = accepted) |
getFriendship(a, b) | Get the friendship row between two users |
listFriendships(userId, query) | List with status filter and cursor pagination |
sendFriendRequest(from, to) | Create pending request |
acceptFriendRequest(id, userId) | Accept (addressee only) |
rejectFriendRequest(id, userId) | Reject (addressee only) |
blockUser(blockerId, blockedId) | Block a user |
unblockUser(userId, blockedId) | Unblock (blocker only) |
unfriend(userId, otherId) | Remove friendship |
Cross-user reads run as service_role (bypass RLS) and are scoped in
code by requesterId = auth.uid() OR addresseeId = auth.uid().