diff --git a/specs/SD-0001-mvp-sign-up-and-single-storefront.md b/specs/SD-0001-mvp-sign-up-and-single-storefront.md index 683aea9..f59fbf2 100644 --- a/specs/SD-0001-mvp-sign-up-and-single-storefront.md +++ b/specs/SD-0001-mvp-sign-up-and-single-storefront.md @@ -9,7 +9,7 @@ status: graduated | **Author(s)** | Claude (session ecomm-0020), for Ben Stull | | **Reviewers / approvers** | Ben Stull | | **Status** | `approved` | -| **Version** | v0.2.0 | +| **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** @@ -18,6 +18,7 @@ status: graduated | --- | --- | --- | --- | | 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 | --- @@ -247,10 +248,13 @@ 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. + 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*. @@ -592,12 +596,12 @@ flowchart LR SF[storefronts
storefront · membership] end subgraph platform - DB[db
SQLite WAL + migrations] + DB[db
Postgres + migrations] MAIL[mailer
port: log / SMTP] DEPS[deps
request wiring] end end - STORE[(SQLite file)] + STORE[(PostgreSQL
Cloud SQL · dev: Docker)] RELAY[SMTP relay
deployed envs only] SPA -->|JSON /api/*| BFF @@ -628,8 +632,11 @@ imports flow only downward; domains are imported via their package surface 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/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 @@ -647,10 +654,10 @@ management arrives. | 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 | +| `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: @@ -712,7 +719,7 @@ sequenceDiagram participant ACC as accounts participant SF as storefronts participant M as mailer (port) - participant DB as SQLite + participant DB as PostgreSQL V->>SPA: enter email, "Send code" SPA->>BFF: POST /api/auth/request-code {email} @@ -748,7 +755,7 @@ sequenceDiagram participant SPA as Admin SPA participant BFF as REST BFF participant SF as storefronts - participant DB as SQLite + participant DB as PostgreSQL Mer->>SPA: name (optional), "Create storefront" SPA->>BFF: POST /api/storefronts {name?} @@ -787,11 +794,11 @@ Idempotent; no server state to destroy (sessions are cookie-borne, §6.2). sequenceDiagram participant Op as Operator / Developer participant App as ecomm process - participant DB as SQLite (empty file or absent) + participant DB as PostgreSQL (empty database) actor Mer as First merchant Op->>App: start (dev script · flotilla deploy) - App->>DB: open/create; apply pending migrations in order (INV-7) + 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) @@ -799,14 +806,22 @@ sequenceDiagram - **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 + (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. @@ -823,16 +838,17 @@ sequenceDiagram - Data classification: emails are personal data — the only personal data held (§6.3); code hashes are short-lived secrets; both excluded from logs in deployed environments. - - Secrets are references, never bytes (handbook §6.3): session secret and - SMTP credentials live in Secret Manager, named by deployment config - (INV-8). + - 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. SQLite/WAL with per-request connections is far above - this bar; revisit at real scale (§7.4 R-3). + single-VM deployment. PostgreSQL with a small connection pool is far above + this bar. - **Availability & resilience:** one VM, one process (standard flotilla - stack); deploys are fail-stop with `/healthz` gating; brief deploy-window - unavailability is accepted pre-v1. + 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 @@ -847,7 +863,7 @@ sequenceDiagram | Decision | Chosen | Alternatives considered | Why chosen | | --- | --- | --- | --- | | Overall shape | Carry forward the prototype's 4-layer modular monolith + React SPA | Heavier re-architecture (Postgres, services, SSR); resume prototype code; new stack | Proven by R01–R07 against this exact domain; smallest bootstrap surface (§1.6 outcome 3); rebuild wants clean *code*, not unproven *shape* (§2) | -| Datastore | SQLite (WAL), no ORM, forward-only `.sql` migrations | Postgres (managed or on-VM) | Single-VM flotilla standard; zero standing service to provision per environment — bootstrap stays one gesture; prototype precedent. Revisit at scale (R-3) | +| 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 | @@ -863,7 +879,9 @@ sequenceDiagram The prototype's discipline, inherited: - **Scenario-bound end-to-end tests** drive the app through its HTTP surface - (FastAPI TestClient on a fresh temp DB). Where a corpus scenario covers the + (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. @@ -878,7 +896,8 @@ The prototype's discipline, inherited: (§6.5) unit-tested; component/E2E browser tests deferred until there is UI beyond forms. - **Gate:** `scripts/check.sh` = `lint-imports` (layer contract, INV-6) + - `pytest` + frontend typecheck/build — locally pre-merge and in CI. "Tested" + `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. @@ -892,9 +911,9 @@ The prototype's discipline, inherited: - **Migration fails at startup** → process refuses to start (INV-7); flotilla's fail-stop deploy keeps the prior version serving; operator fixes forward (no down-migrations — MVP migrations are additive). -- **DB file lost/corrupted** → restore from VM disk snapshot (the standard - flotilla VM's recovery story); accepted pre-v1 bar — an explicit backup - cadence is deferred and logged (§9 Q-2). +- **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). @@ -927,10 +946,12 @@ Slice tasks live in each slice's plan, not here. - **Depends on:** — - **Ships:** repo scaffold (backend 4-layer skeleton + import-linter - contract, frontend Vite/React shell, `scripts/check.sh`, CI); `platform/db` - with the migration runner (INV-7) and migration `0001` (the §6.3 schema); - `/healthz`; `scripts/dev.sh`; first cut of `docs/BOOTSTRAP.md` (localhost - section). + 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 @@ -962,8 +983,9 @@ Slice tasks live in each slice's plan, not here. - **Depends on:** SLICE-3; **operator gestures:** PPE + Prod provisioning via launch-app/flotilla (handbook §8.5 — the session hands the operator the - commands and waits), SMTP relay choice + credentials into Secret Manager - (§9 Q-1). + 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. @@ -984,7 +1006,8 @@ the bootstrap state; rollback is flotilla redeploy of the prior version | --- | --- | --- | | R-1: First-ever ecomm deploy — flotilla provisioning/deploy path unexercised for this app | M / H | Isolate in SLICE-4 with all product code already green; PPE before Prod; operator runs provisioning (handbook §8.5); wiggle-snip's deployment.toml as template | | R-2: OTC email deliverability (spam-foldering, relay limits) kills real sign-ups | M / H | Relay is deployment config (swap without code, INV-8); rehearsal on PPE with real mailboxes before Prod; honest `delivery_failed` surfacing (INV-9) makes failures visible, not silent | -| R-3: SQLite write concurrency at real scale | L (MVP) / M | WAL + short transactions now; INV-5/INV-6 keep the storage seam clean for a Postgres move; revisit at the first scale-bearing Feature | +| R-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 | @@ -1009,19 +1032,20 @@ PUC-2a/b (failed proof ≠ admission). | # | Question | Owner | Blocks | | --- | --- | --- | --- | | Q-1 | Which SMTP relay/provider serves PPE and Prod? (Deployment-shape choice — config + Secret Manager entry, not code; the app speaks SMTP either way.) | Operator (Ben) | SLICE-4 deploy config only | -| Q-2 | Backup cadence for the Prod SQLite file beyond VM disk snapshots (and when does it graduate from "accepted pre-v1 bar", §6.9)? | Operator (Ben) | nothing in this Feature; revisit before real merchant data accrues | | Q-3 | Account deletion / data export (OHM: Exit & Portability) — deliberately deferred from the MVP's honest bar; which Feature picks it up? | ecomm team | nothing here; must be captured as a Feature before public launch | **Resolved** | # | Decision | Resolution | Date | | --- | --- | --- | --- | -| D-1 | Architecture for the clean rebuild | Carry forward the prototype's proven shape (4-layer monolith, SQLite/WAL, React SPA, REST BFF), invert seed + admission gate, defer GraphQL/OAuth — §2, §6.7 | 2026-06-10 | +| D-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