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.
No diagram code provided
LegendLiveCompleted
States
| Status | When |
|---|---|
sent | Optimistic state on the client (tempId rendered before server roundtrip) |
delivered | Saved to DB; Socket.io broadcast emitted; chat list updated |
seen | Recipient 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.