← back to Interiordesignershowroom
db/schema.sql
109 lines
-- interiordesignershowroom catalog schema
-- One normalized products table fed by 4 network adapters (CJ, Amazon, Rakuten, ShareASale).
-- The (network, external_id) pair is the natural key so re-ingests UPSERT instead of duplicating.
CREATE TABLE IF NOT EXISTS products (
id BIGSERIAL PRIMARY KEY,
network TEXT NOT NULL, -- cj | amazon | rakuten | shareasale
advertiser TEXT, -- merchant/brand the link points at (Wayfair, etc.)
external_id TEXT NOT NULL, -- network product id / ASIN / SKU
title TEXT NOT NULL,
description TEXT,
brand TEXT,
category TEXT, -- vendor category (raw)
room TEXT, -- OUR taxonomy: living-room, bedroom, ...
style TEXT, -- OUR taxonomy: modern, traditional, ...
color TEXT, -- OUR taxonomy: neutral, blue, green, ...
price NUMERIC(12,2),
sale_price NUMERIC(12,2),
currency TEXT DEFAULT 'USD',
image_url TEXT,
affiliate_url TEXT NOT NULL, -- the tracked deep link (the whole point)
in_stock BOOLEAN DEFAULT TRUE,
featured BOOLEAN DEFAULT FALSE, -- hand-picked hero pieces
suppressed BOOLEAN NOT NULL DEFAULT FALSE, -- admin "hide this product/brand" flag (kept in DB, hidden from every browse surface). Distinct from affiliate_settings (source-level).
price_checked_at TIMESTAMPTZ, -- Amazon TOS: never show stale prices
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (network, external_id)
);
-- ── Idempotent column migrations (products) ──────────────────────────────────
-- CREATE TABLE IF NOT EXISTS is a no-op on an existing table, so it will NOT add
-- a column introduced after that table's genesis. Every later-added column MUST
-- get an explicit ADD COLUMN IF NOT EXISTS guard HERE — before the indexes that
-- depend on it — or code reading it 500s on any DB provisioned before the column
-- existed (exactly what broke /shop on prod 2026-08-03: `suppressed` was code-side
-- only). These guards run first so idx_products_suppressed below always resolves.
ALTER TABLE products ADD COLUMN IF NOT EXISTS suppressed BOOLEAN NOT NULL DEFAULT FALSE;
CREATE INDEX IF NOT EXISTS idx_products_room ON products (room);
CREATE INDEX IF NOT EXISTS idx_products_style ON products (style);
CREATE INDEX IF NOT EXISTS idx_products_color ON products (color);
CREATE INDEX IF NOT EXISTS idx_products_created ON products (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_products_suppressed ON products (suppressed) WHERE suppressed = TRUE;
-- Editorial buying guides / shop-the-look articles (the SEO + helpful-content layer).
CREATE TABLE IF NOT EXISTS guides (
id BIGSERIAL PRIMARY KEY,
slug TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
dek TEXT, -- subtitle / summary
hero_image TEXT,
body_md TEXT, -- markdown body
product_ids BIGINT[] DEFAULT '{}', -- featured products in this guide
room TEXT,
style TEXT,
published BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Affiliate click log — powers "what's converting" analytics + Amazon compliance audit trail.
CREATE TABLE IF NOT EXISTS clicks (
id BIGSERIAL PRIMARY KEY,
product_id BIGINT REFERENCES products(id) ON DELETE SET NULL,
network TEXT,
advertiser TEXT,
referer TEXT,
ua TEXT,
clicked_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_clicks_product ON clicks (product_id);
CREATE INDEX IF NOT EXISTS idx_clicks_time ON clicks (clicked_at DESC);
-- Affiliate on/off control. An "affiliate" here is a source of tracked links —
-- either a whole NETWORK (advertiser = '') or a specific ADVERTISER/merchant on
-- that network. A product is visible on the storefront only if NEITHER its network
-- nor its (network, advertiser) has been switched OFF here. ABSENCE = ENABLED, so a
-- newly-ingested advertiser is live by default and we only persist explicit OFFs.
CREATE TABLE IF NOT EXISTS affiliate_settings (
network TEXT NOT NULL,
advertiser TEXT NOT NULL DEFAULT '', -- '' = the whole network
enabled BOOLEAN NOT NULL DEFAULT TRUE,
updated_at TIMESTAMPTZ DEFAULT now(),
PRIMARY KEY (network, advertiser)
);
-- Only the disabled rows ever need scanning during a storefront query.
CREATE INDEX IF NOT EXISTS idx_affiliate_settings_off ON affiliate_settings (network, advertiser) WHERE enabled = FALSE;
-- Durable keyword/id suppression rules. Distinct from affiliate_settings (which hides
-- by SOURCE) and from the raw `suppressed` flag (a one-off hide): a rule here is a
-- persistent record of "any product whose title/brand/advertiser contains this keyword
-- (or whose id equals this) should be hidden." Adding a rule immediately flips
-- `suppressed=TRUE` on every current match; "Vacuum" re-applies every rule so products
-- ingested LATER that match an old keyword get suppressed too. Kept as data (not just a
-- column flip) so the admin can see, re-apply, and remove what was entered.
CREATE TABLE IF NOT EXISTS suppress_rules (
id BIGSERIAL PRIMARY KEY,
kind TEXT NOT NULL DEFAULT 'keyword' CHECK (kind IN ('keyword','id')),
value TEXT NOT NULL, -- the keyword text, or the product id as text
note TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (kind, value)
);
-- Note: per-column ADD COLUMN IF NOT EXISTS migration guards live inline with each
-- table above (see the products block), so running this whole file top-to-bottom is
-- a complete, safe, repeatable migration on a fresh OR a drifted database.