SD-0001: Solution Design — MVP sign up and create a single storefront (Feature #1) #2

Merged
ben.stull merged 12 commits from claude/optimistic-lumiere-ef8b1e into main 2026-06-10 14:12:57 +00:00
Showing only changes of commit b2e1f11a85 - Show all commits
@@ -808,19 +808,97 @@ sequenceDiagram
### 6.6 Non-functional requirements & cross-cutting concerns
<!-- §6.6 pending -->
- **Security & privacy:**
- Authn: email + one-time code per INV-3; sessions in signed, HTTP-only,
`SameSite=Lax` cookies, `Secure` in deployed environments.
- Authz: MVP has one rule — you act only on your own storefront — enforced
in the domain layer (INV-6).
- No account enumeration: `request-code` answers uniformly for new and
known emails.
- Data classification: emails are personal data — the only personal data
held (§6.3); code hashes are short-lived secrets; both excluded from
logs in deployed environments.
- Secrets are references, never bytes (handbook §6.3): session secret and
SMTP credentials live in Secret Manager, named by deployment config
(INV-8).
- **Performance & scale:** MVP traffic is near-zero; the bar is honesty, not
throughput — interactive responses well under a second on the standard
single-VM deployment. SQLite/WAL with per-request connections is far above
this bar; revisit at real scale (§7.4 R-3).
- **Availability & resilience:** one VM, one process (standard flotilla
stack); deploys are fail-stop with `/healthz` gating; brief deploy-window
unavailability is accepted pre-v1.
- **Observability:** structured request logs (no emails/codes in deployed
logs); `/healthz` reporting migration currency; auth events (code issued /
verify ok / verify failed) logged with hashed identifiers — enough to see
the bootstrap rehearsal happen and debug delivery.
- **Accessibility:** the four screens are forms and text — semantic HTML,
labelled fields, keyboard-completable flows, visible focus; the code input
carries one-time-code semantics so password managers and OS autofill
cooperate.
### 6.7 Key decisions & alternatives considered
<!-- §6.7 pending -->
| Decision | Chosen | Alternatives considered | Why chosen |
| --- | --- | --- | --- |
| Overall shape | Carry forward the prototype's 4-layer modular monolith + React SPA | Heavier re-architecture (Postgres, services, SSR); resume prototype code; new stack | Proven by R01R07 against this exact domain; smallest bootstrap surface (§1.6 outcome 3); rebuild wants clean *code*, not unproven *shape* (§2) |
| Datastore | SQLite (WAL), no ORM, forward-only `.sql` migrations | Postgres (managed or on-VM) | Single-VM flotilla standard; zero standing service to provision per environment — bootstrap stays one gesture; prototype precedent. Revisit at scale (R-3) |
| Tenancy | One shared DB; tenant rows carry `storefront_id` (INV-5); ownership via membership relation | DB-per-storefront; hard 1-1 FK on account | Membership keeps the many-per-account door open (Feature #1 constraint) and is the staff-model seam (13.15.\*); DB-per-tenant multiplies the bootstrap story for no MVP gain |
| One-storefront rule | Service-layer guard + no UX affordance (INV-4) | UX-only; schema `UNIQUE(account_id)` | UX-only leaves the API dishonest (data could contradict the promise); schema-level closes the door Feature #1 says to keep open. The guard is one deletable check |
| Auth mechanism | Email + one-time code, passwordless | Passwords (+ reset/verification machinery); OAuth (Google/Apple) first | Corpus's primary path (14.01.00030004); prototype-proven; collapses the account-lifecycle question — no passwords to reset, verification inherent. OAuth deferred, addable under INV-2 |
| Admission | Open sign-up, no gate | Prototype's pending/granted invite gate | ecomm is a product, not an internal tool; a gate would hand-curate exactly what BUC-5 says must be flow-reachable. Explicit §1.7 non-goal |
| Bootstrap | No seed; empty DB is a working state (INV-1) | Prototype's seeded store/owner; seed scripts per environment | Seeding *is* hand-placed data — it's what Feature #1's acceptance forbids; INV-1 makes the bootstrap story a property of the app, not of tooling |
| API surface | REST BFF only; GraphQL deferred | Prototype's dual REST + GraphQL from day one | Four screens don't justify a second surface; INV-6's one-source-of-truth layering keeps GraphQL addable as a projection later |
| Email delivery | `mailer` port: `LogMailer` (dev/tests) / `SmtpMailer` (deployed); relay is deployment config | Provider SDK baked in; building codes-only without real mail | Port keeps INV-8 (no deployment shape in code) and makes the provider an operator choice (§9 Q-1); real mail is required by Feature #1's "no stubbed auth" in PPE/Prod |
| Sessions | Signed cookie, no session table | Server-side session rows | No revocation/multi-device requirement yet; cookie sessions are one less table and zero queries; revisit with staff (§6.2 note) |
| Vocabulary | **Storefront** is ecomm's canonical noun (corpus "store" maps to it) | Adopt corpus "store" | The charter, Feature #1, and repo names all say storefront; glossary (§10) carries the mapping so corpus scenario IDs still read cleanly |
### 6.8 Testing strategy
<!-- §6.8 pending -->
The prototype's discipline, inherited:
- **Scenario-bound end-to-end tests** drive the app through its HTTP surface
(FastAPI TestClient on a fresh temp DB). Where a corpus scenario covers the
behavior, the test name embeds its ID (`test_14_01_0003_email_code_sent`);
PUCs without a corpus ID bind to the PUC (`test_puc_05_...`). One test per
scenario/PUC, including every unhappy path in §4.
- **The bootstrap test is INV-1's enforcement:** from an empty database, one
test walks request-code → verify → create-storefront → `/me` → admin
answer, asserting no step needed seeded state. A second asserts
`migrate()` is idempotent (re-running applies nothing).
- **Invariant tests** for the sharp edges: code expiry/attempt
exhaustion/cooldown (INV-3), concurrent second-storefront refusal (INV-4),
enumeration uniformity (§6.6), `LogMailer` capture in tests.
- **Frontend:** typecheck + production build in CI; the entry-routing rule
(§6.5) unit-tested; component/E2E browser tests deferred until there is UI
beyond forms.
- **Gate:** `scripts/check.sh` = `lint-imports` (layer contract, INV-6) +
`pytest` + frontend typecheck/build — locally pre-merge and in CI. "Tested"
for a slice (§7.2) means its scenarios are green in this gate.
- **PPE tier:** the §7.2 SLICE-4 rehearsal *is* the deployed-tier test — the
product flows walked against PPE with real mail before Prod exists.
### 6.9 Failure modes, rollback & flags
<!-- §6.9 pending -->
- **Mail relay down / refuses** → `request-code` returns `502
delivery_failed`; the user is told the code didn't go out (INV-9, §5.2) —
never a fake 204. Operator sees it in logs; codes are re-requestable, no
state is corrupted.
- **Migration fails at startup** → process refuses to start (INV-7);
flotilla's fail-stop deploy keeps the prior version serving; operator fixes
forward (no down-migrations — MVP migrations are additive).
- **DB file lost/corrupted** → restore from VM disk snapshot (the standard
flotilla VM's recovery story); accepted pre-v1 bar — an explicit backup
cadence is deferred and logged (§9 Q-2).
- **Session-secret leak** → rotate the secret in Secret Manager + redeploy;
all sessions invalidate at once (accepted blast radius at MVP scale,
§6.2).
- **Code-guessing attempts** → INV-3's attempt cap and cooldown bound the
attack; events are logged.
- **Rollback** = flotilla redeploy of the previous version; additive-only
MVP migrations make old-code-on-new-schema safe.
- **Feature flags / kill switch:** none — pre-v1, nothing to protect that a
redeploy doesn't cover; first flags arrive when there are users to shield.
## 7. Delivery Plan