Files
nick-doc/02 - Data Models/User.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

16 KiB

title, tags, aliases
title tags aliases
User
data-model
mongoose
postgres
dual-write
User Model
IUser
Account

User

Last updated: 2026-06-03 — added Postgres/Drizzle schema, guard role (migration 0017), dual-write status. Previous update: 2026-05-29 (see Doc vs Code Audit Report)

The core identity document for every actor in the marketplace: buyers, sellers, and admins. Stores credentials (password + WebAuthn passkeys), profile/preference data, referral bookkeeping, point balances, and a soft-delete status flag. Almost every other model carries an ObjectId (Mongo) or uuid (Postgres) reference back to User, so this collection is the relational hub of the system.

[!info] Migration status: DUAL-WRITE Writes go to both MongoDB (User collection) and Postgres (users table) via DualWriteUserRepo. Reads still come from MongoDB — PG reads are not yet enabled. Repositories: DrizzleUserRepo, MongoUserRepo, DualWriteUserRepo Postgres table: usersbackend/src/db/schema/users.ts


Postgres Table: users

[!note] Source backend/src/db/schema/users.ts

Columns

Column PG Type Nullable Default Notes
id uuid no gen_random_uuid() Primary key
legacy_object_id text yes Mongo ObjectId; partial-unique index WHERE NOT NULL; used for idempotent backfill upserts
email varchar(255) yes Partial-unique index WHERE NOT NULL
password varchar(255) yes Hashed
first_name text yes
last_name text yes
role user_role enum no buyer Values: admin, buyer, seller, resolver, guard (added migration 0017)
is_email_verified bool yes false
auth_provider auth_provider enum no email Values: email, google, telegram
telegram_verified bool yes false
email_verification_token text yes Legacy token flow
email_verification_code text yes OTP code
email_verification_code_expires timestamptz yes
password_reset_token text yes
password_reset_expires timestamptz yes
password_reset_code text yes
password_reset_code_expires timestamptz yes
profile jsonb yes Stores avatar, photoURL, phone, address, bio, website, walletAddress, walletType, walletProvider, walletProofVerified, walletProofTimestamp, isPublic
preferences jsonb yes Stores language, currency, notifications.{email,sms,push}
status user_status enum yes active Values: active, suspended, deleted
last_login_at timestamptz yes
referral_code varchar(255) yes Partial-unique index
referred_by_id uuid yes Self-FK → users(id); index
points_total int yes 0
points_available int yes 0
points_used int yes 0
points_level int yes 1 Indexed
referral_stats_total int yes 0
referral_stats_active int yes 0
referral_stats_total_earned int yes 0
created_at timestamptz no now()
updated_at timestamptz no now()

Child Tables

user_passkeys — WebAuthn credentials extracted from the embedded array:

Column Type Notes
id text (PK) WebAuthn credential ID
user_id uuid FK→users CASCADE Owner
public_key text Stored public key
counter int Signature counter
device_type passkey_device_type enum platform / cross-platform
device_name text Optional human label
created_at timestamptz

user_refresh_tokens — Active JWT refresh tokens extracted from the Mongo array:

Column Type Notes
token text (PK) The refresh token string
user_id uuid FK→users CASCADE Owner

Indexes (Postgres)

Index Type Condition
users_email_unique partial-unique WHERE email IS NOT NULL
users_referral_code_unique partial-unique WHERE referral_code IS NOT NULL
users_legacy_object_id_unique partial-unique WHERE legacy_object_id IS NOT NULL
users_role_idx btree
users_status_idx btree
users_auth_provider_idx btree
users_referral_code_idx btree
users_referred_by_id_idx btree
users_points_level_idx btree

Relations

  • Self-referential: referred_by_id → users.id (parent/children for referral tree)
  • One-to-many: user_passkeys.user_id, user_refresh_tokens.user_id

MongoDB Collection: User (legacy — reads still active)

[!note] Source backend/src/models/User.ts:70 — schema definition backend/src/models/User.ts:257 — model export

