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