SD-0001 v0.3.0: PostgreSQL everywhere (Cloud SQL deployed, Docker container in dev) #1

Merged
ben.stull merged 1 commits from spec/sd-0001-v0.3.0-postgres into main 2026-06-10 15:09:37 +00:00
@@ -9,7 +9,7 @@ status: graduated
| **Author(s)** | Claude (session ecomm-0020), for Ben Stull | | **Author(s)** | Claude (session ecomm-0020), for Ben Stull |
| **Reviewers / approvers** | Ben Stull | | **Reviewers / approvers** | Ben Stull |
| **Status** | `approved` | | **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) (R01R07) · Reference: Shopify first-run experience · Supersedes: — | | **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) (R01R07) · Reference: Shopify first-run experience · Supersedes: — |
**Change log** **Change log**
@@ -18,6 +18,7 @@ status: graduated
| --- | --- | --- | --- | | --- | --- | --- | --- |
| 2026-06-10 | v0.1.0 | Initial draft (session ecomm-0020) | Claude | | 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.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.26.9, 7.2, 7.4, 9 touched | Ben + Claude |
--- ---
@@ -247,10 +248,13 @@ shape and inverts its *disqualifying* assumption:
- **Carry forward** (validated by R01R07 of the prototype): a four-layer - **Carry forward** (validated by R01R07 of the prototype): a four-layer
Python backend (`entrypoint → api → domains → platform`) with the layer Python backend (`entrypoint → api → domains → platform`) with the layer
contract mechanically enforced; SQLite (WAL) with forward-only numbered contract mechanically enforced; relational persistence through forward-only
migrations and no ORM; a React/Vite SPA admin talking to a screen-shaped numbered migrations and no ORM — with one deliberate engine swap from the
REST BFF; scenario-bound end-to-end tests (one test per BDD scenario ID); prototype's SQLite to **PostgreSQL in every environment** (Cloud SQL when
OHM concepts cited inline next to the rules they ground. 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 - **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 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*. with one hard-coded store and one invited owner; ecomm is born *empty*.
@@ -592,12 +596,12 @@ flowchart LR
SF[storefronts<br/>storefront · membership] SF[storefronts<br/>storefront · membership]
end end
subgraph platform subgraph platform
DB[db<br/>SQLite WAL + migrations] DB[db<br/>Postgres + migrations]
MAIL[mailer<br/>port: log / SMTP] MAIL[mailer<br/>port: log / SMTP]
DEPS[deps<br/>request wiring] DEPS[deps<br/>request wiring]
end end
end end
STORE[(SQLite file)] STORE[(PostgreSQL<br/>Cloud SQL · dev: Docker)]
RELAY[SMTP relay<br/>deployed envs only] RELAY[SMTP relay<br/>deployed envs only]
SPA -->|JSON /api/*| BFF 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 account↔storefront membership (INV-4, INV-5), including the entry-routing
answer ("which storefront, if any, does this account have?"). Must never answer ("which storefront, if any, does this account have?"). Must never
mint identity. mint identity.
- **`platform/db`** — SQLite WAL connection + forward-only migration runner - **`platform/db`** — PostgreSQL connections (psycopg, small pool) + the
(INV-7); owns the schema lifecycle, no business rules. 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 - **`platform/mailer`** — a one-method port (`send(to, subject, body)`) with
two adapters: `LogMailer` (localhost/dev and tests — the message lands in two adapters: `LogMailer` (localhost/dev and tests — the message lands in
the app log / a test inbox) and `SmtpMailer` (PPE/Prod — relay coordinates 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 | | Entity | Owned by | Key fields | System of record |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `account` | accounts | `id`, `email` (unique, normalized — INV-2), `created_at` | 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) | SQLite | | `auth_code` | accounts | `id`, `email`, `code_hash`, `expires_at`, `attempts`, `consumed_at`, `created_at` (INV-3) | PostgreSQL |
| `storefront` | storefronts | `id`, `name`, `created_at` | SQLite | | `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)` | SQLite | | `storefront_membership` | storefronts | `account_id` (FK), `storefront_id` (FK), `role` (MVP: always `'owner'`), `created_at`; PK `(account_id, storefront_id)` | PostgreSQL |
Notes: Notes:
@@ -712,7 +719,7 @@ sequenceDiagram
participant ACC as accounts participant ACC as accounts
participant SF as storefronts participant SF as storefronts
participant M as mailer (port) participant M as mailer (port)
participant DB as SQLite participant DB as PostgreSQL
V->>SPA: enter email, "Send code" V->>SPA: enter email, "Send code"
SPA->>BFF: POST /api/auth/request-code {email} SPA->>BFF: POST /api/auth/request-code {email}
@@ -748,7 +755,7 @@ sequenceDiagram
participant SPA as Admin SPA participant SPA as Admin SPA
participant BFF as REST BFF participant BFF as REST BFF
participant SF as storefronts participant SF as storefronts
participant DB as SQLite participant DB as PostgreSQL
Mer->>SPA: name (optional), "Create storefront" Mer->>SPA: name (optional), "Create storefront"
SPA->>BFF: POST /api/storefronts {name?} SPA->>BFF: POST /api/storefronts {name?}
@@ -787,11 +794,11 @@ Idempotent; no server state to destroy (sessions are cookie-borne, §6.2).
sequenceDiagram sequenceDiagram
participant Op as Operator / Developer participant Op as Operator / Developer
participant App as ecomm process participant App as ecomm process
participant DB as SQLite (empty file or absent) participant DB as PostgreSQL (empty database)
actor Mer as First merchant actor Mer as First merchant
Op->>App: start (dev script · flotilla deploy) 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) App-->>Op: /healthz 200 (migrations current)
Mer->>App: sign up → create storefront → admin (the ordinary PUC-2/4/6 flows) 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) 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 - **Implementation:** identical in all three environments; only configuration
differs (INV-8): **localhost**`scripts/dev.sh` from a clean checkout differs (INV-8): **localhost**`scripts/dev.sh` from a clean checkout
(venv + deps, backend on :8000, Vite on :5173 proxying `/api`); mailer is (documented prerequisite: Docker): it first brings up the dev datastore —
`LogMailer`, so the one-time code appears in the backend log (the "local a **single PostgreSQL container** (one-service Docker Compose, pinned to
dev channel" of PUC-10 — auth itself is not stubbed; the same issue/verify the Cloud SQL major version, named volume, healthcheck; the app itself
path runs). **PPE/Prod** — provisioned and deployed through stays native for the fast dev loop) — waits for healthy, then starts the
flotilla/launch-app (operator gesture, handbook §8.5); mailer is backend (venv + deps, :8000) and Vite (:5173 proxying `/api`);
`SmtpMailer` with relay coordinates and secrets resolved from Secret `docker compose down -v` is the one-command reset-to-empty gesture for
Manager via deployment config; migrations apply at startup under rehearsing the bootstrap. Mailer is `LogMailer`, so the one-time code
flotilla's fail-stop deploy, gated by `/healthz`. The bring-up runbook 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`) (per-environment: prerequisites, the one gesture, how to watch `/healthz`)
ships in-repo as `docs/BOOTSTRAP.md` — the documented gesture BUC-5 ships in-repo as `docs/BOOTSTRAP.md` — the documented gesture BUC-5
demands. demands.
@@ -823,16 +838,17 @@ sequenceDiagram
- Data classification: emails are personal data — the only personal data - Data classification: emails are personal data — the only personal data
held (§6.3); code hashes are short-lived secrets; both excluded from held (§6.3); code hashes are short-lived secrets; both excluded from
logs in deployed environments. logs in deployed environments.
- Secrets are references, never bytes (handbook §6.3): session secret and - Secrets are references, never bytes (handbook §6.3): session secret,
SMTP credentials live in Secret Manager, named by deployment config database credentials, and SMTP credentials live in Secret Manager, named
(INV-8). by deployment config (INV-8).
- **Performance & scale:** MVP traffic is near-zero; the bar is honesty, not - **Performance & scale:** MVP traffic is near-zero; the bar is honesty, not
throughput — interactive responses well under a second on the standard throughput — interactive responses well under a second on the standard
single-VM deployment. SQLite/WAL with per-request connections is far above single-VM deployment. PostgreSQL with a small connection pool is far above
this bar; revisit at real scale (§7.4 R-3). this bar.
- **Availability & resilience:** one VM, one process (standard flotilla - **Availability & resilience:** one VM, one process (standard flotilla
stack); deploys are fail-stop with `/healthz` gating; brief deploy-window stack) against managed Cloud SQL (automated backups + point-in-time
unavailability is accepted pre-v1. 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 - **Observability:** structured request logs (no emails/codes in deployed
logs); `/healthz` reporting migration currency; auth events (code issued / logs); `/healthz` reporting migration currency; auth events (code issued /
verify ok / verify failed) logged with hashed identifiers — enough to see verify ok / verify failed) logged with hashed identifiers — enough to see
@@ -847,7 +863,7 @@ sequenceDiagram
| Decision | Chosen | Alternatives considered | Why chosen | | 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 R01R07 against this exact domain; smallest bootstrap surface (§1.6 outcome 3); rebuild wants clean *code*, not unproven *shape* (§2) | | 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 R01R07 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.1v0.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 | | 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 | | 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.00030004); prototype-proven; collapses the account-lifecycle question — no passwords to reset, verification inherent. OAuth deferred, addable under INV-2 | | Auth mechanism | Email + one-time code, passwordless | Passwords (+ reset/verification machinery); OAuth (Google/Apple) first | Corpus's primary path (14.01.00030004); 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: The prototype's discipline, inherited:
- **Scenario-bound end-to-end tests** drive the app through its HTTP surface - **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`); 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 PUCs without a corpus ID bind to the PUC (`test_puc_05_...`). One test per
scenario/PUC, including every unhappy path in §4. 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 (§6.5) unit-tested; component/E2E browser tests deferred until there is UI
beyond forms. beyond forms.
- **Gate:** `scripts/check.sh` = `lint-imports` (layer contract, INV-6) + - **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. 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 - **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. 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); - **Migration fails at startup** → process refuses to start (INV-7);
flotilla's fail-stop deploy keeps the prior version serving; operator fixes flotilla's fail-stop deploy keeps the prior version serving; operator fixes
forward (no down-migrations — MVP migrations are additive). forward (no down-migrations — MVP migrations are additive).
- **DB file lost/corrupted** → restore from VM disk snapshot (the standard - **Database loss/corruption** → Cloud SQL automated backups +
flotilla VM's recovery story); accepted pre-v1 bar — an explicit backup point-in-time recovery (enabled as part of SLICE-4 provisioning; D-8). In
cadence is deferred and logged (§9 Q-2). dev, the named volume is disposable by design (`docker compose down -v`).
- **Session-secret leak** → rotate the secret in Secret Manager + redeploy; - **Session-secret leak** → rotate the secret in Secret Manager + redeploy;
all sessions invalidate at once (accepted blast radius at MVP scale, all sessions invalidate at once (accepted blast radius at MVP scale,
§6.2). §6.2).
@@ -927,10 +946,12 @@ Slice tasks live in each slice's plan, not here.
- **Depends on:** — - **Depends on:** —
- **Ships:** repo scaffold (backend 4-layer skeleton + import-linter - **Ships:** repo scaffold (backend 4-layer skeleton + import-linter
contract, frontend Vite/React shell, `scripts/check.sh`, CI); `platform/db` contract, frontend Vite/React shell, `scripts/check.sh`, CI); the dev
with the migration runner (INV-7) and migration `0001` (the §6.3 schema); datastore compose file (single pinned PostgreSQL service, named volume);
`/healthz`; `scripts/dev.sh`; first cut of `docs/BOOTSTRAP.md` (localhost `platform/db` (psycopg + pool) with the migration runner (INV-7) and
section). 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 - **Definition of done:** clean checkout → `scripts/dev.sh` → app up on an
empty, self-migrated DB; `/healthz` green; check gate green in CI; the empty, self-migrated DB; `/healthz` green; check gate green in CI; the
bootstrap test skeleton runs migrate-from-empty + idempotent re-migrate 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 - **Depends on:** SLICE-3; **operator gestures:** PPE + Prod provisioning via
launch-app/flotilla (handbook §8.5 — the session hands the operator the launch-app/flotilla (handbook §8.5 — the session hands the operator the
commands and waits), SMTP relay choice + credentials into Secret Manager commands and waits), **Cloud SQL for PostgreSQL per environment** (with
(§9 Q-1). 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`, - **Ships:** `SmtpMailer` + config wiring (INV-8), `deployment.toml`,
deployed-environment hardening from §6.6 (Secure cookies, log hygiene), deployed-environment hardening from §6.6 (Secure cookies, log hygiene),
`docs/BOOTSTRAP.md` completed for PPE/Prod. `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-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-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-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 | | 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 | | # | 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-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 | | 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** **Resolved**
| # | Decision | Resolution | Date | | # | 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-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-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-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-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.00170024) and multi-store selection (14.01.0012/0014) are out; merchant lands straight on the admin | 2026-06-10 | | D-6 | Corpus first-run steps not adopted | Onboarding questionnaire / POS opt-in (14.01.00170024) 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 ## 10. Glossary & References