diff --git a/specs/SD-0001-mvp-sign-up-and-single-storefront.md b/specs/SD-0001-mvp-sign-up-and-single-storefront.md
new file mode 100644
index 0000000..087d178
--- /dev/null
+++ b/specs/SD-0001-mvp-sign-up-and-single-storefront.md
@@ -0,0 +1,1059 @@
+# Solution Design: ecomm MVP — sign up and create a single storefront
+
+| | |
+| --- | --- |
+| **Anchor** | [wiggleverse/wiggleverse-ecomm#1](https://git.wiggleverse.org/wiggleverse/wiggleverse-ecomm/issues/1) (`type/feature`, `priority/P1`) |
+| **Author(s)** | Claude (session ecomm-0020), for Ben Stull |
+| **Reviewers / approvers** | Ben Stull |
+| **Status** | `approved` |
+| **Version** | v0.2.0 |
+| **Source artifacts** | BDD corpus: [research/shopify](https://git.wiggleverse.org/wiggleverse/wiggleverse-ecomm-prototype-content/src/branch/main/research/shopify) (esp. [flows/first-run-experience.md](https://git.wiggleverse.org/wiggleverse/wiggleverse-ecomm-prototype-content/src/branch/main/research/shopify/flows/first-run-experience.md)) · Prototype: [wiggleverse-ecomm-prototype](https://git.wiggleverse.org/wiggleverse/wiggleverse-ecomm-prototype) (R01–R07) · Reference: Shopify first-run experience · Supersedes: — |
+
+**Change log**
+
+| Date | Version | Change | By |
+| --- | --- | --- | --- |
+| 2026-06-10 | v0.1.0 | Initial draft (session ecomm-0020) | Claude |
+| 2026-06-10 | v0.2.0 | Self-review fixes; status → `approved` under the session's autonomous posture (handbook §6.5) — operator amendments welcome as v0.2.x | Claude |
+
+---
+
+## 1. Business Context
+
+*The business lens — solution-agnostic throughout. No mechanism is proposed until §2.*
+
+### 1.1 Executive Summary
+
+Independent merchants who want to sell online need a commerce platform that
+treats them honestly — and today Wiggleverse has nothing to offer them: no way
+to exist on the platform, no storefront to own, no surface to manage. This
+design delivers the first real value of ecomm: a person can become a merchant —
+establish their identity, claim their one storefront, and stand on a stable
+management surface — in every environment the platform runs in, from day one.
+The beneficiaries are the first merchants (who can finally exist on the
+platform) and the ecomm line itself (every subsequent capability — catalog,
+orders, checkout — gains a real account-and-storefront spine to attach to,
+instead of mocks). Just as importantly, it proves the platform can be brought
+to life anywhere from nothing: a fresh, empty environment reaches "first
+merchant, first storefront" through the product alone, with no hand-seeded
+data — so launching the real service is an ordinary, rehearsed act rather than
+a one-off ritual.
+
+### 1.2 Background
+
+ecomm is Wiggleverse's multi-tenant, OHM-grounded alternative to Shopify — a
+commerce platform whose mechanics are accountable to the
+[OHM concept corpus](https://ohm.wiggleverse.org) (no dark patterns, data
+minimization, durable addressability, honest pricing). The product was explored
+in a first-pass prototype (`wiggleverse-ecomm-prototype`, releases R01–R07,
+189 scenario-bound tests) built against a harvested corpus of ~1,238 BDD
+scenarios describing Shopify's behavior. That prototype is read-only prior art;
+`wiggleverse-ecomm` is the clean greenfield rebuild, stood up 2026-06-10 with
+its architecture intentionally undecided until this design.
+
+The strategic moment: the rebuild has repos, an issue tracker, and a charter —
+but no app code and no users. Everything in the product line queues behind one
+question: *how does a person come to exist on the platform and own a
+storefront?* Until that is answered for real — against real persistence, in
+real environments — no other capability can ship honestly.
+
+### 1.3 Business Actors / Roles
+
+| Role | Responsible for (in the business) |
+| --- | --- |
+| **Merchant** | A person running a (small) commerce business who wants to sell under their own storefront: they decide to join a platform, establish their business presence on it, and manage that presence over time. |
+| **Platform operator** | The person/org running the ecomm service itself: stands up and maintains the environments the platform runs in, and is accountable for the service being available to merchants. |
+| **ecomm team** | Builds the platform; needs each capability to rest on real, shippable foundations rather than mocks. |
+
+(Shoppers — the merchants' customers — are deliberately *not* an actor in this
+design; nothing here is shopper-facing yet. See §1.7.)
+
+### 1.4 Problem Statement
+
+A person cannot become a merchant on ecomm. There is no way to establish an
+identity on the platform, no way to claim a storefront, and no surface from
+which to manage one — the platform has no users because it cannot have users.
+And even if it could, there is today no defined path from "fresh, empty
+environment" to "working platform serving its first merchant": local
+development, pre-production, and production bring-up are all undefined, so
+nothing built on top can ever actually launch.
+
+### 1.5 Pain Points
+
+| # | Pain | Who feels it | Cost / frequency today |
+| --- | --- | --- | --- |
+| PP-1 | No way to establish an identity on the platform — a would-be merchant simply cannot exist on ecomm. | Merchant | Total exclusion; blocks every merchant, always. |
+| PP-2 | No way to own a storefront or reach a management surface — even an identity would have nothing to hold. | Merchant | Total; there is no unit of "my business" on the platform. |
+| PP-3 | Every subsequent capability (catalog, orders, checkout, policies) has nowhere real to attach — work would have to build on mocks and be redone. | ecomm team | Blocks the entire product line; rework risk on everything shipped against fakes. |
+| PP-4 | No defined, repeatable path from an empty environment to a working platform — bring-up of dev, pre-prod, and prod is folklore. | Platform operator | The service cannot launch anywhere; every environment question is open-ended toil. |
+
+### 1.6 Targeted Business Outcomes
+
+| Outcome | Success metric | Baseline → Target | Guardrail (must not regress) | How / when measured |
+| --- | --- | --- | --- | --- |
+| Merchants can exist on the platform | A real person can go from stranger to merchant-with-storefront, unaided | impossible → possible in every environment | No dark patterns at the door: no trial clock, no payment demand, no plan wall (OHM: Agency & Anti-Manipulation) | Walk the flow end-to-end in each environment at release |
+| The product line is unblocked | Subsequent Features attach to a real account + storefront spine | everything mocked → nothing mocked | The spine doesn't need rework to host the next Feature (no throwaway auth/tenancy) | First post-MVP Feature builds on it without spine changes |
+| Launching anywhere is routine | A fresh, empty environment reaches "first merchant signs up and creates the first storefront" through the product flows alone | folklore → documented, rehearsed gesture | Zero hand-seeded data in any environment | Bootstrap rehearsal executed on pre-prod, then prod, at release |
+
+### 1.7 Scope (business)
+
+- **In scope:** a person establishing an identity on the platform (joining and
+ returning); a merchant establishing their one storefront; the merchant
+ standing on a stable management surface for it; the platform being
+ bring-up-able from empty in every environment it runs in (local development,
+ pre-production, production).
+- **Out of scope (later designs):** anything sold or bought — catalog, orders,
+ checkout, payments; the shopper-facing public storefront; teams/staff
+ (anyone besides the one merchant touching the storefront); plans, billing,
+ or any commercial relationship between merchant and platform; storefront
+ settings/management beyond simply *having* the surface.
+- **Non-goals:** gating who may join (the prototype admitted users by
+ invitation; ecomm is an open product — anyone may become a merchant, and
+ re-introducing an admission gate is explicitly not pursued); supporting more
+ than one storefront per merchant *in this design* (the business wants the
+ door left open, not the capability now).
+
+### 1.8 Assumptions · Constraints · Dependencies
+
+- **Assumptions:**
+ - One storefront per merchant is the right *experience* bar for the MVP;
+ multiple-storefront merchants are a future need, not a current one. Risk
+ if wrong: an early merchant genuinely needs a second storefront — accepted;
+ the data is designed to keep that door open (Feature #1 constraint; INV-4).
+ - Merchants are reachable by email and will complete an email-based step to
+ join. Risk if wrong: an entry channel rethink — accepted at MVP scale.
+- **Constraints:**
+ - OHM-groundedness is a charter constraint: entry must be honest — no
+ manufactured urgency, no data collected beyond need, no lock-in mechanics.
+ - The three environments are a constraint, not a stretch goal: Feature #1's
+ acceptance says no environment is "later".
+ - Wiggleverse engineering standards apply (handbook): provisioning and
+ deploy via flotilla (§8), secrets never in repos or transcripts (§6.3),
+ private repos by default (§5.5).
+- **Dependencies:**
+ - **flotilla / launch-app** for environment provisioning and deploy (the
+ operator-run front door, handbook §8.5) — needed when the pre-prod and
+ prod environments are stood up.
+ - An **email delivery channel** for the entry step in real environments —
+ needed by the production-readiness slice (§7).
+
+### 1.9 Business Use Cases
+
+**BUC-1 — As a merchant, I can establish my identity on the platform, so that I can begin doing business there.**
+
+```gherkin
+Scenario: BUC-1 — A stranger becomes known to the platform
+ Given a person who has never dealt with the platform
+ When they present themselves and prove who they are
+ Then the platform knows them as a returning individual from then on
+ And they were asked for nothing beyond what proving who they are required
+```
+
+```gherkin
+Scenario: BUC-1a — A person who cannot prove who they are is not admitted as someone else
+ Given a person presenting an identity they cannot prove
+ When their proof fails
+ Then the platform does not mistake them for the claimed identity
+ And the genuine owner of that identity is unharmed
+```
+
+- **BUC-1 acceptance criteria:** the same person, returning later, is
+ recognized as the same merchant; two different people are never conflated;
+ no personal data beyond the minimum was demanded (OHM: Privacy & Data
+ Minimization).
+
+**BUC-2 — As a returning merchant, I can resume my standing on the platform, so that my business presence persists beyond a single visit.**
+
+```gherkin
+Scenario: BUC-2 — A known merchant returns
+ Given a merchant the platform already knows
+ When they return and prove who they are
+ Then they resume exactly the standing they had — same identity, same storefront
+```
+
+- **BUC-2 acceptance criteria:** returning costs only the proof-of-identity
+ step; nothing about their standing is lost or reset.
+
+**BUC-3 — As a merchant, I can establish my storefront, so that my business has a presence of its own on the platform.**
+
+```gherkin
+Scenario: BUC-3 — A merchant claims their storefront
+ Given a merchant known to the platform who has no storefront yet
+ When they establish their storefront
+ Then the platform holds exactly one storefront that is theirs
+ And establishing it cost them nothing and committed them to nothing
+```
+
+```gherkin
+Scenario: BUC-3a — One storefront is the bar
+ Given a merchant who already has their storefront
+ When they deal with the platform
+ Then they are never led toward acquiring a second one
+```
+
+- **BUC-3 acceptance criteria:** every merchant who completes the step has
+ exactly one storefront; no payment, plan, or commitment was demanded
+ (corpus: 14.01.0015, 14.01.0016); the platform's books would still be
+ correct if a merchant were someday allowed several (the 1-1 bar is an
+ experience rule, not a law of the data — Feature #1 constraint).
+
+**BUC-4 — As a merchant, I can stand on a management surface for my storefront, so that everything I will later do for my business has a home.**
+
+```gherkin
+Scenario: BUC-4 — The merchant arrives at their storefront's home
+ Given a merchant with a storefront
+ When they return to the platform
+ Then they arrive directly at their storefront's management surface
+ And it is honestly empty — it claims no capability that does not exist yet
+```
+
+- **BUC-4 acceptance criteria:** arrival is direct (no dead ends, no limbo —
+ a merchant *without* a storefront is guided to establish one instead, never
+ stranded); the surface carries the storefront's identity and nothing
+ fabricated.
+
+**BUC-5 — As a platform operator, I can bring the platform to life in a fresh environment, so that merchants can be served there without any hand-built state.**
+
+```gherkin
+Scenario: BUC-5 — An empty environment reaches its first merchant
+ Given a brand-new environment with empty persistence
+ When the operator performs the documented bring-up gesture
+ And the first merchant walks the ordinary product flows
+ Then the environment serves that merchant a working storefront
+ And no data was placed by hand at any step
+```
+
+```gherkin
+Scenario: BUC-5a — Bring-up is rehearsable
+ Given the bring-up gesture documented for one environment
+ When it is performed in the next environment (dev → pre-prod → prod)
+ Then it is the same gesture, and day-one production is simply the bootstrap state
+```
+
+- **BUC-5 acceptance criteria:** bring-up is a repeatable, documented gesture
+ (not folklore); it holds identically in local development, pre-production,
+ and production; the first merchant arrives through the product flows alone.
+
+---
+
+## 2. Solution Proposal
+
+**Build the first vertical slice of the ecomm web application** — a
+multi-tenant modular monolith that carries forward the prototype's *proven*
+shape and inverts its *disqualifying* assumption:
+
+- **Carry forward** (validated by R01–R07 of the prototype): a four-layer
+ Python backend (`entrypoint → api → domains → platform`) with the layer
+ contract mechanically enforced; SQLite (WAL) with forward-only numbered
+ migrations and no ORM; a React/Vite SPA admin talking to a screen-shaped
+ REST BFF; scenario-bound end-to-end tests (one test per BDD scenario ID);
+ OHM concepts cited inline next to the rules they ground.
+- **Invert** (the prototype's bootstrap was the wrong way round for a real
+ product): **no seeded tenant and no admission gate.** The prototype was born
+ with one hard-coded store and one invited owner; ecomm is born *empty*.
+ Identity is open sign-up via **email + one-time code** (passwordless;
+ corpus 14.01.0003–0004), the storefront is created *through the product* by
+ its merchant, and an empty database is a first-class, fully working state —
+ which is precisely what makes BUC-5's bootstrap story true by construction
+ rather than by tooling.
+- **Defer** (YAGNI for four screens): the prototype's second API surface
+ (GraphQL) is not built in this MVP; the layering keeps the seam so it can be
+ added later as a projection over the same domain core. Google/Apple OAuth
+ and sign-in-by-emailed-link (corpus 14.01.0005–0007) are deferred the same
+ way — the email-canonical identity rule (14.01.0010) keeps them addable
+ without account migration.
+
+**Why this approach** over the alternatives:
+
+- *Do nothing / concierge* (hand-create merchants on request): fails BUC-5
+ outright (every merchant is hand-seeded state) and validates nothing — the
+ prototype already validated the need; the open question is productionizing.
+- *Re-architect on a heavier stack* (Postgres, separate services, SSR
+ framework): buys scale and rendering properties the MVP measurably does not
+ need, at the cost of slower iteration and a bigger bootstrap surface —
+ directly against the "launching anywhere is routine" outcome (§1.6). The
+ single-VM flotilla deployment standard (handbook §8) favors the lighter
+ stack; revisit at real scale (§6.7, §7.4).
+- *Resume the prototype codebase*: it bakes in the seeded-tenant and
+ invite-gate assumptions at the foundation, and the rebuild exists precisely
+ to lay clean foundations. Its *patterns* are kept; its code is reference
+ only.
+
+**Solution-specific scope:** the landing page, sign-up/log-in (email +
+one-time code), create-storefront (both entry points: post-sign-up and
+returning-without-storefront), the empty admin shell, sign-out, and the
+bring-up story for localhost/PPE/Prod including real email delivery in
+deployed environments.
+
+**Solution-specific non-goals (this release):** GraphQL surface; OAuth
+providers and magic-link completion; the onboarding questionnaire and POS
+opt-in from the corpus first-run flow (14.01.0017–0024) — the merchant lands
+directly on their admin; storefront rename/settings (14.01.0025 makes naming
+optional at creation; *changing* it later is the Settings Feature's job);
+account-lifecycle polish beyond what passwordless entry gives for free
+(password reset does not exist because passwords do not exist; the one-time
+code *is* email verification; account deletion is deferred and logged in §9).
+
+---
+
+## 3. Product Personas
+
+| Product persona | In ecomm | Maps to business role(s) |
+| --- | --- | --- |
+| **Visitor** | An unauthenticated person on the entry surfaces: landing, sign-up/log-in. | Merchant (before they are known) |
+| **Merchant** | An authenticated account holder; creates and owns the storefront; lands on its admin. The MVP's only authenticated user type. | Merchant |
+| **Operator** | Brings up and deploys environments; interacts through flotilla and the bring-up runbook, not the web UI. | Platform operator; ecomm team |
+
+## 4. Product Use Cases
+
+Corpus grounding: scenario IDs cite the harvested Shopify first-run experience
+([flows/first-run-experience.md](https://git.wiggleverse.org/wiggleverse/wiggleverse-ecomm-prototype-content/src/branch/main/research/shopify/flows/first-run-experience.md),
+14.01.\*) and the zero-state dashboard (01.01.0008). Divergences are marked.
+
+**PUC-1 — Landing offers the two doors** *(realizes BUC-1, BUC-2; corpus 14.01.0001)*
+
+```gherkin
+Scenario: PUC-1 — The landing page offers signing up or logging in
+ Given a Visitor on the ecomm landing page
+ Then they see what ecomm is, a primary action to get started, and a link to log in
+ And no trial, pricing, or fee is presented (corpus 14.01.0011)
+```
+
+**PUC-2 — Sign up with email + one-time code** *(realizes BUC-1; corpus 14.01.0002–0004)*
+
+```gherkin
+Scenario: PUC-2 — A Visitor signs up
+ Given a Visitor who chose to get started
+ When they enter their email address and request a code
+ Then a one-time code is sent to that address
+ And the screen moves to code entry, telling them where the code went
+ When they enter the code within its validity window
+ Then they are signed in as a new Merchant
+ And they are taken directly into creating their storefront (PUC-4)
+```
+
+```gherkin
+Scenario: PUC-2a — Wrong code
+ Given a Visitor on the code-entry screen
+ When they enter an incorrect code
+ Then the screen says the code didn't match and lets them retry or request a new one
+ And after repeated failures the code is invalidated and a fresh request is required
+```
+
+```gherkin
+Scenario: PUC-2b — Expired code
+ When they enter a code past its validity window
+ Then the screen says the code expired and offers to send a fresh one
+```
+
+```gherkin
+Scenario: PUC-2c — Resend is rate-limited
+ When they ask for another code immediately after one was sent
+ Then the screen asks them to wait briefly before resending
+```
+
+**PUC-3 — Log in as a returning Merchant** *(realizes BUC-2; corpus 14.01.0009, 14.01.0010)*
+
+```gherkin
+Scenario: PUC-3 — A returning Merchant logs in
+ Given a Visitor whose email already belongs to a Merchant account
+ When they complete the same email + code flow (from either door)
+ Then they are signed in to that same account — the email is the canonical key
+ And no duplicate account is created (corpus 14.01.0010)
+```
+
+*(Sign-up and log-in are deliberately the same mechanism; the two doors differ
+only in framing. A "sign up" with a known email is a log-in; a "log in" with an
+unknown email creates the account. Honest copy on the code-entry screen states
+which happened.)*
+
+**PUC-4 — Create the storefront after sign-up** *(realizes BUC-3; corpus 14.01.0013, 14.01.0015, 14.01.0016, 14.01.0025, 14.01.0026)*
+
+```gherkin
+Scenario: PUC-4 — A new Merchant creates their storefront
+ Given a Merchant signed in with no storefront
+ Then they are on the create-storefront screen — there is nothing else to do yet
+ When they optionally enter a storefront name and confirm
+ Then the storefront is created — named as entered, or with a generated default name
+ And no payment method, plan, or commitment was requested (14.01.0015/0016)
+ And they land on the storefront's admin page (PUC-6)
+```
+
+**PUC-5 — Returning without a storefront → prompted to create** *(realizes BUC-3, BUC-4)*
+
+```gherkin
+Scenario: PUC-5 — A Merchant who never finished creating one
+ Given a returning Merchant who has no storefront
+ When they log in
+ Then they arrive at the create-storefront screen (PUC-4), not an empty admin or an error
+```
+
+**PUC-6 — Returning with a storefront → straight to its admin** *(realizes BUC-4; corpus 14.01.0027)*
+
+```gherkin
+Scenario: PUC-6 — A Merchant returns to their storefront
+ Given a returning Merchant who has a storefront
+ When they log in
+ Then they land directly on that storefront's admin page
+ And nothing further is mandatory (corpus 14.01.0027)
+```
+
+**PUC-7 — One storefront: no second-storefront affordance** *(realizes BUC-3a; **diverges** from corpus 14.01.0012/0014 — multi-store per account is deliberately not offered; see §9 D-2)*
+
+```gherkin
+Scenario: PUC-7 — No "create another storefront" exists
+ Given a Merchant with a storefront
+ Then no surface offers creating another one
+ And directly invoking storefront creation anyway is refused with an honest
+ explanation that ecomm is one storefront per account today
+```
+
+**PUC-8 — The admin shell is honestly empty** *(realizes BUC-4; zero-state per corpus 01.01.0008)*
+
+```gherkin
+Scenario: PUC-8 — Empty admin
+ Given a Merchant on their storefront's admin page
+ Then the page carries the storefront's name and the signed-in account's email
+ And states plainly that there is nothing to manage yet
+ And shows no fabricated metrics, fake modules, or teaser upsells
+```
+
+**PUC-9 — Sign out** *(product-only)*
+
+```gherkin
+Scenario: PUC-9 — A Merchant signs out
+ Given a signed-in Merchant anywhere in the admin
+ When they sign out
+ Then their session ends and they are back on the landing page as a Visitor
+```
+
+**PUC-10 — A developer bootstraps localhost** *(realizes BUC-5; Operator-facing — the surface is a terminal, not a screen)*
+
+```gherkin
+Scenario: PUC-10 — Clean checkout to first storefront, locally
+ Given a clean checkout of wiggleverse-ecomm and the documented prerequisites
+ When the developer runs the documented dev bring-up command
+ Then the app starts against an empty local store, applying schema migrations itself
+ And the developer completes sign-up → create storefront → admin in a browser
+ And the one-time code reaches them through the local dev channel (no real mail needed)
+```
+
+**PUC-11 — The Operator bootstraps a deployed environment (PPE, then Prod)** *(realizes BUC-5, BUC-5a)*
+
+```gherkin
+Scenario: PUC-11 — Fresh deployed environment to first merchant
+ Given an environment provisioned and deployed through flotilla, with empty persistence
+ When the app starts, applying schema migrations itself
+ Then the first real merchant can complete sign-up → storefront → admin
+ through the public flows, with the code arriving by real email
+ And the rehearsal on PPE and the day-one Prod state are the same gesture (BUC-5a)
+```
+
+## 5. UX Layout
+
+Text is the source of truth; wireframes are generated from these descriptions
+on demand and not committed. One SPA, four surfaces. Visual language: calm,
+text-first, honest — no countdowns, no badges, no fake density.
+
+### 5.1 Screen: Landing (serves PUC-1)
+
+- **Purpose:** a Visitor learns what ecomm is and picks a door.
+- **Layout (top → bottom):**
+ - **Header:** wordmark "ecomm" (left); "Log in" link (right).
+ - **Primary content:** one short value statement (what ecomm is, in OHM
+ voice — honest commerce, your storefront is yours); primary button
+ **"Create your storefront"**.
+ - **Footer:** minimal (project identity).
+- **States:** happy only — static page. A signed-in Merchant hitting `/` is
+ redirected per entry routing (§6.5, PUC-6/PUC-5).
+- **Notifications:** none.
+
+### 5.2 Screen: Sign in (serves PUC-2, PUC-3; two steps)
+
+- **Purpose:** the single email + one-time-code flow behind both doors.
+- **Step 1 — email entry:**
+ - **Header:** wordmark; back-to-landing link.
+ - **Primary content:** heading framed by the door used ("Create your
+ storefront" / "Log in" — same form); email field (label "Email";
+ autofocus); primary button **"Send code"**; one line of honest copy:
+ "We'll email you a one-time code. That's all we need."
+ - **States:** happy → step 2 · invalid email format: inline field error ·
+ sending: button busy state · resend cooldown active: button disabled with
+ "you can resend in ~Ns" (PUC-2c) · delivery failure: banner "We couldn't
+ send the code — try again" (no fake success; §6.9).
+- **Step 2 — code entry:**
+ - **Primary content:** "We sent a code to ⟨email⟩" (with a "wrong address?"
+ link back to step 1); code field (one-time-code input semantics); primary
+ button **"Continue"**; secondary "Resend code" (cooldown-aware).
+ - **States:** happy: session starts → entry routing (§6.5) · wrong code:
+ inline "That code didn't match" with attempts remaining (PUC-2a) ·
+ expired: inline "That code expired" + resend offer (PUC-2b) · attempts
+ exhausted: code invalidated, prompt to request a fresh one · loading:
+ button busy.
+ - **Honest-copy rule (PUC-3):** after verification the screen states which
+ happened — "Welcome to ecomm" (new account) vs "Welcome back" (existing).
+- **Notifications:** the one-time-code **email** (subject "Your ecomm code:
+ ⟨code⟩"; body: the code, its validity window, and "if you didn't request
+ this, ignore it"). In localhost/dev the code reaches the developer via the
+ dev channel instead (§6.5 PUC-10).
+
+### 5.3 Screen: Create storefront (serves PUC-4, PUC-5, PUC-7)
+
+- **Purpose:** a storefront-less Merchant establishes their one storefront.
+- **Layout:**
+ - **Header:** wordmark; signed-in email (right) with a "Sign out" action
+ (PUC-9).
+ - **Primary content:** heading "Create your storefront"; name field (label
+ "Storefront name", explicitly optional — helper text "You can leave this
+ blank; we'll pick a placeholder name you can change later"); primary
+ button **"Create storefront"**.
+- **States:** happy: → admin (PUC-6) · blank name: allowed — a default name is
+ generated (corpus 14.01.0026) · creating: button busy · already-owns
+ (defense in depth, PUC-7): banner "Your account already has its storefront"
+ + link to the admin · error: banner with retry.
+- **Notifications:** none (no confirmation email — minimal channel use).
+
+### 5.4 Screen: Admin shell (serves PUC-8, PUC-9)
+
+- **Purpose:** the storefront's stable home; honestly empty in this release.
+- **Layout:**
+ - **Header:** storefront name (left — the admin's anchor identity);
+ signed-in email + "Sign out" (right).
+ - **Primary content:** empty state — heading with the storefront's name,
+ one line: "There's nothing to manage yet. Catalog, orders, and settings
+ will appear here as ecomm grows." Nothing else: no zeroed metric tiles,
+ no locked-feature teasers (OHM: Agency & Anti-Manipulation — no
+ manufactured aspiration).
+- **States:** happy/empty are the same state by design · loading: minimal
+ skeleton while `/me` resolves · session expired: redirect to sign-in.
+- **Notifications:** none.
+
+## 6. Technical Design
+
+### 6.1 Invariants
+
+- **INV-1 — Empty is a working state.** The application boots against empty
+ persistence, applies its own schema migrations, and serves the full
+ sign-up → storefront → admin flow. No code path assumes a pre-existing row;
+ there is no seed step. *Enforced mechanically:* the bootstrap test (§6.8)
+ runs the whole flow from a fresh database; it is the first test of the
+ suite.
+- **INV-2 — Email is the canonical account key.** One account per email
+ (case-insensitively normalized); any future auth method resolves to the
+ same account by email (corpus 14.01.0010). *Enforced:* unique index on the
+ normalized email column.
+- **INV-3 — One-time codes are handled like secrets.** Stored only as hashes;
+ expire after 10 minutes; single-use; at most 5 verification attempts per
+ code; resend no more than once per 60 seconds per email. Codes never appear
+ in production logs or API responses. *Enforced:* schema (consumed/attempt
+ columns) + service-layer checks + tests.
+- **INV-4 — One storefront per account is the MVP rule, not the schema.**
+ Ownership lives in a membership relation that admits many storefronts per
+ account and many members per storefront; the *service layer* refuses a
+ second storefront for the same account today (and the UX never offers it,
+ PUC-7). Lifting the rule later is deleting one check, not a migration.
+ *Enforced:* service-layer guard + test; no `UNIQUE(account_id)` anywhere in
+ the storefront path.
+- **INV-5 — Every storefront-scoped row carries its storefront.** All future
+ tenant data references `storefront_id`; nothing tenant-scoped is global.
+ (Trivially satisfied by this MVP's two tenant tables; stated now because it
+ binds every later Feature.) *Enforced:* schema review + FK constraints.
+- **INV-6 — One source of truth for rules.** Authentication, authorization,
+ and the one-storefront guard live in the domain layer exactly once; every
+ API surface (today REST; later GraphQL) delegates to them. *Enforced:*
+ layering contract (import-linter) + the API layer owning no business logic.
+- **INV-7 — Migrations are forward-only and fail-stop.** Numbered `.sql`
+ files applied in order at startup, recorded in a migrations table; a failed
+ migration aborts startup (the deploy fails; the prior version keeps
+ serving). No down-migrations.
+- **INV-8 — No deployment shape in framework code.** URLs, mail relay
+ coordinates, secrets: all arrive via environment/deployment configuration
+ (resolved from Secret Manager in deployed environments); none are baked
+ into the app (handbook §5.3). *Enforced:* config surface table (§6.6) +
+ review.
+- **INV-9 — Honest failure.** When the platform cannot do what was asked
+ (code not sent, storefront not created), the user is told so — no fake
+ success, no silent drop (OHM: Transparency & Traceability).
+
+### 6.2 High-level architecture
+
+```mermaid
+flowchart LR
+ subgraph Browser
+ SPA[Admin SPA
React + Vite]
+ end
+ subgraph "ecomm backend (FastAPI, one process)"
+ BFF[REST BFF
main.py]
+ subgraph domains
+ ACC[accounts
identity · OTC · sessions]
+ SF[storefronts
storefront · membership]
+ end
+ subgraph platform
+ DB[db
SQLite WAL + migrations]
+ MAIL[mailer
port: log / SMTP]
+ DEPS[deps
request wiring]
+ end
+ end
+ STORE[(SQLite file)]
+ RELAY[SMTP relay
deployed envs only]
+
+ SPA -->|JSON /api/*| BFF
+ BFF --> ACC
+ BFF --> SF
+ ACC --> DB
+ SF --> DB
+ ACC --> MAIL
+ DB --> STORE
+ MAIL -.-> RELAY
+```
+
+Layering is the prototype's four-layer contract, mechanically enforced with
+import-linter: `main` (entrypoint + REST BFF) → `domains` → `platform`;
+imports flow only downward; domains are imported via their package surface
+(`__all__`), never by reaching into modules.
+
+- **Admin SPA** — the four surfaces of §5; owns no rules. Renders what the
+ BFF returns; must never enforce a business rule the backend doesn't (the
+ UX-level one-storefront affordance is presentation; the rule is INV-4's
+ service guard).
+- **REST BFF (`main.py`)** — screen-shaped JSON endpoints, one round-trip per
+ screen; translates HTTP ↔ domain calls; owns no business logic (INV-6).
+- **`accounts` domain** — owns identity: account records, one-time-code
+ issue/verify (INV-2, INV-3), session establishment, the signed-in-user
+ gate. Must never know what a storefront is.
+- **`storefronts` domain** — owns the storefront entity and the
+ account↔storefront membership (INV-4, INV-5), including the entry-routing
+ answer ("which storefront, if any, does this account have?"). Must never
+ mint identity.
+- **`platform/db`** — SQLite WAL connection + forward-only migration runner
+ (INV-7); owns the schema lifecycle, no business rules.
+- **`platform/mailer`** — a one-method port (`send(to, subject, body)`) with
+ two adapters: `LogMailer` (localhost/dev and tests — the message lands in
+ the app log / a test inbox) and `SmtpMailer` (PPE/Prod — relay coordinates
+ from deployment config, INV-8). Chosen by configuration at startup.
+- **`platform/deps`** — FastAPI dependency wiring: per-request DB connection,
+ current-session resolution, configured mailer.
+
+Sessions are signed HTTP-only cookies (server secret from configuration; no
+session table). MVP-honest: signing out clears the cookie; a leaked signing
+secret is rotated by replacing the configured secret, which invalidates all
+sessions at once. Revisit (server-side sessions) when staff/multi-device
+management arrives.
+
+### 6.3 Data model & ownership
+
+| Entity | Owned by | Key fields | System of record |
+| --- | --- | --- | --- |
+| `account` | accounts | `id`, `email` (unique, normalized — INV-2), `created_at` | SQLite |
+| `auth_code` | accounts | `id`, `email`, `code_hash`, `expires_at`, `attempts`, `consumed_at`, `created_at` (INV-3) | SQLite |
+| `storefront` | storefronts | `id`, `name`, `created_at` | SQLite |
+| `storefront_membership` | storefronts | `account_id` (FK), `storefront_id` (FK), `role` (MVP: always `'owner'`), `created_at`; PK `(account_id, storefront_id)` | SQLite |
+
+Notes:
+
+- **Data minimization (OHM):** the account stores the email and nothing else —
+ no name, no phone, no profile (corpus sign-up asks for nothing more;
+ prototype precedent). A display name arrives only when a Feature needs it.
+- `auth_code` rows are keyed by *email*, not account, because the code is
+ issued before the account may exist (sign-up and log-in converge, PUC-3).
+ Consumed/expired rows are purged opportunistically on issue.
+- `storefront_membership.role` is a single-value enum today; it is the seam
+ where the corpus's staff & permissions model (13.15.\*) attaches later
+ without reshaping ownership (INV-4 rationale).
+- `storefront.name`: when the merchant leaves it blank, a generated default
+ (e.g. "⟨email local-part⟩'s storefront") is stored — the column is always
+ populated; "unnamed" is a naming choice, not a NULL (corpus 14.01.0026).
+
+### 6.4 Interfaces & contracts
+
+All JSON over `/api/*`; session via signed cookie. Errors share one shape:
+`{"error": {"code": "", "message": ""}}`.
+
+| Endpoint | In | Out | Errors |
+| --- | --- | --- | --- |
+| `POST /api/auth/request-code` | `{email}` | `204` — code issued & dispatched. Response is identical whether the email is new or known (no account enumeration). | `400 invalid_email` · `429 resend_cooldown` (with `retry_after_s`) · `502 delivery_failed` (INV-9 — the relay refused; dev `LogMailer` cannot fail) |
+| `POST /api/auth/verify` | `{email, code}` | `200 {account: {email}, storefront: {id, name} \| null, created: bool}` — session cookie set; `created` drives the honest welcome copy (PUC-3); `storefront` drives entry routing (§6.5). | `400 code_mismatch` (with `attempts_remaining`) · `400 code_expired` · `400 code_exhausted` |
+| `GET /api/auth/me` | — | `200 {account: {email}, storefront: {id, name} \| null}` | `401 unauthenticated` |
+| `POST /api/auth/logout` | — | `204` — cookie cleared | — (idempotent) |
+| `POST /api/storefronts` | `{name?: string}` | `201 {id, name}` — membership row created with `role='owner'` | `401 unauthenticated` · `409 already_owns_storefront` (INV-4, PUC-7) |
+| `GET /healthz` | — | `200 {status: "ok"}` — process up, DB reachable, migrations current | `503` otherwise (deploy gate, §6.9) |
+
+Contract stance: these are the stable seams the SPA and the tests bind to;
+adding fields is free, renaming/removing is a spec change (this document,
+§3.1-handbook).
+
+### 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 SF as storefronts
+ 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
+
+- **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
+
+| 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 R01–R07 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.0003–0004); 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
+
+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
+
+- **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
+
+### 7.1 Approach / strategy
+
+Walking skeleton first, then the two domains in dependency order, then the
+deployed environments — riskiest-last is wrong here: the *deploy* story is the
+novel risk (the prototype never deployed), so it gets its own slice with
+everything else already green, and PPE rehearses Prod (BUC-5a) rather than
+both being attempted at once.
+
+Execution convention (per the handbook): each slice is its own
+planning-and-executing session — implementation plan written just-in-time at
+session start → execute → verify → merge + version bump. The design pass is
+this document, once; if a slice proves it wrong, amend the spec, then replan.
+Slice tasks live in each slice's plan, not here.
+
+### 7.2 Slicing plan
+
+#### SLICE-1 — Walking skeleton → completes PUC-10 (localhost half of BUC-5)
+
+- **Depends on:** —
+- **Ships:** repo scaffold (backend 4-layer skeleton + import-linter
+ contract, frontend Vite/React shell, `scripts/check.sh`, CI); `platform/db`
+ with the migration runner (INV-7) and migration `0001` (the §6.3 schema);
+ `/healthz`; `scripts/dev.sh`; first cut of `docs/BOOTSTRAP.md` (localhost
+ section).
+- **Definition of done:** clean checkout → `scripts/dev.sh` → app up on an
+ empty, self-migrated DB; `/healthz` green; check gate green in CI; the
+ bootstrap test skeleton runs migrate-from-empty + idempotent re-migrate
+ (INV-1 partial — full flow lands with SLICE-2/3).
+
+#### SLICE-2 — Identity → completes PUC-1, PUC-2 (+a/b/c), PUC-3, PUC-9
+
+- **Depends on:** SLICE-1
+- **Ships:** `accounts` domain (request-code / verify / sessions / me /
+ logout per §6.4–6.5), `platform/mailer` port with `LogMailer`, Landing +
+ Sign-in screens, entry routing with storefront hard-wired `null`.
+- **Definition of done:** BUC-1/BUC-2 acceptance criteria met end to end on
+ localhost; scenario tests for 14.01.0001–0004, 0009–0011 and PUC-2a/b/c
+ green; INV-2/INV-3 invariant tests green; no enumeration (uniform 204)
+ asserted.
+
+#### SLICE-3 — Storefront → completes PUC-4, PUC-5, PUC-6, PUC-7, PUC-8
+
+- **Depends on:** SLICE-2
+- **Ships:** `storefronts` domain (create + guard + membership per §6.5),
+ Create-storefront + Admin-shell screens, full entry routing, the complete
+ bootstrap test (INV-1 whole-flow).
+- **Definition of done:** BUC-3/BUC-4 acceptance criteria met; scenario tests
+ for 14.01.0013/0015/0016/0025/0026/0027 and PUC-5/7/8 green; INV-4
+ concurrent-create test green; localhost bootstrap (PUC-10) walkable start
+ to finish — BUC-5's dev third done.
+
+#### SLICE-4 — Deployed environments → completes PUC-11 (BUC-5, BUC-5a)
+
+- **Depends on:** SLICE-3; **operator gestures:** PPE + Prod provisioning via
+ launch-app/flotilla (handbook §8.5 — the session hands the operator the
+ commands and waits), SMTP relay choice + credentials into Secret Manager
+ (§9 Q-1).
+- **Ships:** `SmtpMailer` + config wiring (INV-8), `deployment.toml`,
+ deployed-environment hardening from §6.6 (Secure cookies, log hygiene),
+ `docs/BOOTSTRAP.md` completed for PPE/Prod.
+- **Definition of done:** bootstrap rehearsal executed on PPE — a real
+ sign-up → storefront → admin with a real emailed code — then the identical
+ gesture on Prod; both recorded in the runbook; Feature #1 acceptance fully
+ met; Feature #1 closed.
+
+### 7.3 Rollout / launch plan
+
+Pre-v1, single Prod: PPE rehearsal is the canary (SLICE-4); Prod day-one *is*
+the bootstrap state; rollback is flotilla redeploy of the prior version
+(§6.9). No staged exposure — there is no traffic to stage.
+
+### 7.4 Risks & mitigations
+
+| Risk | Likelihood / impact | Mitigation |
+| --- | --- | --- |
+| R-1: First-ever ecomm deploy — flotilla provisioning/deploy path unexercised for this app | M / H | Isolate in SLICE-4 with all product code already green; PPE before Prod; operator runs provisioning (handbook §8.5); wiggle-snip's deployment.toml as template |
+| R-2: OTC email deliverability (spam-foldering, relay limits) kills real sign-ups | M / H | Relay is deployment config (swap without code, INV-8); rehearsal on PPE with real mailboxes before Prod; honest `delivery_failed` surfacing (INV-9) makes failures visible, not silent |
+| R-3: SQLite write concurrency at real scale | L (MVP) / M | WAL + short transactions now; INV-5/INV-6 keep the storage seam clean for a Postgres move; revisit at the first scale-bearing Feature |
+| R-4: Scope creep into admin features ("just one settings field…") | M / M | PUC-8's honestly-empty rule + §1.7 scope; anything beyond the shell is a new Feature |
+| R-5: The 1-1 storefront rule calcifies into hidden assumptions | L / M | INV-4 names the single deletable guard; membership schema + tests assert the many-capable shape stays intact |
+
+## 8. Traceability matrix
+
+| Pain | Business UC | Product UC | Slice | Tests |
+| --- | --- | --- | --- | --- |
+| PP-1 | BUC-1, BUC-1a, BUC-2 | PUC-1, PUC-2 (+a/b/c), PUC-3, PUC-9 | SLICE-2 | `test_14_01_0001…0004`, `test_14_01_0009…0011`, `test_puc_02a/b/c_*`, `test_puc_09_*`, INV-2/INV-3 tests |
+| PP-2 | BUC-3, BUC-3a, BUC-4 | PUC-4, PUC-5, PUC-6, PUC-7, PUC-8 | SLICE-3 | `test_14_01_0013/0015/0016/0025/0026/0027`, `test_puc_05/07/08_*`, INV-4 test |
+| PP-3 | BUC-1…BUC-4 (the real spine) | all of the above | SLICE-1…3 | the whole gate (`scripts/check.sh`) green on real persistence |
+| PP-4 | BUC-5, BUC-5a | PUC-10, PUC-11 | SLICE-1, SLICE-4 | bootstrap test (INV-1), migrate-idempotence test; PPE + Prod rehearsals recorded in `docs/BOOTSTRAP.md` |
+
+Audit: every pain is relieved by at least one slice; every BUC is realized by
+at least one PUC; every PUC lands in exactly one slice; every slice carries
+tests. PUC-9 is product-only (no BUC) by design; BUC-1a is realized inside
+PUC-2a/b (failed proof ≠ admission).
+
+## 9. Open Questions & Decisions log
+
+**Open**
+
+| # | Question | Owner | Blocks |
+| --- | --- | --- | --- |
+| Q-1 | Which SMTP relay/provider serves PPE and Prod? (Deployment-shape choice — config + Secret Manager entry, not code; the app speaks SMTP either way.) | Operator (Ben) | SLICE-4 deploy config only |
+| Q-2 | Backup cadence for the Prod SQLite file beyond VM disk snapshots (and when does it graduate from "accepted pre-v1 bar", §6.9)? | Operator (Ben) | nothing in this Feature; revisit before real merchant data accrues |
+| Q-3 | Account deletion / data export (OHM: Exit & Portability) — deliberately deferred from the MVP's honest bar; which Feature picks it up? | ecomm team | nothing here; must be captured as a Feature before public launch |
+
+**Resolved**
+
+| # | Decision | Resolution | Date |
+| --- | --- | --- | --- |
+| D-1 | Architecture for the clean rebuild | Carry forward the prototype's proven shape (4-layer monolith, SQLite/WAL, React SPA, REST BFF), invert seed + admission gate, defer GraphQL/OAuth — §2, §6.7 | 2026-06-10 |
+| D-2 | One vs many storefronts per account | UX + service-layer 1-1 (INV-4) over a many-capable membership schema; conscious divergence from corpus 14.01.0012/0014, per Feature #1 | 2026-06-10 |
+| D-3 | Auth bar for the MVP | Passwordless email + OTC; no passwords ⇒ no reset flow; OTC ⇒ verification inherent; OAuth/magic-link deferred under the email-canonical rule (INV-2) | 2026-06-10 |
+| D-4 | Bootstrap mechanism | No seed anywhere; empty persistence is a working state (INV-1); migrations self-apply at startup (INV-7) | 2026-06-10 |
+| D-5 | Canonical noun | "Storefront" (charter + Feature #1), with corpus "store" mapped in §10 | 2026-06-10 |
+| D-6 | Corpus first-run steps not adopted | Onboarding questionnaire / POS opt-in (14.01.0017–0024) and multi-store selection (14.01.0012/0014) are out; merchant lands straight on the admin | 2026-06-10 |
+
+## 10. Glossary & References
+
+- **Storefront** — ecomm's unit of "a merchant's business presence": the
+ thing a merchant owns, manages, and (later) sells through. Maps to the BDD
+ corpus's **store** (corpus scenario text saying "store" reads as
+ "storefront" here — D-5).
+- **Merchant** — the business actor who owns a storefront; as a product
+ persona, the authenticated account holder (the corpus's "Store Owner").
+- **Admin** — the merchant-facing management surface of a storefront; this
+ release ships its honestly-empty shell.
+- **One-time code (OTC)** — the short-lived emailed code that is ecomm's
+ passwordless proof of identity (INV-3).
+- **Entry routing** — the exhaustive arrival rule of §6.5: no session →
+ landing; session without storefront → create; session with storefront →
+ admin.
+- **Bootstrap** — bringing an environment from empty persistence to "first
+ merchant, first storefront" through the product flows alone (BUC-5);
+ documented per environment in `docs/BOOTSTRAP.md`.
+- **PPE** — the pre-production environment; Prod's rehearsal stage (handbook
+ §8).
+- **flotilla** — the Wiggleverse operator CLI that provisions and deploys
+ app environments (handbook §8); provisioning is an operator-run gesture
+ (§8.5).
+- **OHM** — the concept corpus at that grounds
+ ecomm's mechanics (Agency & Anti-Manipulation, Privacy & Data
+ Minimization, Transparency & Traceability, Duty of Care, …).
+- **References:**
+ - Anchoring Feature:
+ - BDD corpus:
+ — esp. `flows/first-run-experience.md` (14.01.\*), `analytics/dashboard.md`
+ (01.01.0008), `settings/general.md` (13.06.\*), `settings/users-and-permissions.md` (13.15.\*)
+ - Reference prototype:
+ (R01–R07; SPEC.md, `.importlinter`, `platform/`, `domains/`)
+ - Engineering handbook & Solution-Design standard:
+ (`README.md`,
+ `solution-design/GUIDE.md`)
+ - Capture session for Feature #1: ecomm session 0019 (2026-06-10)