8.9 KiB
title, tags, aliases
| title | tags | aliases | ||||||
|---|---|---|---|---|---|---|---|---|
| Chat |
|
|
Chat
Last updated: 2026-06-06 — MongoDB fully removed; PostgreSQL + Drizzle is the sole data layer (backend v2.9.12).
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/db/schema/chat.ts— PostgreSQL schema (Drizzle)backend/src/repositories/drizzle/DrizzleChatRepo.ts— repository implementation
[!warning] Embedded messages (JSONB) Messages and participants are stored as JSONB arrays inside the
chatstable (messages jsonb,participants jsonb), not as separate relational child tables. Very long-running chats can accumulate large blobs. Chat normalization (JSONB → relational child tables) is a future improvement, not yet done.
[!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 — chats table (PostgreSQL / Drizzle)
Source:
backend/src/db/schema/chat.ts
PostgreSQL is the only database layer. MongoDB and Mongoose have been completely removed from the backend runtime. Participants and messages are stored as JSONB blobs (ChatParticipant[] and ChatMessage[]) inside the chats table. Chat normalization (splitting messages and participants into separate relational child tables with proper FKs and threading support) is a known future improvement.
Enums (declared in _enums.ts)
| Enum | Values |
|---|---|
chat_type |
direct, group, support |
chat_participant_role |
member, admin, owner |
chat_message_type |
text, image, file, system |
chat_related_to_type |
PurchaseRequest, SellerOffer, Transaction |
Table: chats
| Column | PG type | Nullable | Default | Notes |
|---|---|---|---|---|
id |
uuid |
NOT NULL | gen_random_uuid() |
Primary key (PostgreSQL UUID — use .id, not ._id) |
legacy_object_id |
text |
nullable | — | Former Mongo ObjectId; partial-unique index WHERE NOT NULL |
type |
chat_type enum |
NOT NULL | 'direct' |
|
name |
text |
nullable | — | Group chat display name |
description |
text |
nullable | — | |
participants |
jsonb |
nullable | — | ChatParticipant[] blob — stored as JSONB array |
messages |
jsonb |
nullable | — | ChatMessage[] blob — stored as JSONB array |
related_to |
jsonb |
nullable | — | { type: chat_related_to_type, id: string } blob |
last_message |
jsonb |
nullable | — | Denormalised snapshot |
unread_counts |
jsonb |
nullable | — | { userId, count }[] blob |
settings_is_archived |
boolean |
nullable | false |
|
settings_is_muted |
boolean |
nullable | false |
|
settings_muted_until |
timestamp with time zone |
nullable | — | |
settings_notifications |
boolean |
nullable | true |
|
created_by |
text |
nullable | — | UUID string of creator |
created_at |
timestamp with time zone |
NOT NULL | now() |
|
updated_at |
timestamp with time zone |
NOT NULL | now() |
|
last_activity |
timestamp with time zone |
nullable | now() |
Sort key for chat lists |
Indexes on chats
| Index | Definition | Notes |
|---|---|---|
| PK | id |
|
| partial-unique | legacy_object_id WHERE NOT NULL |
Idempotent backfill upsert |
| regular | type |
|
| regular | created_by |
|
| regular | last_activity |
[!note] No FK to
userscreated_byis stored astext(notuuidFK) to accommodate both legacy Mongo ObjectIds (inlegacy_object_id) and PG UUIDs during the transition period.
Chat Schema — participants and messages (JSONB field shapes)
participants JSONB array element
| Field | Type | Description |
|---|---|---|
userId |
string (UUID) | Member id. |
role |
member / admin / owner |
Member role (default member). |
joinedAt |
ISO date string | Join time. |
lastSeen |
ISO date string? | Last activity. |
leftAt |
ISO date string? | Set on soft removal. |
isActive |
boolean | Still a participant (default true). Set to false on soft removal. |
[!note] Soft removal of participants Removing a participant does not delete the array element. It is a soft removal:
isActiveis set tofalseandleftAtis timestamped, preserving message attribution and history.
messages JSONB array element
| Field | Type | Description |
|---|---|---|
senderId |
string (UUID) | Author. |
senderType |
string | Currently fixed to User. |
content |
string | Message body. maxlength 5000 enforced at controller. |
messageType |
text / image / file / system |
Body kind (default text). |
fileUrl |
string? | If file/image. |
fileName |
string? | Original filename. |
fileSize |
number? | Bytes. |
timestamp |
ISO date string | Sent time. |
isRead |
boolean | Read flag (default false). |
isEdited |
boolean | Edited flag (default false). |
editedAt |
ISO date string? | When edited. |
deletedAt |
ISO date string? | Set on soft-delete; content is cleared but the element is kept. |
replyTo |
string? | Reply target message id. |
reactions |
{ userId: string, reaction: string }[] |
Emoji reactions. |
[!note] Messages are soft-deleted Deleting a message sets
deletedAtand clearscontent(the body becomes empty). The message element is not physically removed from themessages[]JSONB array, and amessage-deletedsocket event is emitted.
ID Field
The primary key is id (PostgreSQL UUID string). There is no _id field. The legacy_object_id column preserves the original MongoDB ObjectId for records migrated from Mongo, but is not used in application logic.
Instance / Document Methods (removed)
Mongoose document methods .addMessage(), .pull(), and .markAsRead() no longer exist. The repository layer (DrizzleChatRepo) performs equivalent operations using plain array operations on the JSONB blobs (read → mutate array in JS → write back).
| Former Mongoose method | Replacement |
|---|---|
chat.addMessage(data) + chat.save() |
DrizzleChatRepo.addMessage(chatId, messageData) — appends to JSONB array, updates last_message, increments unread counts, bumps last_activity |
chat.markAsRead(userId, messageIds?) + chat.save() |
DrizzleChatRepo.markAsRead(chatId, userId, messageIds?) — mutates messages JSONB array and zeroes unread_counts for that user |
chat.participants.pull(...) |
DrizzleChatRepo.removeParticipant(chatId, participantId) — soft-removes by setting isActive: false, leftAt in JSONB array |
Relationships
- References: User (
participants[].userId,messages[].senderId,messages[].reactions[].userId,last_message.senderId,unread_counts[].userId,created_by). - Referenced by: Dispute (
chatId), and any PurchaseRequest / SellerOffer indirectly throughrelated_to.
Future Work: Chat Normalization
The current JSONB-blob design unblocked the Mongo → PG migration but leaves these as known future improvements:
- Design a
chat_messagestable with proper threading/reply support (currentlyreplyTois embedded in the JSONB blob) - Design a
chat_participantstable (currently a JSONB blob with soft-removal semantics) - Migrate reactions, edit history, and read tracking to relational rows
- Align unread counts with the new structure
Until that work is complete, participants and messages in the chats table are not queryable relationally.
State Transitions
No top-level status. Chat-level archival is a boolean flag (settings_is_archived):
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 (DrizzleChatRepo)
await chatRepo.findByParticipant(userId); // filters on participants JSONB, orders by last_activity desc
// Chat for a purchase request
await chatRepo.findByRelatedTo('PurchaseRequest', purchaseRequestId);
// Append a message
await chatRepo.addMessage(chatId, { senderId, content: 'hi', messageType: 'text' });
// Mark read
await chatRepo.markAsRead(chatId, userId);
Related: User, PurchaseRequest, SellerOffer, Dispute.