- 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>
23 KiB
title, tags, aliases
| title | tags | aliases | |||||||
|---|---|---|---|---|---|---|---|---|---|
| Data Model 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), andconfig_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 aroleenum. 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_idstored astext, 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 astextfor 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.tsreads PG directly). - Category — Hierarchical product/service taxonomy referenced by PurchaseRequest and RequestTemplate. Supports parent/child via
parentIdand bilingualname/nameEn. PG table:categories(dual-write active). - Review — Polymorphic 1-5 star review against either a seller or a RequestTemplate (
subjectTypediscriminator). 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
emailVerificationCodeExpirespasses. 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 bothuserIdandtelegramUserId). PG table:telegram_links(schema scaffolded, no dual-write repo yet). - TelegramSession — Short-lived Telegram Mini App session token issued when
initDatais verified. Carries theinitDataFingerprintfor replay protection and auto-expires via a MongoDB TTL index onexpiresAt. 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 commit27fb15a. 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 onnew_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). Storesfx_rate,token_price_usd,depeg_adjustment_bps,settle_amount, chain/token, and expiry. RequiresORACLE_QUOTING_ENABLED=true. 1:1 topayments.user_passkeys— WebAuthn credential store (child ofusers). Columns: credential id (text PK),user_id FK→users CASCADE,public_key,counter,device_type,device_name.user_refresh_tokens— Refresh token store (child ofusers). 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 }, socreatedAtandupdatedAtare always present.- ObjectId references: foreign keys use
Schema.Types.ObjectIdwith an explicitref(e.g.ref: 'User'). The two exceptions are Notification and Payment which use string-typed orMixedidentifiers in places to support template-flow payments.- Soft delete: deletion is modelled as a
statusflag (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
toJSONto strip credentials, refresh tokens, and verification codes before serialisation.
[!warning] Index discipline Several schemas leave a comment noting that
unique: truealready creates an index — addingschema.index({ field: 1 })on top would produce a duplicate-index warning at startup. When introducing new indexes, search forunique: truefirst.
Drizzle/Postgres Conventions
[!note] PG schema patterns
- Legacy bridge: every migrated table carries
legacy_object_id textwith a partial-unique indexWHERE legacy_object_id IS NOT NULLfor idempotent backfill upserts. Theid_maptable records the ObjectId → UUID mapping centrally.- Money columns:
numeric(38,18)for fiat/crypto amounts throughout, exceptseller_offerswhich usesnumeric(18,8)per the Migration Guide. Blockchain balance columns usenumeric(78,0)to hold uint256 without overflow.- Polymorphic triples: the
ref_kindenum (entity|template) discriminator is expanded into three columns (_ref_kind,_id,_external_ref) with a CHECK constraint to enforce discriminator integrity. Used bypayments,funds_ledger_entries, andderived_destinations.- Soft delete:
addressesusesdeleted_at timestamptz(nullable) with partial-unique indexes scoped toWHERE deleted_at IS NULL. Most other tables retain the Mongostatusflag approach.- Timestamps: all timestamp columns declare
withTimezone: true.- Immutability:
funds_ledger_entrieshas 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. Theguardvalue 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:
- A buyer (
User) creates aPurchaseRequestwithstatus: 'pending'. - Sellers (other
Users) attachSellerOfferdocuments; the request transitions throughreceived_offers→in_negotiationas the parties chat in aChat. - The buyer accepts an offer; a
Paymentis opened against the Request Network provider and, once verified by webhook/reconciliation and safety checks, advances to a funded escrow state. IfORACLE_QUOTING_ENABLED=true, apayment_quoterow is written to PG at this point. - The seller marks the request
delivery→delivered; the buyer confirms with the 6-digitdeliveryCodeand the request becomescompleted. - The escrow
Paymentflips toreleasedafter a ledger-gated custody transfer instruction. Each ledger event appends an immutableFundsLedgerEntryrow (Mongo + PG). Optionally the buyer writes aReviewand earns aPointTransaction.
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 andbackend/src/db/schema/<File>.ts:<line>for Drizzle/PG.