Skip to main content

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

StateCan See ProfileCan Invite to EventCan Add to Group
none❌ No❌ No❌ No
pendingLimited❌ No❌ No
accepted✅ Full✅ Yes✅ Yes
rejected❌ No❌ No❌ No
blocked❌ No❌ No❌ No

Rules

Key Constraints

  1. Only friends can:

    • Be invited as event authors
    • Form groups together
    • Add each other to groups
  2. Request constraints:

    • Cannot send multiple pending requests
    • Cannot request already friends
    • Cannot request blocked users
  3. Bidirectional nature:

    -- Query friends of user
    SELECT * FROM friendships
    WHERE (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

MethodEndpointDescription
GET/friendsList friendships
POST/friends/requestSend friend request
POST/friends/:id/acceptAccept request
POST/friends/:id/rejectReject request
POST/friends/:id/blockBlock user

Query Parameters

GET /friends?status=pending|accepted|rejected

Limits

LimitValue
Pending requests per user50
Friends per userNo limit