docs: update PG migration status, data models, architecture + add Telegram Mini App flow (v2.8.59)
- Postgres Runtime Cutover Status: 17 migrations (0000–0017), dual-write repo matrix - Backend Architecture: dual-DB architecture, repo factory, MONGO_CONNECT_MODE modes - Data Model Overview: 23-model index with PG table names and migration status - User, PurchaseRequest, SellerOffer, Chat, Dispute: Drizzle schema + cutover status added - 04 - Flows/Telegram Mini App.md: new doc covering Mini App architecture and flows - mongo-to-pg-migration-prd.md: status block prepended with 2026-06-03 milestone tracking Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
---
|
||||
title: Chat
|
||||
tags: [data-model, mongoose]
|
||||
tags: [data-model, mongoose, postgres]
|
||||
aliases: [Conversation, IChat, IMessage]
|
||||
---
|
||||
|
||||
# Chat
|
||||
> **Last updated:** 2026-05-29 — aligned with code (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
|
||||
> **Last updated:** 2026-06-03 — added Postgres/Drizzle schema section; migration status clarified.
|
||||
|
||||
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`).
|
||||
|
||||
@@ -13,6 +13,7 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
|
||||
> `backend/src/models/Chat.ts:130` — chat schema definition
|
||||
> `backend/src/models/Chat.ts:69` — message subdocument schema
|
||||
> `backend/src/models/Chat.ts:348` — model export
|
||||
> `backend/src/db/schema/chat.ts` — Drizzle/Postgres schema
|
||||
|
||||
> [!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.
|
||||
@@ -20,7 +21,10 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
|
||||
> [!warning] `relatedTo` is NOT set via `POST /api/chat`
|
||||
> Although `relatedTo` exists in the schema, it is **not accepted** by the `POST /api/chat` create endpoint. Purchase-request linkage is established server-side through the dedicated `POST /api/chat/purchase-request`, not by passing `relatedTo` to the generic create endpoint.
|
||||
|
||||
## Schema — Chat
|
||||
> [!danger] Migration status — DUAL-WRITE, reads still on Mongo
|
||||
> Chat writes go to **both** MongoDB and Postgres (via `DrizzleChatRepo`). However, **all reads still come from MongoDB**. The Postgres `chats` table is a conservative shim: `participants` and `messages` are stored as JSONB blobs, not normalised child tables. Full normalisation (splitting messages into a separate table with proper threading) is a **known open blocker** for the Mongo → PG read cutover. Do not assume PG data is queryable relationally until that work is complete.
|
||||
|
||||
## Schema — Chat (MongoDB / Mongoose)
|
||||
|
||||
| Field | Type | Required | Default | Validation | Index | Description |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
@@ -57,7 +61,7 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
|
||||
> [!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: `isActive` is set to `false` and `leftAt` is timestamped, preserving message attribution and history.
|
||||
|
||||
## Schema — Message (embedded)
|
||||
## Schema — Message (embedded, MongoDB)
|
||||
|
||||
| Field | Type | Required | Default | Validation | Description |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
@@ -80,13 +84,65 @@ Conversation container with embedded messages. Used for buyer-seller direct chat
|
||||
> [!note] Messages are soft-deleted
|
||||
> Deleting a message sets `deletedAt` and clears `content` (the body becomes empty). The message subdocument is **not** physically removed from `messages[]`, and a `message-deleted` socket event is emitted.
|
||||
|
||||
## Schema — `chats` table (Postgres / Drizzle)
|
||||
|
||||
> Source: `backend/src/db/schema/chat.ts`
|
||||
|
||||
> [!warning] Conservative JSONB shim — not normalised
|
||||
> Unlike most other migrated tables, participants and messages are stored as **JSONB blobs** (`ChatParticipant[]` and `ChatMessage[]`), not as separate relational child tables. This was a deliberate trade-off to unblock dual-write without committing to a normalisation design. The normalised schema (separate `chat_messages` and `chat_participants` tables with proper FKs and threading support) is the **primary blocker** for cutting reads over to Postgres.
|
||||
|
||||
### 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 |
|
||||
| `legacy_object_id` | `text` | nullable | — | Mongo ObjectId bridge; 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 — **not normalised** |
|
||||
| `messages` | `jsonb` | nullable | — | `ChatMessage[]` blob — **not normalised** |
|
||||
| `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 | — | Mongo ObjectId or 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 `users`
|
||||
> `created_by` is stored as `text` (not `uuid` FK) to accommodate both Mongo ObjectIds and PG UUIDs during the transition period.
|
||||
|
||||
## Virtuals
|
||||
|
||||
| Virtual | Returns | Definition |
|
||||
| --- | --- | --- |
|
||||
| `participantsCount` | Count of active participants | `backend/src/models/Chat.ts:259` |
|
||||
|
||||
## Indexes
|
||||
## Indexes (MongoDB)
|
||||
|
||||
Defined at `backend/src/models/Chat.ts:243-247`:
|
||||
|
||||
@@ -119,6 +175,24 @@ None defined.
|
||||
- **References**: [[User]] (`participants[].userId`, `messages[].senderId`, `messages[].reactions[].userId`, `lastMessage.senderId`, `unreadCounts[].userId`, `metadata.createdBy`).
|
||||
- **Referenced by**: [[Dispute]] (`chatId`), and any [[PurchaseRequest]] / [[SellerOffer]] indirectly through `relatedTo`.
|
||||
|
||||
## Migration Status
|
||||
|
||||
| Dimension | Status |
|
||||
| --- | --- |
|
||||
| Dual-write repo | `DrizzleChatRepo` — active |
|
||||
| Writes | Both MongoDB and Postgres receive writes |
|
||||
| Reads | **MongoDB only** — not yet cut over |
|
||||
| Postgres schema style | JSONB shim (participants + messages as blobs) |
|
||||
| Normalisation blocker | Chat message threading design not finalised — blocks PG read cutover |
|
||||
|
||||
The normalisation work required before reads can be cut to PG:
|
||||
1. Design a `chat_messages` table with proper threading/reply support (currently `replyTo` is an ObjectId embedded in a JSONB blob)
|
||||
2. Design a `chat_participants` table (currently a JSONB blob with soft-removal semantics)
|
||||
3. Migrate reactions, edit history, and read tracking to relational rows
|
||||
4. Align unread counts with the new structure
|
||||
|
||||
Until that work is complete, the Postgres `chats` table is treated as a write-ahead log / backup, not the source of truth for reads.
|
||||
|
||||
## State Transitions
|
||||
|
||||
No top-level status. Chat-level archival is a boolean flag (`settings.isArchived`):
|
||||
|
||||
Reference in New Issue
Block a user