--- title: Data Model Overview tags: [data-model, mongoose, postgres, drizzle, overview] aliases: [Models Index, Schema Overview] --- # Data Model Overview This section documents every Mongoose model that backs the marketplace and the parallel Drizzle/Postgres schema that is progressively replacing it. On backend `integrate-main-into-development@cab0719`, Mongoose models are still the live read path for most domains. The Drizzle layer has 17 applied migrations (0000–0017) and active dual-write repos for the majority of tables. > [!note] Scope > Twenty-two models are present in `backend/src/models/`. The "File" concept exists only at the service layer (`backend/src/services/file/`) and is not persisted as its own Mongoose collection, so it is not listed below. > > [!note] Documentation freshness > As of 2026-06-03 the Postgres migration inventory reflects migrations 0000–0017. The dual-write summary table at the bottom of this page is the authoritative migration-status reference. Individual model pages should be updated to note their PG table name and dual-write repo when they are deepened. > [!warning] Mongo vs Postgres runtime status > Dual-write repos exist for the majority of domain tables, but **reads are still served from Mongo** for all dual-write tables. Postgres is the sole store only for infra/bridge tables (`id_map`, `pg_dualwrite_gaps`), oracle quote rows (`payment_quotes`), and `config_setting_history`. Full read cutover is human-gated. See [[Postgres Runtime Cutover Status]]. ## Index of Models ### Mongo Models (still live read path) - [[User]] — Core identity. Stores credentials, profile, preferences, referral data, points, and WebAuthn passkeys. Every other model that records "who did what" points back at a `User._id`. Buyers, sellers, admins, resolvers, and guards all live in this collection, differentiated by a `role` enum. PG table: `users` (dual-write active). - [[PurchaseRequest]] — The buyer-side document at the heart of the marketplace. Captures what a buyer wants, the budget, urgency, delivery preferences, and the full lifecycle status (`pending_payment` → `seller_paid`). Aggregates [[SellerOffer]] references and tracks delivery codes. PG table: `purchase_requests` + 6 child tables (dual-write active). - [[SellerOffer]] — A seller's bid against a [[PurchaseRequest]]. Holds price, delivery ETA, attachments, and a small status machine (`pending` / `accepted` / `rejected` / `withdrawn`). PG table: `seller_offers` (dual-write active). - [[Payment]] — Records monetary movement intent and state: buyer pay-in, seller release, and refund. The current primary provider path is Request Network plus in-house checkout, derived destinations, funds ledger entries, and Transaction Safety Provider metadata. PG table: `payments` (dual-write active). - [[Chat]] — Conversation container with embedded messages, participants, unread counters, and reactions. Used for direct buyer-seller chats, group chats, and support tickets. Can be linked to a [[PurchaseRequest]] or [[SellerOffer]]. PG table: `chats` (conservative JSONB shim; Chat normalization is an open blocker). - [[Notification]] — Per-user notification with category, type, and 90-day TTL for automatic cleanup. References any related entity by stringified id. PG table: `notifications` (dual-write active; `user_id` stored as `text`, no hard FK). - [[RequestTemplate]] — A seller-authored, sharable template that pre-fills a [[PurchaseRequest]]. Carries a public shareable link, usage counter, and an optional default proposal. PG table: `request_templates` (dual-write active). - [[Dispute]] — Buyer-raised complaint tied to a [[PurchaseRequest]]. Captures evidence uploads, a timeline of admin actions, deadlines, and a structured resolution. PG table: `disputes` (dual-write active; all IDs as `text` for ObjectId/UUID coexistence). - [[BlogPost]] — Editorial content: title, slug, rich content, media, SEO metadata, view/like counters, and a draft/published/archived workflow. PG table: `blog_posts` (dual-write active). - [[Address]] — User shipping address book entry. Enforces a single primary address per user via a pre-save hook. PG table: `addresses` (schema scaffolded, migration 0016; `addressStore.ts` reads PG directly). - [[Category]] — Hierarchical product/service taxonomy referenced by [[PurchaseRequest]] and [[RequestTemplate]]. Supports parent/child via `parentId` and bilingual `name` / `nameEn`. PG table: `categories` (dual-write active). - [[Review]] — Polymorphic 1-5 star review against either a seller or a [[RequestTemplate]] (`subjectType` discriminator). One review per reviewer per subject (compound unique index). PG table: `reviews` (schema scaffolded, no dual-write repo yet). - [[PointTransaction]] — Ledger of point grants and spends per user. Sources include purchase, referral, bonus, admin grant, and redemption. PG table: `point_transactions` (dual-write active). - [[LevelConfig]] — Static configuration of loyalty tiers (level number, point thresholds, benefits, icon, color). Driven by admins; consumed by the [[User]].points.level field. No PG table (read-only config; not yet migrated). - [[ShopSettings]] — One-to-one storefront branding for a seller: name, description, avatar, cover image, review toggles, and social links. PG table: `shop_settings` (schema scaffolded, no dual-write repo yet). - [[TempVerification]] — Short-lived signup record that holds candidate user data and a verification code. Auto-purges via TTL when `emailVerificationCodeExpires` passes. No PG table (TTL-only; not yet migrated). - [[TelegramLink]] — Permanent auditable association between a Telegram user ID and an Amanat [[User]]. Stores Telegram profile metadata, link source (`miniapp` / `bot` / `login_widget`), status (`active` / `blocked`), and last-seen timestamp. One per Telegram user (unique on both `userId` and `telegramUserId`). PG table: `telegram_links` (schema scaffolded, no dual-write repo yet). - [[TelegramSession]] — Short-lived Telegram Mini App session token issued when `initData` is verified. Carries the `initDataFingerprint` for replay protection and auto-expires via a MongoDB TTL index on `expiresAt`. PG table: `telegram_sessions` (schema scaffolded, no dual-write repo yet). - [[ConfigSetting]] — Runtime configuration persisted in MongoDB for operational knobs that need an admin surface rather than a deploy. PG table: `config_settings` (schema scaffolded, no dual-write repo yet). - [[DerivedDestination]] — Per-payment derived wallet destination records used to reduce address reuse and reconcile on-chain pay-ins. PG table: `derived_destinations` + `derived_destination_sweeps` (dual-write active). - [[FundsLedgerEntry]] — Immutable accounting ledger rows for pay-in, hold, release, refund, fee, adjustment, and reversal events. PG table: `funds_ledger_entries` (dual-write active; immutability enforced by DB trigger since migration 0015). - [[TrezorAccount]] — Hardware-wallet/safekeeping account metadata for custody operations and staged signer hardening. PG table: `trezor_accounts` + `trezor_derived_addresses` (dual-write active). - [[ConfigSettingHistory]] — Immutable audit trail of numeric runtime-config changes. Currently used for per-chain confirmation threshold change events, keyed as `confirmation_threshold:`. Added in commit `27fb15a`. PG table: `config_setting_history` (PG-only; no Mongo equivalent). ### PG-Only Tables (no Mongo equivalent) - `id_map` — ObjectId → UUID bridge. Every migrated table upserts here during backfill/dual-write. Composite PK on `(collection, legacy_object_id)`, unique on `new_id`. - `pg_dualwrite_gaps` — Append-only reconciliation gap log for failed PG dual-writes. Tracks collection, op, payload, severity, and resolution metadata. - `payment_quotes` — Oracle pricing quotes per payment (oracle depeg-protection feature). Stores `fx_rate`, `token_price_usd`, `depeg_adjustment_bps`, `settle_amount`, chain/token, and expiry. Requires `ORACLE_QUOTING_ENABLED=true`. 1:1 to `payments`. - `user_passkeys` — WebAuthn credential store (child of `users`). Columns: credential id (text PK), `user_id FK→users CASCADE`, `public_key`, `counter`, `device_type`, `device_name`. - `user_refresh_tokens` — Refresh token store (child of `users`). Columns: `token text PK`, `user_id FK→users CASCADE`. ## Relationship Diagram ```mermaid erDiagram USER ||--o{ PURCHASE_REQUEST : "creates as buyer" USER ||--o{ SELLER_OFFER : "submits as seller" USER ||--o{ ADDRESS : "owns" USER ||--o{ NOTIFICATION : "receives" USER ||--o{ POINT_TRANSACTION : "earns/spends" USER ||--o{ REQUEST_TEMPLATE : "authors as seller" USER ||--o| SHOP_SETTINGS : "configures" USER ||--o{ BLOG_POST : "publishes" USER ||--o{ REVIEW : "writes as reviewer" USER ||--o{ DISPUTE : "raises as buyer" USER ||--o{ USER : "referred by" USER ||--o{ TREZOR_ACCOUNT : "controls custody account" USER ||--o{ USER_PASSKEY : "authenticates with" USER ||--o{ USER_REFRESH_TOKEN : "sessions via" USER ||--o| TELEGRAM_LINK : "links identity" USER ||--o{ TELEGRAM_SESSION : "session for" PURCHASE_REQUEST }o--|| CATEGORY : "belongs to" PURCHASE_REQUEST ||--o{ SELLER_OFFER : "receives" PURCHASE_REQUEST ||--o{ PAYMENT : "settled by" PURCHASE_REQUEST ||--o| CHAT : "discussed in" PURCHASE_REQUEST ||--o{ DISPUTE : "may trigger" PURCHASE_REQUEST ||--o| REVIEW : "rated by buyer" SELLER_OFFER ||--o| PAYMENT : "funds" SELLER_OFFER }o--|| PURCHASE_REQUEST : "responds to" PAYMENT }o--|| USER : "buyer" PAYMENT }o--|| USER : "seller" PAYMENT ||--o{ FUNDS_LEDGER_ENTRY : "accounted by" PAYMENT ||--o| DERIVED_DESTINATION : "collects into" PAYMENT ||--o| PAYMENT_QUOTE : "oracle-priced by" CHAT }o--o{ USER : "participants" CHAT ||--o{ DISPUTE : "support channel" REQUEST_TEMPLATE }o--|| CATEGORY : "belongs to" REQUEST_TEMPLATE ||--o{ REVIEW : "rated as subject" CATEGORY ||--o{ CATEGORY : "parent of" POINT_TRANSACTION }o--|| USER : "owner" LEVEL_CONFIG ||..|| USER : "level lookup" TEMP_VERIFICATION ||..|| USER : "promoted to" TELEGRAM_LINK }o--|| USER : "links identity" TELEGRAM_SESSION }o--o| USER : "session for" TELEGRAM_SESSION }o--|| TELEGRAM_LINK : "matches" TREZOR_ACCOUNT ||--o{ TREZOR_DERIVED_ADDRESS : "issues" DERIVED_DESTINATION ||--o{ DERIVED_DESTINATION_SWEEP : "swept by" ID_MAP ||..|| USER : "bridges ObjectId" ``` ## Conventions Across All Models ### Mongoose Conventions > [!note] Shared schema patterns > - **Timestamps**: every model declares `{ timestamps: true }`, so `createdAt` and `updatedAt` are always present. > - **ObjectId references**: foreign keys use `Schema.Types.ObjectId` with an explicit `ref` (e.g. `ref: 'User'`). The two exceptions are [[Notification]] and [[Payment]] which use string-typed or `Mixed` identifiers in places to support template-flow payments. > - **Soft delete**: deletion is modelled as a `status` flag (e.g. `User.status = 'deleted'`, `BlogPost.status = 'archived'`) rather than physical removal. > - **TTL indexes**: short-lived collections ([[Notification]], [[TempVerification]]) use `{ expireAfterSeconds: ... }` so MongoDB does the cleanup. > - **toJSON sanitisation**: [[User]] overrides `toJSON` to strip credentials, refresh tokens, and verification codes before serialisation. > [!warning] Index discipline > Several schemas leave a comment noting that `unique: true` already creates an index — adding `schema.index({ field: 1 })` on top would produce a duplicate-index warning at startup. When introducing new indexes, search for `unique: true` first. ### Drizzle/Postgres Conventions > [!note] PG schema patterns > - **Legacy bridge**: every migrated table carries `legacy_object_id text` with a partial-unique index `WHERE legacy_object_id IS NOT NULL` for idempotent backfill upserts. The `id_map` table records the ObjectId → UUID mapping centrally. > - **Money columns**: `numeric(38,18)` for fiat/crypto amounts throughout, except `seller_offers` which uses `numeric(18,8)` per the Migration Guide. Blockchain balance columns use `numeric(78,0)` to hold uint256 without overflow. > - **Polymorphic triples**: the `ref_kind` enum (`entity` | `template`) discriminator is expanded into three columns (`_ref_kind`, `_id`, `_external_ref`) with a CHECK constraint to enforce discriminator integrity. Used by `payments`, `funds_ledger_entries`, and `derived_destinations`. > - **Soft delete**: `addresses` uses `deleted_at timestamptz` (nullable) with partial-unique indexes scoped to `WHERE deleted_at IS NULL`. Most other tables retain the Mongo `status` flag approach. > - **Timestamps**: all timestamp columns declare `withTimezone: true`. > - **Immutability**: `funds_ledger_entries` has both an UPDATE-blocking and a DELETE-blocking trigger installed at the DB level (migrations 0004, 0015). A TRUNCATE trigger was added in migration 0013. > - **user_role enum**: values are `admin`, `buyer`, `seller`, `resolver`, `guard`. The `guard` value was added in migration 0017. ## Postgres Migration Inventory Schema entry point: `backend/src/db/schema/index.ts` | Migration | File | Summary | |---|---|---| | 0000 | `0000_slimy_veda.sql` | Initial: core enums + `id_map` + `categories` | | 0001 | `0001_wild_cargill.sql` | `trezor_accounts` + `trezor_derived_addresses` (later reset) | | 0002 | `0002_motionless_grey_gargoyle.sql` | Schema reset: drops 0000/0001 tables to be rebuilt in 0003; adds `categories.parent_id` self-FK | | 0003 | `0003_remarkable_retro_girl.sql` | Comprehensive rebuild: all enums + full core domain (`users`, `payments`, `funds_ledger_entries`, `derived_destinations`, `purchase_requests` + 6 children, `seller_offers`, `point_transactions`, `trezor_*`) | | 0004a | `0004_funds_ledger_entries.sql` | UPDATE-blocking immutability trigger on `funds_ledger_entries` | | 0004b | `0004_seller_offer.sql` | Physical FKs on `seller_offers` → `users` and `purchase_requests` (CASCADE) | | 0005 | `0005_simple_champions.sql` | `pg_dualwrite_gaps`; FKs on `payments`; `legacy_object_id` unique indexes; refined pending-RN payment unique index | | 0006 | `0006_normal_madame_hydra.sql` | CHECK: `purchase_requests.budget_currency` restricted to crypto (USDT, USDC) | | 0007 | `0007_woozy_shaman.sql` | Drops 0006 constraint; sets `budget_currency` default to `'USDT'` | | 0008 | `0008_giant_winter_soldier.sql` | Adds `'TRY'` to `offer_currency` enum; creates `payment_quotes` table | | 0009 | `0009_unique_active_categories.sql` | Category deduplication; partial unique index on normalized active category name | | 0010 | `0010_request_templates.sql` | Creates `request_templates`; deduplicates `purchase_request_specifications`; adds unique key constraint | | 0011 | `0011_chats.sql` | Creates `chats` with JSONB participant/message storage + chat-related enums | | 0012 | `0012_disputes.sql` | Creates `disputes` (text IDs, JSONB evidence/timeline/resolution) | | 0013 | `0013_money_constraints.sql` | Money-integrity CHECKs on `payments`, `payment_quotes`, `point_transactions`, `users`; TRUNCATE trigger on `funds_ledger_entries`; composite PK + unique on `id_map` | | 0014 | `0014_physical_fks.sql` | NOT VALID FKs across all major tables (validated immediately); composite indexes on `payments`, `purchase_requests`, `seller_offers` | | 0015 | `0015_funds_ledger_immutable_trigger.sql` | Replaces/extends ledger triggers: UPDATE-block + new DELETE-block on `funds_ledger_entries` | | 0016 | `0016_addresses_table.sql` | `address_type` enum + `addresses` table; partial-unique primary-address-per-user index | | 0017 | `0017_user_role_guard.sql` | Adds `'guard'` to `user_role` enum (idempotent `ADD VALUE IF NOT EXISTS`) | ## Drizzle Table Inventory and Migration Status ### Infrastructure / Bridge | PG Table | Schema File | Status | Notes | |---|---|---|---| | `id_map` | `idMap.ts` | PG-only | ObjectId → UUID bridge; composite PK + unique on `new_id` | | `pg_dualwrite_gaps` | `pgDualwriteGaps.ts` | PG-only | Append-only reconciliation gap log for failed dual-writes | ### Core Domain | PG Table | Schema File | Status | Dual-Write Repo | |---|---|---|---| | `users` | `users.ts` | Dual-write active | `DualWriteUserRepo` + `DrizzleUserRepo` + `MongoUserRepo` | | `user_passkeys` | `users.ts` | Dual-write active (child of users) | — | | `user_refresh_tokens` | `users.ts` | Dual-write active (child of users) | — | | `categories` | `category.ts` | Dual-write active | `DualWriteMarketplaceRepo` | | `purchase_requests` | `purchaseRequest.ts` | Dual-write active | `DualWriteMarketplaceRepo` | | `purchase_request_delivery_info` | `purchaseRequest.ts` | Dual-write active (1:1 child) | — | | `purchase_request_delivery_address` | `purchaseRequest.ts` | Dual-write active (1:1 child) | — | | `purchase_request_seller_delivery_info` | `purchaseRequest.ts` | Dual-write active (1:1 child) | — | | `delivery_attempts` | `purchaseRequest.ts` | Dual-write active (1:N child) | — | | `purchase_request_service_info` | `purchaseRequest.ts` | Dual-write active (1:1 child) | — | | `purchase_request_specifications` | `purchaseRequest.ts` | Dual-write active (1:N child) | — | | `purchase_request_preferred_sellers` | `purchaseRequest.ts` | Dual-write active (N:M junction) | — | | `seller_offers` | `sellerOffer.ts` | Dual-write active | `DualWriteMarketplaceRepo` | | `payments` | `payment.ts` | Dual-write active | `DualWritePaymentRepo` + `DrizzlePaymentRepo` + `MongoPaymentRepo` | | `payment_quotes` | `paymentQuote.ts` | PG-only | No Mongo equivalent; oracle depeg-protection feature | | `funds_ledger_entries` | `fundsLedgerEntry.ts` | Dual-write active | `DrizzlePaymentRepo` / `DualWritePaymentRepo` | | `derived_destinations` | `derivedDestination.ts` | Dual-write active | `DualWriteDerivedDestinationRepo` + `DrizzleDerivedDestinationRepo` | | `derived_destination_sweeps` | `derivedDestination.ts` | Dual-write active (append-only child) | — | | `trezor_accounts` | `trezorAccount.ts` | Dual-write active | `DualWriteTrezorAccountRepo` + `DrizzleTrezorAccountRepo` | | `trezor_derived_addresses` | `trezorAccount.ts` | Dual-write active (child of trezor_accounts) | — | | `point_transactions` | `pointTransaction.ts` | Dual-write active | `DualWritePointsRepo` + `DrizzlePointsRepo` | | `request_templates` | `requestTemplate.ts` | Dual-write active | `DualWriteMarketplaceRepo` | | `chats` | `chat.ts` | Dual-write active | `DrizzleChatRepo` | | `blog_posts` | `blogPost.ts` | Dual-write active | `DualWriteBlogRepo` + `DrizzleBlogRepo` | | `notifications` | `notification.ts` | Dual-write active | `DualWriteNotificationRepo` + `DrizzleNotificationRepo` | | `disputes` | `dispute.ts` | Dual-write active | `DualWriteDisputeRepo` + `DrizzleDisputeRepo` | | `addresses` | `address.ts` | Schema scaffolded | No dual-write repo; `addressStore.ts` reads PG directly (migration 0016) | | `shop_settings` | `shopSettings.ts` | Schema scaffolded | No dual-write repo | | `config_settings` | `configSetting.ts` | Schema scaffolded | No dual-write repo | | `config_setting_history` | `configSetting.ts` | PG-only | No Mongo equivalent; child of `config_settings` | | `telegram_links` | `telegramLink.ts` | Schema scaffolded | No dual-write repo | | `telegram_sessions` | `telegramSession.ts` | Schema scaffolded | No dual-write repo | | `reviews` | `review.ts` | Schema scaffolded | No dual-write repo | > [!note] Read cutover status > **Dual-write active** means writes go to both Mongo and PG; reads still come from Mongo (per MEMORY.md as of 2026-06-03). **Schema scaffolded** means the Drizzle table exists but no DualWriteRepo plumbs it. **PG-only** means there is no Mongo model for that data. ## Shared Enum Reference Enums live in `backend/src/db/schema/_enums.ts` (shared) and individual schema files. Key enums: | Enum | Values | |---|---| | `user_role` | admin, buyer, seller, resolver, guard | | `auth_provider` | email, google, telegram | | `user_status` | active, suspended, deleted | | `purchase_request_status` | pending_payment, pending, received_offers, in_negotiation, payment_pending, payment_confirmed, in_progress, delivery, delivered, completed, disputed, refunded, seller_paid | | `offer_status` | pending, accepted, rejected, withdrawn, active | | `offer_currency` | USD, EUR, IRR, USDT, USDC, TRY | | `payment_provider` | request.network, amn.scanner, shkeeper, other | | `payment_status` | pending, processing, confirmed, completed, failed, cancelled, refunded | | `escrow_state` | funded, releasable, released, refunded, releasing, failed, cancelled, partial | | `funds_ledger_entry_type` | payment_detected, provider_fee, platform_fee, hold, release, refund, dispute_hold, adjustment | | `derived_destination_status` | active, swept, sweeping, quarantined | | `ref_kind` | entity, template | | `chat_type` | direct, group, support | | `review_subject_kind` | seller, template | | `address_type` | Home, Office, Other | | `telegram_link_source` | miniapp, bot, login_widget | | `telegram_link_status` | active, blocked | ## Lifecycle View The dominant happy-path flow exercises five collections in order: 1. A buyer (`User`) creates a `PurchaseRequest` with `status: 'pending'`. 2. Sellers (other `User`s) attach `SellerOffer` documents; the request transitions through `received_offers` → `in_negotiation` as the parties chat in a `Chat`. 3. The buyer accepts an offer; a `Payment` is opened against the Request Network provider and, once verified by webhook/reconciliation and safety checks, advances to a funded escrow state. If `ORACLE_QUOTING_ENABLED=true`, a `payment_quote` row is written to PG at this point. 4. The seller marks the request `delivery` → `delivered`; the buyer confirms with the 6-digit `deliveryCode` and the request becomes `completed`. 5. The escrow `Payment` flips to `released` after a ledger-gated custody transfer instruction. Each ledger event appends an immutable `FundsLedgerEntry` row (Mongo + PG). Optionally the buyer writes a `Review` and earns a `PointTransaction`. If anything goes sideways, the buyer can open a `Dispute`, which freezes release until an admin resolves it (refund, replacement, compensation, or no-action). ## How to Navigate Each model has its own note in this folder. Cross-references use `[[wikilinks]]` so backlinks work in Obsidian's graph view. Schemas are documented at field-level granularity — every field is listed with its type, default, validation, and indexing decisions. Where a model carries a meaningful state machine, a Mermaid `stateDiagram-v2` accompanies the schema table. > [!note] Source of truth > The information below is mirrored from the TypeScript schema definitions. If a field listed here disagrees with the code, the code wins — please update the note. All source citations use the form `backend/src/models/.ts:` for Mongo and `backend/src/db/schema/.ts:` for Drizzle/PG.