9462423642
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>
49 lines
2.4 KiB
SQL
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)
|
|
);
|