Files
wiggleverse-ecomm/backend/migrations/0001_init.sql
T
ben.stull 9462423642 feat(slice-1): platform/db migration runner + migration 0001 (INV-7, §6.3 schema)
Forward-only, fail-stop, advisory-lock-guarded psycopg runner; migration 0001 lays
the account/auth_code/storefront/membership schema. Tests prove migrate-from-empty
and idempotent re-migrate (INV-1 partial) and the INV-4 many-capable shape.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 08:37:09 -07:00

49 lines
2.4 KiB
SQL

-- 0001_init.sql — SD-0001 §6.3 data model. Forward-only and fail-stop (INV-7):
-- do NOT edit this file once it has merged/applied; add a new numbered migration.
-- account — a person's identity on the platform. Email is the canonical key (INV-2);
-- the address is stored already-normalized (lowercased) by the service layer, so a
-- plain unique index enforces one-account-per-email. Data minimization (OHM): email
-- and nothing else (§6.3).
CREATE TABLE account (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX account_email_key ON account (email); -- INV-2
-- auth_code — one-time codes, handled like secrets (INV-3): only the hash is stored;
-- keyed by email (the code is issued before the account may exist — sign-up and
-- log-in converge, PUC-3).
CREATE TABLE auth_code (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email TEXT NOT NULL,
code_hash TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
consumed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX auth_code_email_idx ON auth_code (email);
-- storefront — a merchant's business presence. Name is always populated (a blank
-- entry gets a generated default at the service layer, §6.3); "unnamed" is never NULL.
CREATE TABLE storefront (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- storefront_membership — account <-> storefront ownership. INV-4: deliberately a
-- many-to-many shape (composite PK, no UNIQUE(account_id)) so many-storefronts-per-
-- account stays open; the one-storefront rule is a deletable service-layer guard, not
-- a schema law. INV-5: every storefront-scoped row carries its storefront_id. `role`
-- is a single-value enum today; it is the seam the staff/permissions model attaches to.
CREATE TABLE storefront_membership (
account_id BIGINT NOT NULL REFERENCES account (id),
storefront_id BIGINT NOT NULL REFERENCES storefront (id),
role TEXT NOT NULL DEFAULT 'owner' CHECK (role IN ('owner')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (account_id, storefront_id)
);