[!note] Email change re-verification When a profile update (PUT /api/user/profile, userController.updateUserProfile) changes email to a new value, the controller sets isEmailVerified = false, generates a 6-digit emailVerificationCode (valid 15 minutes), stores it on emailVerificationCode / emailVerificationCodeExpires, and emails the code to the new address. The user must then confirm via POST /api/user/profile/email/verify (or request a new code with POST /api/user/profile/email/resend-verification).

[!note] Wallet ownership proof PATCH /api/user/wallet-address accepts both EVM and TON wallets. EVM addresses require an EIP-191 signature (ethers.verifyMessage); TON addresses are format-validated and may include an optional TonProof. A successful proof sets profile.walletProofVerified = true and profile.walletProofTimestamp.

Schema

Field Type Required Default Validation Index Description
email String no lowercase, trim unique, sparse Primary email login identifier. Nullable for Telegram-only accounts.
password String no minlength 6 Hashed password. Optional to support passkey-only, Google, and Telegram accounts.
firstName String no "کاربر" trim Persian default ("user").
lastName String no "جدید" trim Persian default ("new").
role String yes "buyer" enum: admin / buyer / seller / resolver / guard yes Authorisation tier. resolver (commit fce8a19): can view/resolve disputes and bypass chat membership checks. guard (migration 0017): added in PG schema; purpose TBD.
isEmailVerified Boolean no false Set to true after the email verification code is consumed. Warning: Changing the email via PUT /api/user/profile resets this to false and dispatches a fresh 6-digit verification code to the new address.
authProvider String yes "email" enum: email / google / telegram yes Provider used to create the account. Existing email/password accounts remain email; Telegram-only users are telegram.
telegramVerified Boolean no false Set when Telegram identity has been signature-verified and linked through TelegramLink.
emailVerificationToken String no Legacy token-based email verification.
emailVerificationCode String no OTP code for email verification.
emailVerificationCodeExpires Date no Expiry for emailVerificationCode.
passwordResetToken String no Token for reset link flow.
passwordResetExpires Date no Expiry of passwordResetToken.
passwordResetCode String no OTP reset code.
passwordResetCodeExpires Date no Expiry for OTP reset code.
passkeys[] Subdocument array no [] WebAuthn credentials. Extracted to user_passkeys table in PG.
passkeys[].id String yes Credential ID.
passkeys[].publicKey String yes Stored public key.
passkeys[].counter Number yes 0 Signature counter.
passkeys[].deviceType String yes enum: platform / cross-platform Authenticator class.
passkeys[].deviceName String no Optional human label.
passkeys[].createdAt Date no Date.now Registration timestamp.
profile.avatar String no Avatar URL.
profile.photoURL String no Alternative photo URL.
profile.phone String no Contact phone.
profile.address.street String no Inline address (separate from Address book).
profile.address.city String no
profile.address.state String no
profile.address.zipCode String no
profile.address.country String no
profile.bio String no Free-form bio.
profile.website String no Personal website URL.
profile.walletAddress String no On-chain wallet address (EVM 0x… or TON). Set via PATCH /api/user/wallet-address.
profile.walletType String no enum: evm / ton Which chain family the stored walletAddress belongs to.
profile.walletProvider String no Wallet provider label (e.g. evm, telegram-wallet). Defaults to telegram-wallet for TON, evm otherwise.
profile.walletProofVerified Boolean no True when ownership was proven — EIP-191 signature for EVM, or a verified TonProof for TON.
profile.walletProofTimestamp Date no When the wallet proof was last verified (only set when walletProofVerified is true).
profile.isPublic Boolean no false Whether the profile is publicly visible.
preferences.language String no "en" UI language.
preferences.currency String no "USD" Display currency.
preferences.notifications.email Boolean no true Opt-in for email notifications.
preferences.notifications.sms Boolean no false Opt-in for SMS notifications.
preferences.notifications.push Boolean no true Opt-in for push notifications.
status String no "active" enum: active / suspended / deleted yes Soft-delete and moderation flag.
lastLoginAt Date no Updated by auth middleware.
refreshTokens[] String[] no [] Array of currently active JWT refresh tokens. Warning: Reset to [] on password change and on password reset, invalidating every outstanding session. Extracted to user_refresh_tokens table in PG.
referralCode String no unique, sparse Not yet implemented — planned for referral programme.
referredBy ObjectId -> User no yes Not yet implemented — planned for referral programme.
points.total Number no 0 Not yet implemented — planned for loyalty system.
points.available Number no 0 Not yet implemented.
points.used Number no 0 Not yet implemented.
points.level Number no 1 yes Not yet implemented — planned for LevelConfig lookup.
referralStats.totalReferrals Number no 0 Not yet implemented.
referralStats.activeReferrals Number no 0 Not yet implemented.
referralStats.totalEarned Number no 0 Not yet implemented.
createdAt Date auto Mongoose timestamp.
updatedAt Date auto Mongoose timestamp.

