Chat¶
Chat is how plans become real. Every event has a chat room. Every friendship can have a DM. Small groups can spin up freeform rooms for one purpose and let them disappear. Messaging is built around the specific kind of conversation a location-aware social product needs — short-lived, plan-shaped, sometimes literally disappearing.
The story¶
The first decision we made about chat is that there is no inbox.
Conversations live inside the things they're about — an event chat is
part of the event, a DM is with one specific friend, a group chat
is the room you set up for the dinner. The room types share a single
storage shape (one chat_rooms table, three flavours) but the user
never confuses an event-room for a DM, because they're surfaced where
the user expects each of them: an event-room inside the event, a DM
inside the connect tab, a group inside its own list.
The second decision is disappearing messages, first-class. Every
room — DM, group, event — has a disappearing_messages setting with
four values: off, seen, 24h, and 7d. Toggling the setting
affects future messages only; historic messages don't retroactively
expire. A background worker ticks every 30 seconds, hard-deletes the
expired messages, and broadcasts a messages_expired event so the
clients can drop the rows from their lists. It's not a privacy theatre
feature — the messages are actually gone. We built it because a
location-aware product holds enough sensitive data that every
conversation should be allowed to be temporary by design.
The third decision is anonymisation on user delete. When a user
hard-deletes their account, their messages in group rooms have
user_id nulled out so the rest of the conversation still makes sense
to the remaining members. Direct (1:1) rooms are deleted wholesale —
the other person has nothing to preserve. The deleted user shows up in
the frontend as "Deleted user" with no avatar and no link to a profile.
The point is that leaving Tomoda doesn't break the room for the people
you left behind.
Layered on top: replies with inline preview, reactions (one emoji per user per message, toggle on/off), edit and soft-delete, mark-read with unread counts, image attachments, search within a room and across every room the user is in, group avatars and nicknames, mute, and the admin / member role for group rooms. The shape is conventional; what sets it apart is that conversations are scoped to the plan they support, and they're built to expire when the plan does.
What we're optimising for¶
- Conversations live where they belong. Event chat inside the event. DMs inside the connect tab. Group rooms in their own list.
- Disappearing is first-class. Every room can disappear; it's not a feature buried in settings.
- Ephemerality applies to plans too. Messages on an event chat room are purged once the event archives — the conversation served the plan, not the archive.
- Friend chats are permanent. The retention purger never touches DM history; only the event-room purger does.
- Anonymisation over deletion in shared rooms. Group chats survive a member leaving the product.
- WebSocket-first, HTTP fallback. The realtime channel is the primary surface; the HTTP send endpoint is there for clients that can't hold a socket open.
What chat is not¶
- It is not an inbox-first messenger. You enter conversations from the thing they're attached to — the event, the friend, the group.
- It is not threaded (in the Slack sense). Replies inline; no nested threads.
- It is not a public broadcast surface. There is no "channel" or "server".
- It is not voice or video. Today it's text + images + reactions.
See also¶
- Domain spec: Domains → Chat
- Backend implementation: Backend → Chat service
- The realtime topology: Architecture → Real-time
- How the event room is created with an event: Bible → Events
- UX for chat list, room, settings:
../ux/chat.md(coming soon)