Overview
Twenty-four PostgreSQL tables via Prisma 7 (@prisma/adapter-pg), modeling one idea carefully: what the customer bought must never change when the catalog does. Variants, snapshots, soft deletes, Decimal money, and an append-only audit trail do the heavy lifting.
The Variant Model
Apparel needs combinations, not just products:
productsown identity: name, slug, brand, description, images.product_variantsown everything purchasable: SKU (unique), barcode, price, color, size, dimensions, status.inventoryis 1:1 per variant withquantity_on_hand,quantity_reserved, andreorder_level.
A black medium shirt and a blue large shirt are two variants of one product — each with its own price, stock, and photos.
Orders Are Immutable Snapshots
order_items copies at purchase time: unit price, discount percentage, product name/slug/SKU, and full variant attributes including dimensions. shipments copies the shipping address. Renaming a product or changing a price never rewrites history.
Why it matters
Invoices, returns, and audits all read order data as it was at purchase — not as the catalog looks today.
Identity: Two IDs Per Entity
Every major entity carries both:
| ID | Purpose |
|---|---|
Internal autoincrement Int | Joins and foreign keys |
public_id — prefixed nanoid (usr_, prd_, var_, ord_, pimg_, vimg_, cat_, adr_, ses_) | URLs and API responses |
Internal IDs never appear in the API. Enumeration by guessing sequential IDs is structurally prevented.
Conventions That Hold Everywhere
- Money:
Decimal(10,2)(12,2foroperating_expenses) computed with decimal math, serialized as fixed strings. No floating-point anywhere. - Soft deletes:
deleted_aton ten tables (users,user_addresses,products,product_variants,categories,coupons,reviews,order_items,payments,shipments); repositories always filter them out — nothing commerce-relevant is ever hard-deleted.audit_logsandcoupon_usagesare never deleted. - Audit trail:
audit_logsis append-only by contract ("rows are never updated or deleted") — actor, action, entity type/id, method/path/status, request body (sensitive keys redacted), previous values/diffs, IP, user agent. - P&L:
operating_expenses(rent/salaries/marketing/… withspent_atdate) alongside COGS to compute true net profit. - Indexes with intent: composite indexes for real query patterns (
[users_id, placed_at]on orders), dedicated index migrations added after query analysis; every FK column carries an index.
Auth & Security Tables
| Table | Keeps |
|---|---|
sessions | SHA-256(token + SESSION_SECRET) hashes per device, IP/user-agent/device_name/country/city, last_activity_at, 14-day idle timeout, revocation |
verification_tokens | Hashed email/email-change/phone-change tokens (24h for email, 10 min for phone OTP) — purpose + channel allow OTP and link rows to coexist |
password_reset_tokens | Link tokens + 6-digit OTPs (15 min TTL, 5-attempt lock, link invalidates prior tokens) |
coupon_usages | Per-order redemption records (orders_id unique) backing global/per-user limits + hard 410/409 guards |
audit_logs | Append-only trail: actor, action, entity, request body (redacted), prev/diff, status, IP/UA |
operating_expenses | P&L ledger: category enum, spent_at date, created_by_users_id (super_admin) |
Result
A schema where catalog can change freely, orders cannot, and every security-sensitive action leaves an immutable trace.