Skip to content

Prisma models

The canonical source is prisma/schema.prisma. This page is the human-readable reference. For the why behind the shape, see Architecture → Data model.

Enum: Category

CPU GPU MOTHERBOARD RAM PSU CASE COOLER STORAGE LAPTOP PREBUILT

Used as Product.category. LAPTOP and PREBUILT are reserved for #24 and #25; not in active use in M1.

Retailer

Field Type Notes
id String (uuid) Primary key
name String (unique) Human-readable retailer name (e.g., "PCAndParts")
slug String (unique) URL slug (e.g., pcandparts)
domain String Canonical domain (e.g., pcandparts.com)
scraperId String (unique) Matches the file in src/scrapers/sites/, e.g., pcandparts for pcandparts.ts
active Boolean (default true) Soft on/off switch — inactive retailers are skipped by the scraper runner
createdAt DateTime Set on insert
updatedAt DateTime Set by Prisma on update

Relations: listings: Listing[], clicks: Click[].

Product

The canonical SKU. Independent of any retailer.

Field Type Notes
id String (uuid) Primary key
category Category (enum) One of the values above
brand String E.g., AMD, NVIDIA, Corsair
model String E.g., Ryzen 7 7800X3D, RM850x
slug String (unique) URL slug, e.g., amd-ryzen-7-7800x3d
cpuSocket String? E.g., AM5, LGA1700. Used for compat checks.
ramType String? E.g., DDR4, DDR5
ramSpeedMhz Int? E.g., 6000
formFactor String? E.g., ATX, mATX, ITX
tdpWatts Int? Used for PSU wattage compat check
lengthMm Int? GPU length, used for case clearance check
heightMm Int? Cooler height, used for case max-cooler-height check
psuWattage Int? For PSU products
specs Json (default {}) Free-form spec blob — anything not promoted to a typed column
imageUrl String? Backfilled by the scraper pipeline; first scraper to find an image wins
releaseDate DateTime? Optional, for sorting by recency
msrpUsd Decimal(10,2)? Manufacturer's suggested retail; reference only
createdAt DateTime
updatedAt DateTime

Indexes: category, (category, cpuSocket), (brand, model).

Relations: listings: Listing[].

Listing

One retailer's offer for a Product (or an unmatched scrape).

Field Type Notes
id String (uuid) Primary key
retailerId String FK → Retailer
productId String? FK → Product. Nullable — set only after matcher resolves
url String Full URL to the retailer's product page
retailerSku String? Retailer's own SKU, when available
titleRaw String Original listing title as scraped — used for matcher debugging and re-runs
matchConfidence Float? 0-1, set by src/lib/matching.ts
matchStatus String (default "unmatched") One of unmatched, weak, matched, manual
lastSeenAt DateTime (default now) Updated on every scrape — drives staleness detection
createdAt DateTime
updatedAt DateTime

Constraints: @@unique([retailerId, url]) — one Listing per retailer URL, ever.

Indexes: productId, matchStatus.

Relations: retailer: Retailer, product: Product?, prices: ListingPrice[], clicks: Click[].

ListingPrice

Append-only price snapshot. New row per scrape.

Field Type Notes
id String (uuid) Primary key
listingId String FK → Listing, cascades on delete
priceUsd Decimal(10,2)? Null = "Quote-only" / no public price (common on 961Souq)
inStock Boolean (default true) Drives the Listing's effective Active / OutOfStock state
scrapedAt DateTime (default now) The moment the price snapshot was taken

Indexes: (listingId, scrapedAt DESC) — supports "latest price" queries.

Append-only invariant: never updated, only inserted. The history is the source of truth. See Listing lifecycle.

Click

Outbound deep-link tracking. Written when a user clicks "Buy" on a Listing (UC-2).

Field Type Notes
id String (uuid) Primary key
clickToken String (unique) Random token, used for affiliate postback (#17)
retailerId String FK → Retailer
listingId String FK → Listing
source String Where the click originated: product-detail, build-cart, etc.
ipHash String? Salted hash of source IP, for affiliate fraud detection. Salt = IP_HASH_SECRET env var
ua String? User-Agent, for analytics
referrer String? HTTP Referer, where available
createdAt DateTime

Indexes: (retailerId, createdAt DESC), (listingId, createdAt DESC).

Notable absences

These don't exist yet — they're part of M2/M3 scope:

  • Build — saved PC build. Cookie-persisted for Visitors, account-bound for Builders. Composes 8 nullable Product references. See ADR-0003, #12.
  • User — added with Auth.js (#11).
  • Subscription — for UC-G price drop alerts (#14).
  • Conversion — written when an S2S postback redeems a Click (#17).