Chat — Domain Spec¶
Chat is one storage shape (chat_rooms) hosting three room flavours
— event, direct, group — with disappearing-messages, reactions, edit,
delete, and search. This page is the rules for room types, expiry,
visibility, and how chat interacts with events.
Mental model¶
Three shapes:
- One table, three flavours. All rooms live in
chat_rooms. Event rooms carryevent_idandtype='group'. Direct rooms aretype='direct'. Freeform groups aretype='group'with noevent_id. - Disappearing is per-room, forward-only. Toggling the
disappearing_messagessetting affects future messages only; historic messages without anexpires_atare never retroactively expired. - Two cleanup paths. A 30-second worker hard-deletes individual
expired messages and broadcasts
messages_expired. A 24-hour worker bulk-purges messages from archived event chat rooms. Friend chats are never bulk-purged.
Room types¶
| Type | chat_rooms.type |
event_id |
Created by |
|---|---|---|---|
| Event chat | group |
set | Auto on CreateEvent (host added as admin) |
| Direct message | direct |
null | GET /dm/room/:userId lazily creates |
| Freeform group | group |
null | POST /dm/group from a member list |
WebSocket subscribers can pass either an event ID or a chat-room
UUID; CheckParticipation resolves both.
Disappearing messages¶
| Setting | Behaviour |
|---|---|
off |
No expiry; messages persist |
seen |
Expires after the recipient's last_read_at advances past it (managed via seen_expires_at per participant) |
24h |
Stamps expires_at = created_at + 24h |
7d |
Stamps expires_at = created_at + 7d |
Per-room setting controls future messages only. disappearing_since
records when the setting was last enabled.
Reactions¶
| Rule | Notes |
|---|---|
| One emoji per user per message | Uniqueness on (message_id, user_id, emoji) |
| Toggle on/off | Same-emoji second tap removes the row |
| Multiple users may use the same emoji | Aggregated counts per emoji |
| Reaction set | (TBD product policy — currently any unicode emoji) |
Invariants¶
| Invariant | Why |
|---|---|
| Every event has exactly one chat room | Created in EventService.CreateEvent |
| Disappearing is forward-only | No retroactive expiry of historic messages |
| Friend chats are never purged by the event-retention worker | MessagePurgerService only touches rooms with event_id set |
| User hard delete anonymises group-room messages, deletes direct rooms | Conversation history preserved in groups; nothing to preserve in 1:1 |
| WebSocket auth uses the same JWT | Passed via ?token= query parameter on upgrade |
chat_room_id echoed only to joined participants |
Enforced at the event-detail read site |
Edit and delete are soft (re-broadcast as message_updated / message_deleted) |
Clients update in place |
Lifecycle / state machine (prose)¶
A message goes through created → (edited)* → (deleted | expired).
Edit and delete are soft (the row stays, edited_at or deleted_at
is stamped) and re-broadcast over WebSocket so clients update in
place. Expiry is the hard-delete path: the 30-second message_expiry
scheduler finds rows with expires_at < now, hard-deletes them, and
broadcasts a messages_expired envelope with the message IDs so
clients can drop them from their local lists.
An event room's lifecycle tracks the parent event: created with the
event, lives for the duration of all event phases, and is bulk-purged
by MessagePurgerService once the event has archived past its
retention window. The room row itself can stick around (so the
event.chat_room_id reference still resolves) — only the messages
are purged.
Cross-domain interactions¶
| Other domain | Interaction |
|---|---|
| Events | Event creation creates a room; event archival triggers message purge |
| Friends | DM creation typically requires accepted friendship |
| Identity | On user hard delete, group-room messages are anonymised, direct rooms hard-deleted |
| Discovery | (none direct) — discovery reads event metadata, not chat content |
Privacy / visibility¶
| Field on room | Direct (1:1) | Group | Event |
|---|---|---|---|
| Member list | Both participants | Members | Approved participants + host |
| Message history | Both participants | Members | Approved participants + host |
chat_room_id discovery |
Friend graph | Member invite | Via approved event participation |
| Disappearing setting | Either side can change | Admins only (TBD) | Host only (TBD) |
| Avatar / name | Pulled from the other user | Set by creator/admin | Mirrored from event title |
See also¶
- Narrative: Bible → Chat
- Backend: Backend → Chat service
- System view: Architecture → Real-time
- UX:
../ux/chat.md(coming soon)