Friendship Logic
Planned module
The friends module is not yet shipped. The model below is the
final design; implementation will live at src/feats/friends/ on the
new Hono + Supabase + Drizzle stack.
Inbloom has a bidirectional friendship system with specific rules.
Overview
Friendships in Inbloom are bidirectional — when users become friends, both parties are linked.
State Machine
States
| State | Can See Profile | Can Invite to Event | Can Add to Group |
|---|---|---|---|
none | ❌ No | ❌ No | ❌ No |
pending | Limited | ❌ No | ❌ No |
accepted | ✅ Full | ✅ Yes | ✅ Yes |
rejected | ❌ No | ❌ No | ❌ No |
blocked | ❌ No | ❌ No | ❌ No |
Rules
Key Constraints
-
Only friends can:
- Be invited as event authors
- Form groups together
- Add each other to groups
-
Request constraints:
- Cannot send multiple pending requests
- Cannot request already friends
- Cannot request blocked users
-
Bidirectional nature:
-- Query friends of userSELECT * FROM friendshipsWHERE (requester_id = :userId OR addressee_id = :userId)AND status = 'accepted';
Database Schema
The friendships table references auth.users.id (Supabase) directly
— we do not duplicate user identity into public. See the
Users model.
CREATE TABLE friendships (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
requester_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
addressee_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','accepted','rejected','blocked')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (requester_id <> addressee_id)
);
-- Order-independent unique pair so (A,B) and (B,A) collide.
CREATE UNIQUE INDEX friendships_pair_uniq
ON friendships (
LEAST(requester_id, addressee_id),
GREATEST(requester_id, addressee_id)
);
CREATE INDEX friendships_status_idx ON friendships (status);
CREATE INDEX friendships_requester_idx ON friendships (requester_id);
CREATE INDEX friendships_addressee_idx ON friendships (addressee_id);
Implementation
// src/feats/friends/friends.service.ts (planned)
import { and, eq, or, sql } from "drizzle-orm";
import { db } from "@infra/db/drizzle.client";
import { friendshipsTable } from "@infra/db/schema/friends.schema";
export async function areFriends(a: string, b: string): Promise<boolean> {
const row = await db
.select({ ok: sql<number>`1` })
.from(friendshipsTable)
.where(
and(
eq(friendshipsTable.status, "accepted"),
or(
and(eq(friendshipsTable.requesterId, a), eq(friendshipsTable.addresseeId, b)),
and(eq(friendshipsTable.requesterId, b), eq(friendshipsTable.addresseeId, a)),
),
),
)
.limit(1);
return row.length > 0;
}
export async function canInviteAsAuthor(inviterId: string, inviteeId: string) {
if (!(await areFriends(inviterId, inviteeId))) return false;
// Additional event-specific checks happen at the events.service layer.
return true;
}
A SECURITY DEFINER helper is also exposed in Postgres so RLS policies on other tables can ask "are these two users friends?" cheaply:
CREATE FUNCTION public.are_friends(a uuid, b uuid)
RETURNS boolean
LANGUAGE sql
SECURITY DEFINER
STABLE
AS $$
SELECT EXISTS (
SELECT 1 FROM public.friendships
WHERE status = 'accepted'
AND ((requester_id = a AND addressee_id = b)
OR (requester_id = b AND addressee_id = a))
);
$$;
Friendship Use Cases
1. Inviting Event Authors
Creator ──friend──> User A ──friend──> User B
↓
Invited as author
↓
User A can approve groups
2. Forming Like Groups
User A ──friend──> User B
↓
Can form group
↓
Add friends to group
↓
max 10 members (including creator)
3. Creating Groups with Non-Friends
❌ NOT ALLOWED
Group members must ALL be friends with:
- Group creator
- Each other (implied by friend with creator)
Graph Relationships
Friendship can be queried as a graph:
-- Find all friends of friends (2nd degree)
WITH direct_friends AS (
SELECT CASE
WHEN requester_id = :userId THEN addressee_id
ELSE requester_id
END as friend_id
FROM friendships
WHERE (requester_id = :userId OR addressee_id = :userId)
AND status = 'accepted'
)
SELECT DISTINCT f2.friend_id
FROM direct_friends f1
JOIN friendships f2 ON (
f2.requester_id = f1.friend_id OR
f2.addressee_id = f1.friend_id
)
WHERE f2.status = 'accepted'
AND f2.friend_id != :userId;
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /friends | List friendships |
| POST | /friends/request | Send friend request |
| POST | /friends/:id/accept | Accept request |
| POST | /friends/:id/reject | Reject request |
| POST | /friends/:id/block | Block user |
Query Parameters
GET /friends?status=pending|accepted|rejected
Limits
| Limit | Value |
|---|---|
| Pending requests per user | 50 |
| Friends per user | No limit |