diff --git a/specs/SD-0001-mvp-sign-up-and-single-storefront.md b/specs/SD-0001-mvp-sign-up-and-single-storefront.md index 00aaa7f..1acd18d 100644 --- a/specs/SD-0001-mvp-sign-up-and-single-storefront.md +++ b/specs/SD-0001-mvp-sign-up-and-single-storefront.md @@ -683,7 +683,128 @@ adding fields is free, renaming/removing is a spec change (this document, ### 6.5 Per–Product-Use-Case design - +#### Entry routing (the shared spine: PUC-1, PUC-5, PUC-6) + +One client-side rule, fed by one server answer, decides where any arrival +lands. The server's answer is `GET /api/auth/me` (or the `verify` response, +which carries the same shape): + +| Session | Storefront | Lands on | +| --- | --- | --- | +| none | — | Landing (PUC-1) | +| valid | `null` | Create storefront (PUC-5) | +| valid | present | Admin shell (PUC-6) | + +The rule is exhaustive — no state lands nowhere (BUC-4's "never stranded"; +OHM: Duty of Care). The SPA router applies it on load and after `verify`. + +#### PUC-2 / PUC-3 — Sign up & log in (one flow; realizes BUC-1, BUC-2) + +```mermaid +sequenceDiagram + actor V as Visitor + participant SPA as Admin SPA + participant BFF as REST BFF + participant ACC as accounts + participant M as mailer (port) + participant DB as SQLite + + V->>SPA: enter email, "Send code" + SPA->>BFF: POST /api/auth/request-code {email} + BFF->>ACC: request_code(email) + ACC->>DB: upsert auth_code (hash, expiry; cooldown check) + ACC->>M: send(email, "Your ecomm code: …") + ACC-->>BFF: ok (uniform, new-or-known) + BFF-->>SPA: 204 → code-entry step + V->>SPA: enter code, "Continue" + SPA->>BFF: POST /api/auth/verify {email, code} + BFF->>ACC: verify(email, code) + ACC->>DB: check hash, expiry, attempts; consume (INV-3) + ACC->>DB: get-or-create account by normalized email (INV-2) + ACC-->>BFF: account, created? + BFF->>SF: storefront_for(account) — via domains, for the routing answer + BFF-->>SPA: 200 {account, storefront|null, created} + session cookie + SPA-->>V: honest welcome → entry routing +``` + +- **Implementation:** `request_code` is uniform for new vs known emails (no + enumeration, §6.6) and enforces the 60s cooldown (INV-3 → PUC-2c). + `verify` consumes the code transactionally — increment attempts on + mismatch (PUC-2a), refuse expired (PUC-2b), invalidate at 5 (INV-3) — and + only then gets-or-creates the account (INV-2). Account creation *is* email + verification: the address provably received the code. Honors INV-1 (works + on an empty DB: the first verify creates account #1), INV-6, INV-9. + +#### PUC-4 / PUC-7 — Create the storefront (realizes BUC-3, BUC-3a) + +```mermaid +sequenceDiagram + actor Mer as Merchant + participant SPA as Admin SPA + participant BFF as REST BFF + participant SF as storefronts + participant DB as SQLite + + Mer->>SPA: name (optional), "Create storefront" + SPA->>BFF: POST /api/storefronts {name?} + BFF->>SF: create_storefront(account, name?) + SF->>DB: any membership for account? (INV-4 guard) + alt already owns one + SF-->>BFF: refuse + BFF-->>SPA: 409 already_owns_storefront (PUC-7, honest copy) + else none yet + SF->>DB: insert storefront (name or generated default) + membership(owner) + SF-->>BFF: storefront + BFF-->>SPA: 201 {id, name} + SPA-->>Mer: → admin shell (PUC-6) + end +``` + +- **Implementation:** guard + insert run in one transaction (two concurrent + creates cannot both pass the guard). Default naming per §6.3. No payment, + plan, or extra field exists to ask for (corpus 14.01.0015/0016). Honors + INV-4, INV-5, INV-6. + +#### PUC-8 — Admin shell (realizes BUC-4) + +No new backend surface: the shell renders from `GET /api/auth/me` +(storefront name + account email) and is otherwise static honest-empty copy +(§5.4). Honors INV-9 — it claims nothing that doesn't exist. + +#### PUC-9 — Sign out + +`POST /api/auth/logout` clears the session cookie; SPA returns to Landing. +Idempotent; no server state to destroy (sessions are cookie-borne, §6.2). + +#### PUC-10 / PUC-11 — Bootstrap (realizes BUC-5, BUC-5a) + +```mermaid +sequenceDiagram + participant Op as Operator / Developer + participant App as ecomm process + participant DB as SQLite (empty file or absent) + actor Mer as First merchant + + Op->>App: start (dev script · flotilla deploy) + App->>DB: open/create; apply pending migrations in order (INV-7) + App-->>Op: /healthz 200 (migrations current) + Mer->>App: sign up → create storefront → admin (the ordinary PUC-2/4/6 flows) + Note over App,Mer: first rows created through the product alone (INV-1) +``` + +- **Implementation:** identical in all three environments; only configuration + differs (INV-8): **localhost** — `scripts/dev.sh` from a clean checkout + (venv + deps, backend on :8000, Vite on :5173 proxying `/api`); mailer is + `LogMailer`, so the one-time code appears in the backend log (the "local + dev channel" of PUC-10 — auth itself is not stubbed; the same issue/verify + path runs). **PPE/Prod** — provisioned and deployed through + flotilla/launch-app (operator gesture, handbook §8.5); mailer is + `SmtpMailer` with relay coordinates and secrets resolved from Secret + Manager via deployment config; migrations apply at startup under + flotilla's fail-stop deploy, gated by `/healthz`. The bring-up runbook + (per-environment: prerequisites, the one gesture, how to watch `/healthz`) + ships in-repo as `docs/BOOTSTRAP.md` — the documented gesture BUC-5 + demands. ### 6.6 Non-functional requirements & cross-cutting concerns