Skip to content

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:

  1. One table, three flavours. All rooms live in chat_rooms. Event rooms carry event_id and type='group'. Direct rooms are type='direct'. Freeform groups are type='group' with no event_id.
  2. Disappearing is per-room, forward-only. Toggling the disappearing_messages setting affects future messages only; historic messages without an expires_at are never retroactively expired.
  3. 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