Message lifecycle

The message FSM is per-message, not per-conversation. Every messages row carries its own status; the conversation row aggregates unread counts but has no lifecycle of its own.

LegendLiveCompleted

States

StatusWhen
sentOptimistic state on the client (tempId rendered before server roundtrip)
deliveredSaved to DB; Socket.io broadcast emitted; chat list updated
seenRecipient opened the conversation and the read receipt flipped the row

Why per-message

Group chats and one-to-one chats share the same model. Per-message status keeps "you have unread messages" math simple — count WHERE conversationId = X AND status != "seen" AND senderId != currentUserId. There's no useful "conversation state" beyond what the participants are doing right now.

Not exposed in god view

The trace UI doesn't have a message lane. Messages are noisy (one event per outbound + one per read receipt) and they aren't part of any commerce flow's critical path. Support debugging chat issues goes through /chat and /chat/reports, not through the trace.

On this page