0881a4cdf0
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
854 lines
41 KiB
Markdown
854 lines
41 KiB
Markdown
# 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** | `draft` |
|
||
| **Version** | v0.1.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 |
|
||
|
||
---
|
||
|
||
## 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 door is designed to stay open (§1.7, #1 acceptance).
|
||
- 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<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/>SQLite WAL + migrations]
|
||
MAIL[mailer<br/>port: log / SMTP]
|
||
DEPS[deps<br/>request wiring]
|
||
end
|
||
end
|
||
STORE[(SQLite file)]
|
||
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`** — 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": "<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 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
|
||
|
||
<!-- §6.6 pending -->
|
||
|
||
### 6.7 Key decisions & alternatives considered
|
||
|
||
<!-- §6.7 pending -->
|
||
|
||
### 6.8 Testing strategy
|
||
|
||
<!-- §6.8 pending -->
|
||
|
||
### 6.9 Failure modes, rollback & flags
|
||
|
||
<!-- §6.9 pending -->
|
||
|
||
## 7. Delivery Plan
|
||
|
||
### 7.1 Approach / strategy
|
||
|
||
<!-- §7.1 pending -->
|
||
|
||
### 7.2 Slicing plan
|
||
|
||
<!-- §7.2 pending -->
|
||
|
||
### 7.3 Rollout / launch plan
|
||
|
||
<!-- §7.3 pending -->
|
||
|
||
### 7.4 Risks & mitigations
|
||
|
||
<!-- §7.4 pending -->
|
||
|
||
## 8. Traceability matrix
|
||
|
||
<!-- §8 pending -->
|
||
|
||
## 9. Open Questions & Decisions log
|
||
|
||
<!-- §9 pending -->
|
||
|
||
## 10. Glossary & References
|
||
|
||
<!-- §10 pending -->
|