---
status: graduated
---
# 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.3.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 |
| 2026-06-10 | v0.3.0 | Datastore: **PostgreSQL everywhere** (Cloud SQL in PPE/Prod; single pinned Docker container in dev) replaces SQLite — D-7/D-8; retires R-3, adds R-6; §§2, 6.2–6.9, 7.2, 7.4, 9 touched | Ben + 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; relational persistence through forward-only
  numbered migrations and no ORM — with one deliberate engine swap from the
  prototype's SQLite to **PostgreSQL in every environment** (Cloud SQL when
  deployed, a Docker container in dev; rationale in §6.7/D-7); 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<br/>React + Vite]
  end
  subgraph "ecomm backend (FastAPI, one process)"
    BFF[REST BFF<br/>main.py]
    subgraph domains
      ACC[accounts<br/>identity · OTC · sessions]
      SF[storefronts<br/>storefront · membership]
    end
    subgraph platform
      DB[db<br/>Postgres + migrations]
      MAIL[mailer<br/>port: log / SMTP]
      DEPS[deps<br/>request wiring]
    end
  end
  STORE[(PostgreSQL<br/>Cloud SQL · dev: Docker)]
  RELAY[SMTP relay<br/>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`** — PostgreSQL connections (psycopg, small pool) + the
  forward-only migration runner (INV-7); owns the schema lifecycle, no
  business rules. The engine is PostgreSQL in **every** environment: Cloud
  SQL in PPE/Prod, a single pinned-version Docker container in dev and tests
  (D-7) — one engine, one driver, full dev/prod parity.
- **`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` | PostgreSQL |
| `auth_code` | accounts | `id`, `email`, `code_hash`, `expires_at`, `attempts`, `consumed_at`, `created_at` (INV-3) | PostgreSQL |
| `storefront` | storefronts | `id`, `name`, `created_at` | PostgreSQL |
| `storefront_membership` | storefronts | `account_id` (FK), `storefront_id` (FK), `role` (MVP: always `'owner'`), `created_at`; PK `(account_id, storefront_id)` | PostgreSQL |

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": "<machine-code>", "message": "<honest human text>"}}`.

| 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 PostgreSQL

  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 PostgreSQL

  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 PostgreSQL (empty database)
  actor Mer as First merchant

  Op->>App: start (dev script · flotilla deploy)
  App->>DB: connect; 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
  (documented prerequisite: Docker): it first brings up the dev datastore —
  a **single PostgreSQL container** (one-service Docker Compose, pinned to
  the Cloud SQL major version, named volume, healthcheck; the app itself
  stays native for the fast dev loop) — waits for healthy, then starts the
  backend (venv + deps, :8000) and Vite (:5173 proxying `/api`);
  `docker compose down -v` is the one-command reset-to-empty gesture for
  rehearsing the bootstrap. 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); persistence is **Cloud SQL for PostgreSQL**, provisioned
  per environment as part of that same gesture, with the connection URL and
  credentials resolved from Secret Manager via deployment config (INV-8);
  mailer is `SmtpMailer` with relay coordinates and secrets resolved the
  same way; 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,
    database credentials, 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. PostgreSQL with a small connection pool is far above
  this bar.
- **Availability & resilience:** one VM, one process (standard flotilla
  stack) against managed Cloud SQL (automated backups + point-in-time
  recovery); 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 | **PostgreSQL everywhere** (Cloud SQL in PPE/Prod; single pinned Docker container in dev/tests), psycopg, no ORM, forward-only `.sql` migrations | SQLite everywhere (prototype precedent, v0.1–v0.2 of this spec); SQLite-dev + Postgres-deployed bridge with dual-engine CI; SQLite + Litestream | ecomm's destination workload is transactional, money-bearing, multi-tenant — row locking (`FOR UPDATE`/`SKIP LOCKED`) arrives with the first inventory/checkout Feature, which SQLite cannot express; pre-data is the cheapest moment to swap (zero rows, ~10 queries); no ORM means dialect drift compounds per Feature, so the bridge was a planned demolition; Docker is already assumed by the standard local test tier, so dev parity costs one compose file (D-7) |
| 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 per-test PostgreSQL database — created from
  a template against the dev container, so every test runs the engine Prod
  runs). 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` (against the compose Postgres locally; a Postgres service in CI)
  + 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).
- **Database loss/corruption** → Cloud SQL automated backups +
  point-in-time recovery (enabled as part of SLICE-4 provisioning; D-8). In
  dev, the named volume is disposable by design (`docker compose down -v`).
- **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); the dev
  datastore compose file (single pinned PostgreSQL service, named volume);
  `platform/db` (psycopg + pool) with the migration runner (INV-7) and
  migration `0001` (the §6.3 schema); `/healthz`; `scripts/dev.sh` (owns the
  container lifecycle); first cut of `docs/BOOTSTRAP.md` (localhost section,
  Docker prerequisite).
- **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), **Cloud SQL for PostgreSQL per environment** (with
  automated backups + PITR enabled, D-8) and its credentials into Secret
  Manager, 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~~ | — | Retired v0.3.0 — superseded by D-7 (PostgreSQL everywhere); the risk no longer exists |
| R-6: Docker-in-dev prerequisite adds bring-up friction (PUC-10 is no longer zero-dependency) | L / L | `scripts/dev.sh` owns the container lifecycle end to end; Docker is already required by the standard local test tier; `docs/BOOTSTRAP.md` names it as the one prerequisite |
| 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-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, React SPA, REST BFF; datastore engine since revised by D-7), 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 |
| D-7 | Datastore engine (revisits D-1's SQLite) | **PostgreSQL in every environment**: Cloud SQL (PPE/Prod), single pinned Docker container (dev/tests; app stays native — the container is the datastore only). Chosen pre-data over SQLite-everywhere and over a SQLite-dev/Postgres-deployed bridge — see §6.7 | 2026-06-10 |
| D-8 | Q-2 (Prod backup cadence) | Resolved by D-7: Cloud SQL automated backups + point-in-time recovery, enabled at SLICE-4 provisioning | 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 <https://ohm.wiggleverse.org> that grounds
  ecomm's mechanics (Agency & Anti-Manipulation, Privacy & Data
  Minimization, Transparency & Traceability, Duty of Care, …).
- **References:**
  - Anchoring Feature: <https://git.wiggleverse.org/wiggleverse/wiggleverse-ecomm/issues/1>
  - BDD corpus: <https://git.wiggleverse.org/wiggleverse/wiggleverse-ecomm-prototype-content/src/branch/main/research/shopify>
    — 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: <https://git.wiggleverse.org/wiggleverse/wiggleverse-ecomm-prototype>
    (R01–R07; SPEC.md, `.importlinter`, `platform/`, `domains/`)
  - Engineering handbook & Solution-Design standard:
    <https://git.wiggleverse.org/wiggleverse/engineering> (`README.md`,
    `solution-design/GUIDE.md`)
  - Capture session for Feature #1: ecomm session 0019 (2026-06-10)
