Approval Algorithm
The like-group / approval module is not yet shipped on the new stack. The algorithm itself is finalized; the implementation snippets below show how it will be expressed in Hono + Drizzle.
The approval algorithm determines when a like group is approved to access an event's private chat.
Overview
A = Number of event authors
G = Number of group members
Group is approved when: approvals >= min(A, G)
The Formula
Key Insight: Each group member needs at least one author approval, but we use the minimum of authors and members to avoid requiring more approvals than members exist.
Approval Threshold
approval_threshold = min(authors_count, group_members_count)
Required Approvals
approved = (approvals_received >= approval_threshold)
Examples
| Authors (A) | Members (G) | Threshold | Required Approvals |
|---|---|---|---|
| 1 | 1 | min(1,1)=1 | 1 |
| 3 | 1 | min(3,1)=1 | 1 |
| 3 | 3 | min(3,3)=3 | 3 |
| 2 | 3 | min(2,3)=2 | 2 |
| 5 | 10 | min(5,10)=5 | 5 |
State Machine
State Values
| State | Description | Chat Created |
|---|---|---|
pending | Awaiting author approval | ❌ No |
approved | Approved by enough authors | ✅ Yes |
rejected | Rejected by any author | ❌ No |
expired | Timed out (24h) | ❌ No |
Implementation
The check runs inside the Drizzle transaction that records the
approval. Pseudocode for src/feats/groups/groups.service.ts:
import { db } from "@infra/db/drizzle.client";
import {
likeGroupApprovalsTable,
likeGroupMembersTable,
likeGroupsTable,
eventAuthorsTable,
chatsTable,
chatParticipantsTable,
} from "@infra/db/schema";
export async function approveGroup(authorId: string, groupId: string) {
return db.transaction(async (tx) => {
// 1. Record the approval (idempotent on (group_id, author_id) unique).
await tx
.insert(likeGroupApprovalsTable)
.values({ groupId, authorId, approved: true })
.onConflictDoNothing();
// 2. Recompute counts under the same snapshot.
const [{ approvals, members, authors }] = await tx.execute(/* sql */ `
SELECT
(SELECT count(*) FROM like_group_approvals
WHERE group_id = ${groupId} AND approved = true)::int AS approvals,
(SELECT count(*) FROM like_group_members
WHERE group_id = ${groupId})::int AS members,
(SELECT count(*) FROM event_authors a
JOIN like_groups g ON g.event_id = a.event_id
WHERE g.id = ${groupId})::int AS authors
`);
const threshold = Math.min(authors, members);
if (approvals < threshold) return { approved: false } as const;
// 3. Create the chat + add participants.
const [chat] = await tx
.insert(chatsTable)
.values({ type: "approved_group" })
.returning();
const memberRows = await tx
.select({ userId: likeGroupMembersTable.userId })
.from(likeGroupMembersTable)
.where(/* group_id = groupId */);
const authorRows = await tx
.select({ userId: eventAuthorsTable.userId })
.from(eventAuthorsTable)
.innerJoin(/* join via like_groups.event_id */);
await tx.insert(chatParticipantsTable).values(
[...memberRows, ...authorRows].map((p) => ({
chatId: chat.id,
userId: p.userId,
})),
);
// 4. Flip group status.
await tx
.update(likeGroupsTable)
.set({ status: "approved", chatId: chat.id })
.where(/* id = groupId AND status = 'pending' */);
return { approved: true, chatId: chat.id } as const;
});
}
Notes:
-
The whole sequence is one Drizzle transaction so
pending → approvedand chat creation are atomic. -
onConflictDoNothing()makes the approval insert idempotent against the(group_id, author_id)unique index — see the model. -
Only the author-role check is done in code; we do not RLS-gate this because the API runs as
service_role. The check is:const isAuthor = await tx.select({ ok: sql<number>`1` }).from(eventAuthorsTable).innerJoin(/* like_groups → events */).where(/* event_id = group.event_id AND user_id = authorId */).limit(1);if (!isAuthor.length) throw new ForbiddenHTTPException();
Edge Cases
1. Group Shrinks Before Approval
If group creator removes members before approval:
- Recalculate threshold with new member count
- Previously submitted approvals still count
2. Author Removed
If event author is removed:
- Recalculate threshold
- Existing approvals may need recount
3. Multiple Authors Approve Same Member
Only one approval per author counts, even if they approve multiple members:
approvals >= thresholdnotunique_members_approved >= threshold
4. Concurrent Approvals
If multiple authors approve simultaneously:
- All approvals count
- First to reach threshold triggers chat creation
- Others are ignored but recorded for audit
Side Effects
When approved:
- Group status →
approved - Chat created → New
chatsrow of typeapproved_group - Users added → All group members + event authors as
chat_participants - Notification sent →
notificationsrow of typegroup_approved, fanned out to clients via Supabase Realtime
Security Considerations
- Only event authors can approve/reject
- Approval is per-group, not per-member
- Rejection is immediate (any author can reject)
- Once approved, cannot be undone
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /groups/:id/approve | Approve a group |
| POST | /groups/:id/reject | Reject a group |
| GET | /groups/:id/approvals | Get approval status |