Files
nick-doc/02 - Data Models/Data Model Overview.md
Siavash Sameni d072238fe8 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>
2026-06-03 10:30:51 +04:00

23 KiB
Raw Blame History

title, tags, aliases
title tags aliases
Data Model Overview
data-model
mongoose
postgres
drizzle
overview
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 (00000017) 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 00000017. 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_paymentseller_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:<chainId>. 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

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_offersusers 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 Users) attach SellerOffer documents; the request transitions through received_offersin_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 deliverydelivered; 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/<File>.ts:<line> for Mongo and backend/src/db/schema/<File>.ts:<line> for Drizzle/PG.