Virtuals

Virtual Returns Definition
fullName ${firstName} ${lastName} backend/src/models/User.ts:238

Indexes (MongoDB)

Defined explicitly:

  • { email: 1 } unique sparse — allows multiple Telegram-only users without email while preserving uniqueness for email-bearing users.
  • { role: 1 }backend/src/models/User.ts:178
  • { status: 1 }backend/src/models/User.ts:179
  • { authProvider: 1 } — supports provider-level account reporting and cleanup.

[!warning] Missing indexes in Mongo schema The schema currently defines only role and status indexes. The referralCode, referredBy, and points.level indexes documented below are not yet present in User.ts.

Pre/Post Hooks

None declared at the schema level.

Instance Methods

Signature Purpose
toJSON(): object Strips password, refreshTokens, all emailVerification* and passwordReset* fields before serialisation. Defined at backend/src/models/User.ts:243.

Static Methods

None defined on the schema.


Roles

Role Added Capabilities
admin original Full platform access
buyer original Place purchase requests, confirm delivery
seller original Submit offers, manage shop
resolver commit fce8a19 View/resolve disputes; bypass chat membership checks; no other admin privileges
guard migration 0017 (PG only) Purpose TBD — defined in user_role PG enum, not yet in Mongo schema

[!warning] Role enum drift The Postgres user_role enum includes guard; the Mongo schema enum does not. Until the Mongo schema is updated, any guard-role user created through PG will not be representable in Mongo and will break dual-write for that record.


Relationships

  • References: User (self, via referredBy / referred_by_id).
  • Referenced by: PurchaseRequest (buyerId, preferredSellerIds, deliveryInfo.deliveryCodeUsedBy, deliveryInfo.deliveryAttempts[].sellerId), SellerOffer (sellerId), Payment (buyerId, sellerId), Chat (participants[].userId, messages[].senderId, metadata.createdBy), Notification (userId as string), RequestTemplate (sellerId), Dispute (buyerId, sellerId, adminId), BlogPost (author.id), Address (userId), Review (sellerId, reviewerId), PointTransaction (user, referredUser), ShopSettings (sellerId).

State Transitions

stateDiagram-v2
    [*] --> active : signup verified
    active --> suspended : admin action
    suspended --> active : admin restore
    active --> deleted : self-delete
    suspended --> deleted : admin purge
    deleted --> [*]

Common Queries

// Mongo — Find by email (login)
User.findOne({ email: email.toLowerCase() });

// Mongo — Active sellers
User.find({ role: 'seller', status: 'active' });

// Mongo — Validate referral
User.findOne({ referralCode: code });

// Mongo — Leaderboard by points
User.find({ status: 'active' }).sort({ 'points.total': -1 }).limit(10);

// Mongo — Promote level
User.updateOne({ _id: id }, { $set: { 'points.level': newLevel } });
-- PG — Find by email
SELECT * FROM users WHERE email = lower($1) AND email IS NOT NULL;

-- PG — Active sellers
SELECT * FROM users WHERE role = 'seller' AND status = 'active';

-- PG — Leaderboard by points
SELECT * FROM users WHERE status = 'active' ORDER BY points_total DESC LIMIT 10;

Related: TempVerification, LevelConfig, PointTransaction, ShopSettings.