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,14 +1,108 @@
|
||||
---
|
||||
title: User
|
||||
tags: [data-model, mongoose]
|
||||
tags: [data-model, mongoose, postgres, dual-write]
|
||||
aliases: [User Model, IUser, Account]
|
||||
---
|
||||
|
||||
# User
|
||||
|
||||
> **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, `guard` role (migration 0017), dual-write status. Previous update: 2026-05-29 (see [Doc vs Code Audit Report](../09%20-%20Audits/Doc%20vs%20Code%20Audit%20Report%20-%202026-05-29.md))
|
||||
|
||||
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` reference back to `User`, so this collection is the relational hub of the system.
|
||||
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: **`users`** — `backend/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
|
||||
@@ -20,7 +114,7 @@ The core identity document for every actor in the marketplace: buyers, sellers,
|
||||
> [!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
|
||||
### Schema
|
||||
|
||||
| Field | Type | Required | Default | Validation | Index | Description |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
@@ -28,8 +122,8 @@ The core identity document for every actor in the marketplace: buyers, sellers,
|
||||
| `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` | yes | Authorisation tier. `resolver` was added in commit `fce8a19` — can view and resolve disputes, and bypass chat membership checks, but has no other admin privileges. |
|
||||
| `isEmailVerified` | Boolean | no | `false` | — | — | Set to true after the email verification code is consumed. ⚠️ Changing the email via `PUT /api/user/profile` **resets this to `false`** and dispatches a fresh **6-digit** verification code to the new address (see Email verification note below). |
|
||||
| `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. |
|
||||
@@ -39,7 +133,7 @@ The core identity document for every actor in the marketplace: buyers, sellers,
|
||||
| `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 (see below). |
|
||||
| `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. |
|
||||
@@ -49,7 +143,7 @@ The core identity document for every actor in the marketplace: buyers, sellers,
|
||||
| `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.street` | String | no | — | — | — | Inline address (separate from Address book). |
|
||||
| `profile.address.city` | String | no | — | — | — | — |
|
||||
| `profile.address.state` | String | no | — | — | — | — |
|
||||
| `profile.address.zipCode` | String | no | — | — | — | — |
|
||||
@@ -69,26 +163,26 @@ The core identity document for every actor in the marketplace: buyers, sellers,
|
||||
| `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. ⚠️ Reset to `[]` on password change and on password reset, which invalidates every outstanding session and forces re-login everywhere. |
|
||||
| `referralCode` | String | no | — | — | unique, sparse | **Not yet implemented** in `User.ts` — planned for referral programme. |
|
||||
| `referredBy` | ObjectId → User | no | — | — | yes | **Not yet implemented** in `User.ts` — planned for referral programme. |
|
||||
| `points.total` | Number | no | `0` | — | — | **Not yet implemented** in `User.ts` — planned for loyalty system. |
|
||||
| `points.available` | Number | no | `0` | — | — | **Not yet implemented** in `User.ts`. |
|
||||
| `points.used` | Number | no | `0` | — | — | **Not yet implemented** in `User.ts`. |
|
||||
| `points.level` | Number | no | `1` | — | yes (`points.level`) | **Not yet implemented** in `User.ts` — planned for [[LevelConfig]] lookup. |
|
||||
| `referralStats.totalReferrals` | Number | no | `0` | — | — | **Not yet implemented** in `User.ts`. |
|
||||
| `referralStats.activeReferrals` | Number | no | `0` | — | — | **Not yet implemented** in `User.ts`. |
|
||||
| `referralStats.totalEarned` | Number | no | `0` | — | — | **Not yet implemented** in `User.ts`. |
|
||||
| `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
|
||||
### Virtuals
|
||||
|
||||
| Virtual | Returns | Definition |
|
||||
| --- | --- | --- |
|
||||
| `fullName` | `${firstName} ${lastName}` | `backend/src/models/User.ts:238` |
|
||||
|
||||
## Indexes
|
||||
### Indexes (MongoDB)
|
||||
|
||||
Defined explicitly:
|
||||
|
||||
@@ -97,27 +191,44 @@ Defined explicitly:
|
||||
- `{ status: 1 }` — `backend/src/models/User.ts:179`
|
||||
- `{ authProvider: 1 }` — supports provider-level account reporting and cleanup.
|
||||
|
||||
> [!warning] Missing indexes
|
||||
> 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`:
|
||||
> [!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
|
||||
### Pre/Post Hooks
|
||||
|
||||
None declared at the schema level.
|
||||
|
||||
## Instance Methods
|
||||
### 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
|
||||
### 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`).
|
||||
- **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`).
|
||||
- **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
|
||||
|
||||
@@ -134,20 +245,31 @@ stateDiagram-v2
|
||||
## Common Queries
|
||||
|
||||
```ts
|
||||
// Find by email (login)
|
||||
// Mongo — Find by email (login)
|
||||
User.findOne({ email: email.toLowerCase() });
|
||||
|
||||
// Active sellers
|
||||
// Mongo — Active sellers
|
||||
User.find({ role: 'seller', status: 'active' });
|
||||
|
||||
// Validate referral
|
||||
// Mongo — Validate referral
|
||||
User.findOne({ referralCode: code });
|
||||
|
||||
// Leaderboard by points
|
||||
// Mongo — Leaderboard by points
|
||||
User.find({ status: 'active' }).sort({ 'points.total': -1 }).limit(10);
|
||||
|
||||
// Promote level
|
||||
// Mongo — Promote level
|
||||
User.updateOne({ _id: id }, { $set: { 'points.level': newLevel } });
|
||||
```
|
||||
|
||||
Related: [[TempVerification]], [[LevelConfig]], [[PointTransaction]], [[ShopSettings]].
|
||||
```sql
|
||||
-- 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.
|
||||
|
||||
Reference in New Issue
Block a user