Remaining docs updated to match code (the docs that the first pass had not covered):
- Flows: Chat, Referral, Rating, Registration, Google OAuth, Negotiation, Payout,
Trezor Safekeeping — corrected endpoints, socket events, status enums, auth gaps
- API Reference: User API, Trezor API — admin route prefix/verb/status corrections,
added undocumented endpoints (ton-proof challenge, profile email verify,
GET /trezor/account, POST /trezor/verify-operation)
- Data Models: Chat, Notification, Payment, PointTransaction, User — corrected
enums (PaymentProvider, escrowState, PointTransaction.type, User.status),
90-day notification TTL, soft-delete semantics, wallet fields
Trezor "zero frontend" finding (audit C31/C32) corrected as STALE:
- Verified current code HAS a full frontend Trezor implementation (admin/trezor
page, TrezorSettingsView, trezorConnector via @trezor/connect-web,
TrezorSignDialog, actions/trezor.ts building the {message,signature} object)
- Fixed Trezor Safekeeping Flow doc (removed false "no frontend" warnings)
- Reclassified ISSUE-012 as invalid/superseded with explanation
Issue set reconciled to a single canonical numbering (ISSUE-001..054):
- Adopted the comprehensive 51-issue set (long-slug, fully indexed)
- Removed 35 superseded short-slug duplicates from the first pass
- Removed a duplicate ISSUE-046 file
- Added 3 issues the 51-set lacked: ISSUE-052 (completed-not-counted-in-stats),
ISSUE-053 (axios 401-only interceptor), ISSUE-054 (rate limiter counts all attempts)
- Regenerated Issues Index: 53 open (14 critical, 39 major) + 1 invalid
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
18 KiB
title, tags, related_models, related_apis
| title | tags | related_models | related_apis | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Chat Flow |
|
|
|
Chat Flow
Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
Real-time messaging between buyer & seller (direct), three-way dispute mediation (group), and user-to-support (support). Backed by Chat documents with embedded messages[] and a Socket.IO room per chat for live updates.
Actors
- User A (initiator) — typically a buyer.
- User B (recipient) — typically a seller.
- Support agent — for
type: 'support'chats (user issupport@amn.gg). - Admin — added as a third participant in dispute chats.
- Frontend —
frontend/src/sections/chat/(chat list, conversation view, message composer). - Backend —
ChatService(backend/src/services/chat/ChatService.ts), routes under/api/chat. - MongoDB —
chatscollection with embeddedmessages,participants,unreadCounts,settings,metadata. - Socket.IO — events
new-message,chat-notification,messages-read,user-typing,user-status-change,message-deleted.
Preconditions
- Both parties authenticated.
- For
directchats tied to a purchase request, the request exists and both users are participants in it.
Chat lifecycle
stateDiagram-v2
[*] --> Created: ChatService.createChat\n(or auto on first contact)
Created --> Active: messages flowing
Active --> Active: send / read / typing
Active --> Archived: PATCH /api/chat/:id/archive (toggle)
Archived --> Active: PATCH /api/chat/:id/archive (same endpoint toggles back)
Active --> [*]: chat deleted (rare)
Step-by-step narrative
Creation
-
Direct chat (find-or-create) — when buyer clicks "Chat with seller" on a request detail, frontend POSTs
POST /api/chatwith{ type: 'direct', participantIds: [sellerId] }. The endpoint requires exactly 1 externalparticipantId; the authenticated caller is auto-appended to make 2.[!warning]
relatedTois NOT accepted onPOST /api/chatDespite the schema carrying arelatedTodiscriminator, the create endpoint ignores/does not accept arelatedTopayload. Purchase-request linkage is performed server-side via the dedicatedPOST /api/chat/purchase-request(see step 5), not by passingrelatedTotoPOST /api/chat. -
ChatService.createChat(ChatService.ts:90-192):- For
directwith exactly 2 participants, runsChat.findOne({ type: 'direct', 'participants.userId': { $all: [...] }, 'participants.isActive': true })and returns the existing chat if found. - Otherwise creates a new
Chatwithparticipants(each withrole:'member',joinedAt,isActive:true), zeroedunreadCounts, defaultsettings,metadata.createdBy. - Appends a system welcome message (
messageType: 'system'). - Re-loads with
populate('participants.userId', 'firstName lastName profile.avatar email')for the response.
- For
-
Group chat (dispute) — same pattern, but
type: 'group', all three participants (buyer, seller, admin) added (admin is added later byDisputeService.assignAdmin). -
Support chat —
ChatService.createSupportChat(userId)(:41-88) auto-discoversUser.findOne({ email: 'support@amn.gg' })and creates atype: 'support'chat with a welcome message. Idempotent. -
Post-payment / purchase-request auto-chat —
POST /api/chat/purchase-requestexists on the backend and creates/links a direct chat for a purchase request. When payment is confirmed, the payment-state cascade ensures a direct chat exists between buyer and winning seller. No frontend action is wired toPOST /api/chat/purchase-request— this direct chat is created server-side.
Joining the room (real-time)
-
On chat page mount, the frontend emits
socket.emit('join-chat-room', chatId)(backend/src/app.ts:130-133). The socket joins roomchat-{chatId}. -
join-user-roomanduser-onlineare SEPARATE events (do not conflate them):socket.emit('join-user-room', userId)makes the socket join the personaluser-{userId}room (so it can receivechat-notification).socket.emit('user-online', userId)broadcasts auser-status-change(online) to other clients.
[!warning] No offline broadcast on disconnect — stale "online" status On socket disconnect, no offline
user-status-changeis emitted. Other users keep seeing a stale "online" indicator for a peer who has actually left. Document this as a known gap.
Sending a message
- User types and hits send. Frontend POSTs
POST /api/chat/:id/messageswith{ content, messageType?, fileUrl?, fileName?, fileSize?, replyTo? }. Backend enforces a 5000-character maximum oncontentat both Mongoose schema and controller validation levels. ChatService.sendMessage(:195-260):- Loads chat, verifies the sender is in
participants[]andisActive. - Builds
Message:{ senderId, content, messageType: 'text', fileUrl?, fileName?, fileSize?, replyTo?, timestamp, isRead: false, isEdited: false }. chat.addMessage(messageData)— schema method that pushes the message, updatesmetadata.lastActivity, incrementsunreadCountsfor non-senders, and updates the cachedlastMessagesummary.- Persists.
- Emits
new-messagetochat-{chatId}(everyone in the room sees the message immediately). - Emits
chat-notificationto each non-sender'suser-{userId}room (drives the chat-list unread badge and the toast/notification bell if the user is not currently viewing the chat).
- Loads chat, verifies the sender is in
- Frontend reconciles its own message list (the sender either appends optimistically and then matches the server echo or waits for the round-trip).
Attachments
-
File upload endpoint: the real endpoint is
POST /api/chat/:id/messages/file(multipart/form-data). The flow previously referencedPOST /api/chat/:chatId/upload, which does NOT exist.[!bug] ⚠️ KNOWN BUG — file uploads broken The frontend
chatService.sendFileMessagecurrently POSTs to the text message endpoint (POST /api/chat/:id/messages) instead ofPOST /api/chat/:id/messages/file. As a result file uploads are broken — they hit the wrong endpoint. -
When working correctly, the backend handles the multipart payload at
POST /api/chat/:id/messages/file, persists the upload viafileService(returns{ fileUrl, fileName, fileSize }), and records the message withmessageType: 'image' | 'file'.[!warning] ⚠️ Security concern — anonymous file access Uploaded files are stored under
uploads/chat/and served with anonymous access. Sensitive attachments (KYC docs, dispute evidence) are fetchable by any user who has the URL. Consider signed URLs or per-user authorisation.
Editing a message
-
Editing a message uses a body of
{ content }(max 5000 chars). Edits are only allowed within a 15-minute edit window — edits attempted after that return 400.[!bug] ⚠️ KNOWN BUG — edits fail / are ignored The frontend
editMessageaction sends{ text }, but the backend expects{ content }. The mismatched field name means edits fail or are silently ignored.
Deleting a message (soft-delete)
- Message DELETE soft-deletes: it sets
deletedAt, clears the messagecontent, and emitsmessage-deletedtochat-{chatId}. The subdocument is not physically removed.
Read receipts
- When the user opens a chat, frontend marks messages read via
PATCH /api/chat/:id/messages/read(note: PATCH, not POST; there is noPOST /api/chat/:chatId/read). The body may carrymessageIds: string[]; ifmessageIdsis empty or omitted, ALL messages are marked read. ChatService.markMessagesAsRead(:438-483):- Calls
chat.markAsRead(userId, messageObjectIds)(schema method that flipsisReadon the relevant messages and zeros the user'sunreadCountsentry). - Emits
messages-readtochat-{chatId}so the sender sees the double-tick.
- Calls
Typing indicator
- On
inputevents, frontend emitssocket.emit('typing-start', { chatId, userId, userName }); on idle/blur emitstyping-stop. - Backend
app.ts:142-158relays tochat-{chatId}asuser-typing(excluding the sender). No DB persistence. Limited to 5 typing indicators per 10 seconds.
Participants (add / remove / role)
-
Add a participant — real endpoint
POST /api/chat/:id/participantsexpects a body of{ userId }(a single id).[!bug] ⚠️ KNOWN BUG — add participant payload mismatch The frontend
addParticipantsaction sends{ participants: string[] }(an array), but the backend expects{ userId }(a single id). The shapes do not match. -
Remove / leave — to remove a participant (or have a user leave), use
DELETE /api/chat/:id/participants/:participantId. Removal is a soft removal: the participant subdocument is kept withisActive=falseand aleftAttimestamp.[!bug] ⚠️ KNOWN BUG — leave action 404s
PUT /chat/:id/leavedoes NOT exist on the backend. The frontendleaveConversationaction targets that path and therefore 404s. UseDELETE /api/chat/:id/participants/:participantIdinstead. -
List participants —
[!bug] ⚠️ KNOWN BUG — getParticipants 404s
GET /chat/:id/participantsdoes NOT exist — the backend only exposesPOST(add) andDELETE(remove) on that path. The frontendgetParticipantsaction 404s. Participants must be read fromGET /api/chat/:id/infoinstead. -
Change a participant role —
[!bug] ⚠️ NOT IMPLEMENTED — updateParticipantRole
PUT /chat/:id/participants/:participantIddoes NOT exist on the backend. The frontendupdateParticipantRoleaction has no backend counterpart.
Chat info
getChatInfo→GET /api/chat/:id/inforeturns chat details plus only the first 50 messages (page 1, limit 50) — not the full message history. Use the paginatedGET /api/chat/:id/messagesto load older messages.
Sequence diagram
sequenceDiagram
autonumber
actor A as User A
actor B as User B
participant FE_A as Frontend A
participant FE_B as Frontend B
participant BE as Backend
participant DB as MongoDB
participant IO as Socket.IO
A->>FE_A: Open conversation
FE_A->>BE: POST /api/chat {type:direct, participantIds:[sellerId]}
BE->>DB: find-or-create Chat (caller auto-appended)
BE-->>FE_A: { chat }
FE_A->>IO: emit 'join-chat-room' chatId
FE_A->>IO: emit 'join-user-room' userId (separate from user-online)
FE_B->>IO: emit 'join-chat-room' chatId (when B opens too)
A->>FE_A: type & send
FE_A->>BE: POST /api/chat/{id}/messages {content} (max 5000 chars)
BE->>DB: chat.addMessage and update metadata.lastActivity to now
BE->>IO: emit chat-{id} 'new-message'
IO-->>FE_A: 'new-message' (echo)
IO-->>FE_B: 'new-message' (live)
BE->>IO: emit user-{B} 'chat-notification' (badge)
A->>FE_A: attach file
FE_A->>BE: POST /api/chat/{id}/messages/file (multipart/form-data)
BE->>DB: chat.addMessage with fileUrl/fileName/fileSize
BE->>IO: emit chat-{id} 'new-message'
B->>FE_B: opens chat
FE_B->>BE: PATCH /api/chat/{id}/messages/read (empty messageIds = all)
BE->>DB: chat.markAsRead(B)
BE->>IO: emit chat-{id} 'messages-read'
IO-->>FE_A: 'messages-read' (double-tick)
A->>IO: emit 'typing-start'
IO-->>FE_B: 'user-typing' {isTyping:true}
API calls
| Method | Endpoint | Purpose |
|---|---|---|
POST |
/api/chat |
Find-or-create chat (exactly 1 external participantId; caller auto-appended; relatedTo NOT accepted) |
GET |
/api/chat |
List user's chats |
GET |
/api/chat/:id/info |
Chat details + first 50 messages (page 1, limit 50) + participants |
GET |
/api/chat/:id/messages |
Paginated message history |
POST |
/api/chat/:id/messages |
Send text message |
POST |
/api/chat/:id/messages/file |
Send file attachment (multipart/form-data) |
PATCH |
/api/chat/:id/messages/read |
Mark read (empty/omitted messageIds marks ALL read) |
PUT |
/api/chat/:id/messages/:messageId |
Edit message — body { content }, 15-min edit window |
DELETE |
/api/chat/:id/messages/:messageId |
Soft-delete a message (deletedAt, content cleared, emits message-deleted) |
POST |
/api/chat/:id/participants |
Add a participant — body { userId } (single) |
DELETE |
/api/chat/:id/participants/:participantId |
Remove / leave (soft: isActive=false, leftAt) |
POST |
/api/chat/support |
Create/get support chat |
POST |
/api/chat/purchase-request |
Create/link direct chat for a purchase request (no frontend action wired) |
PATCH |
/api/chat/:id/archive |
Toggle archived state (archive and unarchive via same endpoint) |
[!bug] Frontend actions that target non-existent or mismatched backend endpoints
leaveConversation→PUT /chat/:id/leave— does NOT exist (404). UseDELETE /api/chat/:id/participants/:participantId.getParticipants→GET /chat/:id/participants— does NOT exist (404). UseGET /api/chat/:id/info.updateParticipantRole→PUT /chat/:id/participants/:participantId— NOT IMPLEMENTED on backend.editMessage→ sends{ text }but backend expects{ content }— edits fail/ignored.addParticipants→ sends{ participants: string[] }but backend expects{ userId }(single).sendFileMessage→ POSTs to the text endpoint instead ofPOST /api/chat/:id/messages/file— file uploads broken.
Rate limits & constraints
- Messages: 20 messages / minute per user per chat.
- Typing indicators: 5 / 10 seconds.
- Message dedup: 5-minute window (duplicate sends within the window are de-duplicated).
- Edit window: 15 minutes — edits after that return 400.
- Message length: 5000-character maximum (schema + controller).
Database writes
chats: insert on create;$pushintomessages[];$setmetadata.lastActivity;unreadCounts.$.countincrement per recipient;settings.isArchivedtoggled (archive/unarchive); message soft-delete setsdeletedAt+ clearscontent; participant removal setsparticipants.$.isActive=false+participants.$.leftAt.
Socket events emitted
new-message→chat-{chatId}(every message).chat-notification→user-{recipientId}for non-senders (badge).messages-read→chat-{chatId}after read mark.message-deleted→chat-{chatId}after a message soft-delete.user-typing→chat-{chatId}(relayed byapp.ts).user-status-change→ broadcast whenuser-onlineis emitted (online only; no offline broadcast on disconnect).new-message(system) for system welcome lines on chat creation.
Side effects
metadata.lastActivitydrives the chat-list sort order.lastMessagecache lets the chat-list render previews without loading the entiremessages[]array.unreadCountsis the source-of-truth for badge counts; resetting on read also drives global unread totals.- Embedded messages array can grow large; consider migrating to a separate
messagescollection if conversations exceed several thousand messages.
Error / edge cases
- Sender not a participant →
403 "User is not a participant in this chat"(:209-211). - Chat not found →
404ongetChatMessages. - Direct duplicate → idempotent —
createChatreturns existing chat. - Content too long — backend rejects messages exceeding 5000 characters at both Mongoose schema and controller validation levels.
- Edit after 15 minutes →
400. - Files served from
uploads/chat/— anonymous access is allowed. Sensitive attachments (KYC docs, dispute evidence) should be protected by signed URLs or per-user authorisation; currently any user with the URL can fetch. - Stale online status — no offline broadcast on disconnect; peers may show "online" for a user who has left.
- Long conversations —
getChatMessagesslices an in-memory copy of the messages array. For >10k messages this is inefficient. Use aggregation$sliceor a separate collection. - Race on
markAsRead— two parallel reads may double-zero the counter, which is harmless. - Typing indicator spam — clients should debounce
typing-startand emittyping-stopon idle (rate-limited to 5/10s server-side regardless).
[!warning] Notification message uses placeholder sender name
ChatService.sendMessagepostschat-notificationwithsenderName: "کاربر"(:248) — the literal Persian word for "user". ResolvesenderNamefromparticipant.userId.firstNamefor a better UX.
Linked flows
- Notification Flow —
chat-notificationis one of the inputs. - Negotiation Flow — uses chats heavily.
- Dispute Flow — three-way group chat.
- Authentication Flow — supplies the
user-${userId}rooms viajoin-user-room.
Source files
- Backend:
backend/src/services/chat/ChatService.ts - Backend:
backend/src/services/chat/chatController.ts - Backend:
backend/src/services/chat/routes.ts(under/api/chat) - Backend:
backend/src/models/Chat.ts - Backend:
backend/src/app.ts:130-179(Socket.IO chat handlers) - Frontend:
frontend/src/sections/chat/ - Frontend:
frontend/src/contexts/socket-context.tsx(or equivalent socket provider)