Groups, chat & notifications
Description
A group is the unit of collaboration: a team, optionally bound to a GitHub repository. Everything else in Ko-Lab hangs off one. Groups carry members with roles, a chat channel, workspace boards, and — once linked — a repository with per-member code access.
Alongside group chat there are 1:1 direct messages, and a notification centre that collects membership events, change-request outcomes and missed calls.
Setup
Needs only the database and a working WebSocket connection:
DATABASE_URL=postgresql://...
WS_AUTH_SECRET=<shared with apps/web-socket>
NEXT_PUBLIC_WEB_SOCKET_URL=ws://localhost:8080/ws
WS_ALLOWED_ORIGINS=http://localhost:3000
Email invites additionally need SMTP, but link invites work without it.
Integration
Frontend Backend WebSocket
──────── ─────── ─────────
/groups ─► GET /api/my-groups
/create-group ─► POST /api/create-group-data
/group/[groupId] ─► GET /api/groups/[id]
│ │
├─ send message ────► POST /api/save-group-message │
│ (durability) │
└─ send message ─────────────────────────────────► │ {type:"message"}
│ broadcast
/notifications ─► GET /api/notifications
Note the dual path for a message: HTTP for persistence, WebSocket for immediacy. Both must succeed.
Data model
| Model | Purpose |
|---|---|
Group | Name, owner, optional githubRepo |
GroupMember | Membership, role, codeAccess |
GroupMessage | Group chat history |
DirectMessage | 1:1 messages |
Notifications | In-app notification feed |
InviteLink | Tokenised join links |
How it works
Messages take two paths
Sending does both at once, which is why a message can appear live and then vanish on reload if the HTTP write failed:
// Persist
await fetch("/api/save-group-message", {
method: "POST",
body: JSON.stringify({ groupId, content, senderId }),
});
// Broadcast
send({ type: "message", groupId, content, senderId, timestamp: Date.now() });
When debugging "disappearing messages", check the network tab for a failed
save-group-message — the WebSocket half almost always worked.
Membership events become chat messages
apps/web/app/lib/memberEvents.ts and systemMessages.ts turn joins, leaves
and role changes into system messages in the channel, so the chat doubles as
an audit trail. __tests__/api/memberEvents.test.ts covers this.
Guest mode
POST /api/guest-mode grants limited access without a full account — useful
for showing a board to someone outside the team. Guests cannot commit code.
Testing
Automated
| File | Covers |
|---|---|
__tests__/api/memberEvents.test.ts | Membership → system messages |
__tests__/api/notifications.test.ts | Notification list |
Group creation, membership and invite flows have no tests. A worthwhile first addition:
describe("POST /api/add-group-member", () => {
it("rejects a caller who is not the group owner", async () => {
// owner is u1; u2 attempts to add u3
const res = await POST(
new Request("http://localhost/api/add-group-member", {
method: "POST",
body: JSON.stringify({ groupId: "g1", userId: "u3", callerId: "u2" }),
}),
);
expect(res.status).toBe(403);
});
});
Manual
Two browser profiles, signed in as different users.
Group lifecycle
- A creates a group → appears in
/groups - A invites B by link → B opens
/join/<token>and lands in the group - B appears in
/viewMembers/<groupId>for A - A system message announces the join in chat
- A removes B → B loses access immediately
Chat
- Both open the group
- A sends → appears for B in ~1s with no refresh
- Delivery receipt updates
- Kill the WS server → offline indicator appears
- Send while offline → queued or clearly failed, never silently lost
- Restart the server → reconnects and backfills
- Reload both → history matches
Direct messages
- A messages B from
/chat - B sees an unread badge
- Opening the thread clears it
- Verify
GET /api/direct-message/unreadmatches the badge
Notifications
- Trigger a membership change, a rejected CR and a missed call
- Each appears in
/notifications - Marking read persists across reload
Edge cases worth checking
- A user in 10+ groups — does the group list stay responsive?
- A very long message — the WS cap is 8 KB
- Rapid sending — the rate limit is 10/s and will close the socket
- Two tabs as the same user — both should receive
Demo
:::info Video coming soon A walkthrough of group creation, invites and live chat. :::