spec(SD-0002 §6.3.1): storage design — Cloud SQL additive, per-env GCS bucket, key layout, app-served images with CDN seam
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -743,6 +743,57 @@ meaning are #15's design. In this MVP a non-`standalone` `Type` or a
|
|||||||
non-empty `Component n` column is a row error ("kits arrive in a coming
|
non-empty `Component n` column is a row error ("kits arrive in a coming
|
||||||
release").
|
release").
|
||||||
|
|
||||||
|
#### 6.3.1 Storage: GCP resources, layout & serving
|
||||||
|
|
||||||
|
**Catalog data — Cloud SQL for PostgreSQL, no new resource.** The §6.3
|
||||||
|
tables are additive forward-only migrations in the existing per-environment
|
||||||
|
Cloud SQL instance (PPE: `ecomm-ppe-pg`; prod: its sibling when prod stands
|
||||||
|
up). At MVP scale (≤ 5k products × ~30 columns + run/error records) this is
|
||||||
|
tens of megabytes. Image *bytes* never enter Postgres (§6.7).
|
||||||
|
|
||||||
|
**Image bytes — one GCS bucket per environment**, project
|
||||||
|
`wiggleverse-ecomm`, same region as the VM, Standard class, uniform
|
||||||
|
bucket-level access, versioning off, **no public access**. Provisioning is a
|
||||||
|
flotilla/launch-app gesture (a `provision-bucket` sibling of
|
||||||
|
`provision-datastore`), run by the operator when the first slice needing it
|
||||||
|
reaches PPE (handbook §8.5); the bucket name reaches the app via deployment
|
||||||
|
config (SD-0001 INV-8). The VM's service account gets
|
||||||
|
`roles/storage.objectAdmin` scoped to this bucket only. Local dev and tests
|
||||||
|
use the `objectstore` port's local-disk adapter under the identical key
|
||||||
|
scheme — no GCS, no emulator.
|
||||||
|
|
||||||
|
**Key layout** — tenant-scoped, immutable, cache-friendly:
|
||||||
|
|
||||||
|
```
|
||||||
|
gs://wiggleverse-ecomm-<env>-media/
|
||||||
|
storefronts/{storefront_id}/product-images/{image_id}/original ← bytes as fetched
|
||||||
|
storefronts/{storefront_id}/product-images/{image_id}/thumb.webp ← ~160px
|
||||||
|
storefronts/{storefront_id}/product-images/{image_id}/card.webp ← ~480px
|
||||||
|
storefronts/{storefront_id}/product-images/{image_id}/detail.webp ← ≤1600px
|
||||||
|
import-drafts/{draft_id}/upload.csv ← preview blobs
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `storefronts/{storefront_id}/` prefix extends INV-14 into storage.
|
||||||
|
- Keys are `image_id`-addressed and **written once, never mutated** — a
|
||||||
|
changed image is a new image id, so every object is immutable and serves
|
||||||
|
with `Cache-Control: public, max-age=31536000, immutable`.
|
||||||
|
- Renditions normalize to **WebP**; the `original` keeps its source format
|
||||||
|
(provenance; renditions can be re-derived if the bar or sizes change).
|
||||||
|
- `import-drafts/` carries a **GCS lifecycle rule: delete after 1 day** —
|
||||||
|
the deterministic safety net behind app-level draft deletion (PUC-3a), so
|
||||||
|
a crash can never leak a merchant's uploaded file.
|
||||||
|
|
||||||
|
**Serving — app-served with a CDN seam (decided, §6.7).** Surfaces load
|
||||||
|
images from `GET /api/products/images/{image_id}/{rendition}` (§6.4): the
|
||||||
|
app authorizes (image → storefront, INV-14/16), streams from the bucket, and
|
||||||
|
sets the immutable cache headers. Exported `Image Src` URLs therefore live
|
||||||
|
on the **app's own domain** — they survive the `market.wiggleverse.org`
|
||||||
|
rebrand (#23) via ordinary redirects, and hosted-URL recognition (§6.3) is a
|
||||||
|
same-origin path parse. **The seam:** when shopper-facing storefronts (#24)
|
||||||
|
bring real image traffic, a CDN-fronted bucket (LB + Cloud CDN +
|
||||||
|
`images.<domain>`) takes over the same keys; only the URL builder and the
|
||||||
|
serving route change.
|
||||||
|
|
||||||
### 6.4 Interfaces & contracts
|
### 6.4 Interfaces & contracts
|
||||||
|
|
||||||
All under `/api/products/*`; session cookie + storefront membership required
|
All under `/api/products/*`; session cookie + storefront membership required
|
||||||
@@ -762,6 +813,7 @@ a spec change.
|
|||||||
| `GET /api/products/export` | `?status=all\|active\|draft\|archived` | `200 text/csv` (streamed) — canonical format, hosted image URLs (PUC-9, INV-12) | `409 empty_catalog` |
|
| `GET /api/products/export` | `?status=all\|active\|draft\|archived` | `200 text/csv` (streamed) — canonical format, hosted image URLs (PUC-9, INV-12) | `409 empty_catalog` |
|
||||||
| `GET /api/products/sample.csv` | — | `200 text/csv` — the documented sample (PUC-11) | — |
|
| `GET /api/products/sample.csv` | — | `200 text/csv` — the documented sample (PUC-11) | — |
|
||||||
| `GET /api/products/summary` | — | `200 {product_count, image_problem_count, latest_run_id}` — drives the Products page header + notice band (§5.2) | — |
|
| `GET /api/products/summary` | — | `200 {product_count, image_problem_count, latest_run_id}` — drives the Products page header + notice band (§5.2) | — |
|
||||||
|
| `GET /api/products/images/{image_id}/{rendition}` | `rendition ∈ original\|thumb\|card\|detail` | `200 image bytes` — storefront-authorized, streamed from the bucket, `Cache-Control: immutable` (§6.3.1, INV-16) | `404` (incl. wrong storefront) · `409 not_fetched` (status ≠ fetched → SPA shows placeholder) |
|
||||||
|
|
||||||
### 6.5 Per–Product-Use-Case design
|
### 6.5 Per–Product-Use-Case design
|
||||||
|
|
||||||
@@ -968,6 +1020,8 @@ endpoints; PUC-11 (sample CSV) is a static documented asset served at
|
|||||||
| Progress delivery | SPA polls run detail | WebSockets/SSE | One poll endpoint, no connection infra; seconds-stale progress is fine for this UX. |
|
| Progress delivery | SPA polls run detail | WebSockets/SSE | One poll endpoint, no connection infra; seconds-stale progress is fine for this UX. |
|
||||||
| Notifications | In-app only | Email summaries via SmtpMailer | Operator decision; revisit if imports go async/long. |
|
| Notifications | In-app only | Email summaries via SmtpMailer | Operator decision; revisit if imports go async/long. |
|
||||||
| Object storage | New `platform/objectstore` port (local/GCS) | Store images as DB blobs; reuse a public bucket directly in domain code | Follows the mailer port pattern (SD-0001); DB blobs bloat backups and can't serve renditions efficiently. |
|
| Object storage | New `platform/objectstore` port (local/GCS) | Store images as DB blobs; reuse a public bucket directly in domain code | Follows the mailer port pattern (SD-0001); DB blobs bloat backups and can't serve renditions efficiently. |
|
||||||
|
| Image serving | App-served route + private bucket, CDN seam named (§6.3.1) | Public bucket with direct GCS URLs; CDN + custom domain now; signed URLs | Exported URLs stay on the app domain (rebrand-safe #23, hosted-URL recognition trivial); bucket stays private; CDN waits for shopper traffic (#24). Signed URLs ruled out — expiry poisons the round-trip (INV-12). |
|
||||||
|
| Rendition format | WebP renditions, original kept as-source | Keep source format everywhere; AVIF | WebP: universal browser support + good compression; original retained so renditions can be re-derived (incl. a future AVIF tier). |
|
||||||
|
|
||||||
### 6.8 Testing strategy
|
### 6.8 Testing strategy
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user