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>
8.3 KiB
title, tags, aliases
| title | tags | aliases | |||||
|---|---|---|---|---|---|---|---|
| Chat |
|
|
Chat
Last updated: 2026-05-29 — aligned with code (see Doc vs Code Audit Report)
Conversation container with embedded messages. Used for buyer-seller direct chats, group chats, and support tickets. Each chat carries a list of participants, an embedded messages[] array (with reactions, replies, edit history), a denormalised lastMessage snapshot for list views, and per-user unreadCounts. A chat can be linked to any other entity through the relatedTo discriminator (currently PurchaseRequest, SellerOffer, or Transaction).
[!note] Source
backend/src/models/Chat.ts:130— chat schema definitionbackend/src/models/Chat.ts:69— message subdocument schemabackend/src/models/Chat.ts:348— model export
[!warning] Embedded messages Messages live inside the chat document. Very long-running chats can grow past the 16 MB document limit. Treat this as a known constraint of the current schema.
[!warning]
relatedTois NOT set viaPOST /api/chatAlthoughrelatedToexists in the schema, it is not accepted by thePOST /api/chatcreate endpoint. Purchase-request linkage is established server-side through the dedicatedPOST /api/chat/purchase-request, not by passingrelatedToto the generic create endpoint.
Schema — Chat
| Field | Type | Required | Default | Validation | Index | Description |
|---|---|---|---|---|---|---|
type |
String | yes | direct |
enum: direct / group / support |
yes | Conversation type. |
name |
String | no | — | maxlength 100 | — | Display name (group chats). |
description |
String | no | — | maxlength 500 | — | Optional description. |
participants[].userId |
ObjectId → User | yes | — | — | yes | Member id. |
participants[].role |
String | no | member |
enum: member / admin / owner |
— | Member role. |
participants[].joinedAt |
Date | no | Date.now |
— | — | Join time. |
participants[].lastSeen |
Date | no | — | — | — | Last activity. |
participants[].leftAt |
Date | no | — | — | — | Set when the participant is removed (soft removal). |
participants[].isActive |
Boolean | no | true |
— | — | Still a participant. Set to false on soft removal (subdocument is kept). |
messages[] |
Subdocument[] | no | [] |
— | yes (messages.timestamp) |
Embedded messages. |
relatedTo.type |
String | no | — | enum: PurchaseRequest / SellerOffer / Transaction |
yes (compound) | Linked entity kind. Not accepted via POST /api/chat — set only via POST /api/chat/purchase-request. |
relatedTo.id |
ObjectId | no | — | — | yes (compound) | Linked entity id. |
lastMessage.content |
String | no | — | — | — | Snapshot for list views. |
lastMessage.senderId |
ObjectId → User | no | — | — | — | Last sender. |
lastMessage.timestamp |
Date | no | — | — | — | Last message time. |
lastMessage.messageType |
String | no | — | — | — | Last message type. |
unreadCounts[].userId |
ObjectId → User | no | — | — | — | User the counter belongs to. |
unreadCounts[].count |
Number | no | 0 |
— | — | Number of unread messages. |
settings.isArchived |
Boolean | no | false |
— | — | Archived flag. |
settings.isMuted |
Boolean | no | false |
— | — | Muted flag. |
settings.mutedUntil |
Date | no | — | — | — | Mute expiry. |
settings.notifications |
Boolean | no | true |
— | — | Per-chat notification toggle. |
metadata.createdBy |
ObjectId → User | yes | — | — | — | Original creator. |
metadata.createdAt |
Date | no | Date.now |
— | — | Created timestamp. |
metadata.updatedAt |
Date | no | Date.now |
— | — | Touched by pre-save. |
metadata.lastActivity |
Date | no | Date.now |
— | yes (desc) | Sort key for chat lists. |
[!note] No top-level
timestampsUnlike most models, this schema does not pass{ timestamps: true }. It uses its ownmetadata.createdAt/metadata.updatedAtinstead, maintained by the pre-save hook.
[!note] Soft removal of participants Removing a participant (via
DELETE /api/chat/:id/participants/:participantId) does not delete the subdocument. It is a soft removal:isActiveis set tofalseandleftAtis timestamped, preserving message attribution and history.
Schema — Message (embedded)
| Field | Type | Required | Default | Validation | Description |
|---|---|---|---|---|---|
senderId |
ObjectId → User | yes | — | — | Author. |
senderType |
String | no | User |
— | Currently fixed. |
content |
String | yes | — | maxlength 5000 | Message body. Enforced at both schema and controller. |
messageType |
String | no | text |
enum: text / image / file / system |
Body kind. |
fileUrl |
String | no | — | — | If file/image. |
fileName |
String | no | — | — | Original filename. |
fileSize |
Number | no | — | — | Bytes. |
timestamp |
Date | no | Date.now |
— | Sent time. |
isRead |
Boolean | no | false |
— | Read flag. |
isEdited |
Boolean | no | false |
— | Edited flag. |
editedAt |
Date | no | — | — | When edited. |
deletedAt |
Date | no | — | — | Set on soft-delete; content is cleared but the subdocument is kept. |
replyTo |
ObjectId | no | — | — | Reply target message id. |
reactions[].userId |
ObjectId → User | no | — | — | Reacting user. |
reactions[].reaction |
String | no | — | maxlength 10 | Emoji. |
[!note] Messages are soft-deleted Deleting a message sets
deletedAtand clearscontent(the body becomes empty). The message subdocument is not physically removed frommessages[], and amessage-deletedsocket event is emitted.
Virtuals
| Virtual | Returns | Definition |
|---|---|---|
participantsCount |
Count of active participants | backend/src/models/Chat.ts:259 |
Indexes
Defined at backend/src/models/Chat.ts:243-247:
{ 'participants.userId': 1 }{ 'metadata.lastActivity': -1 }{ 'relatedTo.type': 1, 'relatedTo.id': 1 }{ 'messages.timestamp': -1 }{ type: 1 }
Pre/Post Hooks
| Hook | Behaviour |
|---|---|
pre('save') (backend/src/models/Chat.ts:250) |
Updates metadata.updatedAt and refreshes metadata.lastActivity when there are messages. |
Instance Methods
| Signature | Purpose |
|---|---|
getUnreadCount(userId: Types.ObjectId): number |
Returns the unread counter for a participant. backend/src/models/Chat.ts:264 |
addMessage(messageData: Partial<IMessage>): IMessage |
Pushes a message, updates lastMessage, increments unread counters for everyone except the sender, and bumps lastActivity. backend/src/models/Chat.ts:270 |
markAsRead(userId, messageIds?: Types.ObjectId[]): void |
Marks listed messages (or all when messageIds is empty/omitted) as read for the user, zeros their unread counter, and updates lastSeen. backend/src/models/Chat.ts:308 |
Static Methods
None defined.
Relationships
- References: User (
participants[].userId,messages[].senderId,messages[].reactions[].userId,lastMessage.senderId,unreadCounts[].userId,metadata.createdBy). - Referenced by: Dispute (
chatId), and any PurchaseRequest / SellerOffer indirectly throughrelatedTo.
State Transitions
No top-level status. Chat-level archival is a boolean flag (settings.isArchived):
stateDiagram-v2
[*] --> active
active --> muted : user mutes
muted --> active : unmute / mute expires
active --> archived : user archives
archived --> active : restore
Common Queries
// A user's recent chats
Chat.find({ 'participants.userId': userId, 'participants.isActive': true })
.sort({ 'metadata.lastActivity': -1 });
// Chat for a purchase request
Chat.findOne({ 'relatedTo.type': 'PurchaseRequest', 'relatedTo.id': prId });
// Append a message
const chat = await Chat.findById(id);
chat.addMessage({ senderId, content: 'hi', messageType: 'text' });
await chat.save();
// Mark read
chat.markAsRead(userId);
await chat.save();
Related: User, PurchaseRequest, SellerOffer, Dispute.