Specification
3. Database Schema

3. Database Schema

Nine custom tables extending Medusa v2's internal data model.

All primary keys are UUIDs. Price fields are stored as integers in cents (AUD) to avoid floating-point issues. All tables use soft deletes (deleted_at) where applicable.

3.1 Tables Overview

TablePurposeKey Relations
customersStaff/customer accounts with auth fieldsRoot entity
productsWallpacks, t-shirts, digital items→ files (1:1), → order_items (1:N)
ordersPurchase records with payment/fulfilment status→ customers, → order_items
order_itemsLine items with price snapshot at purchase→ orders, → products
filesZIP file metadata stored in R2→ products (1:1)
download_linksSecure download tokens per order item→ customers, orders, products, files
invite_tokensStaff invite system→ customers (invited_by)
promo_codesDiscount codes with usage limits→ orders (1:N)
promo_code_usesAudit log of promo code usage→ promo_codes, customers, orders

3.2 Table Definitions

customers

id                    UUID PRIMARY KEY
email                 VARCHAR(255) UNIQUE NOT NULL
password_hash         TEXT NOT NULL
first_name            VARCHAR(50)
last_name             VARCHAR(50)
phone                 VARCHAR(20)
status                ENUM('active', 'inactive', 'suspended')
failed_login_attempts INTEGER DEFAULT 0
locked_until          TIMESTAMP
deleted_at            TIMESTAMP
anonymised_at         TIMESTAMP
created_at            TIMESTAMP NOT NULL
updated_at            TIMESTAMP NOT NULL

products

id              UUID PRIMARY KEY
title           VARCHAR(255) NOT NULL
description     TEXT
sku             VARCHAR(100) UNIQUE
price           INTEGER NOT NULL  -- cents AUD
thumbnail       TEXT
status          ENUM('draft', 'published', 'archived')
type            ENUM('digital', 'physical')
collection_id   UUID REFERENCES collections(id)
download_count  INTEGER DEFAULT 0
created_by      UUID REFERENCES customers(id)
deleted_at      TIMESTAMP
created_at      TIMESTAMP NOT NULL
updated_at      TIMESTAMP NOT NULL

orders

id                  UUID PRIMARY KEY
invoice_number      VARCHAR(20) UNIQUE NOT NULL
customer_id         UUID REFERENCES customers(id) NOT NULL
promo_code_id       UUID REFERENCES promo_codes(id)
status              ENUM('pending', 'completed', 'cancelled', 'refunded')
payment_status      ENUM('awaiting', 'captured', 'refunded', 'failed')
fulfillment_status  ENUM('not_fulfilled', 'fulfilled', 'partially_fulfilled')
currency            VARCHAR(3) DEFAULT 'AUD'
subtotal            INTEGER NOT NULL
discount_total      INTEGER DEFAULT 0
tax_total           INTEGER DEFAULT 0
total               INTEGER NOT NULL
ip_address          VARCHAR(45)
created_at          TIMESTAMP NOT NULL
updated_at          TIMESTAMP NOT NULL

order_items

id                UUID PRIMARY KEY
order_id          UUID REFERENCES orders(id) NOT NULL
product_id        UUID REFERENCES products(id) NOT NULL
quantity          INTEGER NOT NULL DEFAULT 1
unit_price        INTEGER NOT NULL  -- snapshot at purchase
product_name      VARCHAR(255) NOT NULL  -- snapshot
product_thumbnail TEXT  -- snapshot
subtotal          INTEGER NOT NULL
created_at        TIMESTAMP NOT NULL

files

id          UUID PRIMARY KEY
product_id  UUID REFERENCES products(id) UNIQUE NOT NULL
filename    VARCHAR(255) NOT NULL
storage_key TEXT NOT NULL  -- R2 object key
size_bytes  BIGINT NOT NULL
mime_type   VARCHAR(100) NOT NULL
checksum    VARCHAR(255)
uploaded_by UUID REFERENCES customers(id)
created_at  TIMESTAMP NOT NULL
updated_at  TIMESTAMP NOT NULL

download_links

id                  UUID PRIMARY KEY
customer_id         UUID REFERENCES customers(id) NOT NULL
order_id            UUID REFERENCES orders(id) NOT NULL
product_id          UUID REFERENCES products(id) NOT NULL
file_id             UUID REFERENCES files(id) NOT NULL
token               UUID UNIQUE NOT NULL
status              ENUM('active', 'expired', 'exhausted', 'revoked')
download_count      INTEGER DEFAULT 0
max_downloads       INTEGER DEFAULT 3
expires_at          TIMESTAMP NOT NULL  -- 30 days from creation
last_downloaded_at  TIMESTAMP
created_at          TIMESTAMP NOT NULL
updated_at          TIMESTAMP NOT NULL

invite_tokens

id          UUID PRIMARY KEY
email       VARCHAR(255) NOT NULL
token       UUID UNIQUE NOT NULL
status      ENUM('pending', 'accepted', 'expired', 'revoked')
invited_by  UUID REFERENCES customers(id) NOT NULL
expires_at  TIMESTAMP NOT NULL  -- 30 days
accepted_at TIMESTAMP
created_at  TIMESTAMP NOT NULL
updated_at  TIMESTAMP NOT NULL

promo_codes

id              UUID PRIMARY KEY
code            VARCHAR(50) UNIQUE NOT NULL
discount_type   ENUM('percentage', 'fixed')
discount_value  INTEGER NOT NULL
min_order_value INTEGER DEFAULT 0
max_uses        INTEGER
uses_count      INTEGER DEFAULT 0
is_active       BOOLEAN DEFAULT true
expires_at      TIMESTAMP
created_by      UUID REFERENCES customers(id)
created_at      TIMESTAMP NOT NULL
updated_at      TIMESTAMP NOT NULL

promo_code_uses

id              UUID PRIMARY KEY
promo_code_id   UUID REFERENCES promo_codes(id) NOT NULL
customer_id     UUID REFERENCES customers(id) NOT NULL
order_id        UUID REFERENCES orders(id) NOT NULL
discount_amount INTEGER NOT NULL
used_at         TIMESTAMP NOT NULL

3.3 Database Indexes

TableColumnReason
customersemailLogin lookup — unique constraint
orderscustomer_idOrder history queries
download_linkstokenDownload validation — unique constraint
download_linkscustomer_idAccount download list
invite_tokenstokenInvite validation — unique constraint
productsstatusFilter published products

3.4 Key Design Decisions

  • Price snapshots on order_itemsunit_price, product_name, product_thumbnail are copied at purchase time. Product price changes don't affect past orders.
  • Brute force fields on customersfailed_login_attempts + locked_until enforced at the application layer. 5 failures → 30 min lockout.
  • Download tokencrypto.randomUUID(), never user-controlled, never sequential. Expiry and max downloads enforced on every request.