Technical Documentation
Data Models
This document describes the entity schemas that underpin the Open Mandi exchange. Financial values are represented using decimal types in the data model to minimize floating-point precision issues.
Entity Relationship Overview
User 1──* Wallet
User 1──* Order
User 1──* Position
Order 1──* Trade (as maker or taker)
Wallet 1──* Transaction
Position 1──* TradeUser
Represents a registered account on the platform.
User {
id: UUID (primary key)
firebaseUid: string (unique, indexed — Firebase Auth identifier)
email: string (unique, indexed)
createdAt: timestamp
updatedAt: timestamp
// Authentication is handled by Firebase Auth; no passwords or password
// hashes are stored in Postgres.
}Wallet
Each user has two wallets: one for USDT and one for USDC. The balance is the total funds, while availableBalance excludes funds locked as margin or in pending orders.
Wallet {
id: UUID (primary key)
userId: UUID (foreign key → User)
currency: enum ("USDT" | "USDC")
balance: decimal (total balance)
availableBalance: decimal (balance - locked funds)
updatedAt: timestamp
UNIQUE(userId, currency)
}Invariant: availableBalance <= balance at all times.
Order
Represents a buy or sell order on any of the three order books (USDT/USDC, XAU, XAG).
Order {
id: UUID (primary key)
userId: UUID (foreign key → User)
pair: enum ("USDT-USDC" | "XAU-USD" | "XAG-USD")
side: enum ("buy" | "sell")
type: enum ("limit" | "market")
price: decimal? (null for market orders)
quantity: decimal (total order quantity)
filledQuantity: decimal (quantity already matched)
status: enum ("open" | "partial" | "filled" | "cancelled")
createdAt: timestamp
updatedAt: timestamp
INDEX(pair, status, price) -- for order book queries
INDEX(userId, status) -- for user's open orders
}Trade
Represents a single execution (fill) between a maker order and a taker order. One order can generate multiple trades (partial fills).
Trade {
id: UUID (primary key)
pair: enum ("USDT-USDC" | "XAU-USD" | "XAG-USD")
makerOrderId: UUID (foreign key → Order)
takerOrderId: UUID (foreign key → Order)
makerUserId: UUID (foreign key → User)
takerUserId: UUID (foreign key → User)
price: decimal (execution price)
quantity: decimal (executed quantity)
makerFee: decimal (fee charged to maker)
takerFee: decimal (fee charged to taker)
createdAt: timestamp
INDEX(pair, createdAt) -- for recent trades query
}Position
Represents an open futures position for a user.
Position {
id: UUID (primary key)
userId: UUID (foreign key → User)
contract: enum ("XAU-PERP" | "XAG-PERP")
side: enum ("long" | "short")
entryPrice: decimal (average entry price)
quantity: decimal (number of contracts)
margin: decimal (collateral locked)
collateralCurrency: enum ("USDT" | "USDC")
liquidationPrice: decimal (calculated)
realizedPnl: decimal (from partial closes)
status: enum ("open" | "closed" | "liquidated")
createdAt: timestamp
updatedAt: timestamp
INDEX(userId, status)
INDEX(contract, status) -- for open interest queries
}Transaction (Ledger)
Immutable record of every balance-affecting operation. Serves as the audit trail and enables double-entry bookkeeping verification.
Transaction {
id: UUID (primary key)
userId: UUID (foreign key → User)
walletId: UUID (foreign key → Wallet)
type: enum ("deposit" | "withdrawal" | "trade_debit" |
"trade_credit" | "fee" | "margin_lock" |
"margin_release" | "liquidation" | "funding")
currency: enum ("USDT" | "USDC")
amount: decimal (signed: positive for credits, negative for debits)
balanceAfter: decimal (wallet balance after this transaction)
referenceId: UUID? (links to Order, Trade, or Position)
referenceType: string? ("order" | "trade" | "position" | "deposit" | "withdrawal")
createdAt: timestamp
INDEX(userId, createdAt)
INDEX(walletId, createdAt)
INDEX(type, createdAt) -- for transparency aggregation
}Design Principles
- Decimal arithmetic — all monetary values use fixed-precision decimal types (e.g.,
DECIMAL(18,8)) to prevent floating-point errors - Immutable ledger — Transaction records are insert-only; corrections are made by adding compensating entries, never by modifying existing records
- Referential integrity — all foreign keys are enforced at the database level
- Soft deletes — users and orders use status fields rather than physical deletion
- Optimistic locking — wallet balance updates use version checks to prevent lost updates under concurrency