A catalogue of soaps without photographs sells nothing, and the storefront that will display it has nothing to show. This is the pipeline the design has described since the start (docs/design/catalog.md § 5) and that nothing implements.
Already decided, not reopened
ADR 0004 settles the storage: local disk, behind a single-entry storage layer so object storage can be added later. This epic builds that entry, it does not choose it.
Decisions taken 2026-09-03
- Served sizes: 200, 600 and 1400 px on the long side, ratio kept — thumbnail, medium, large, as the design already names them.
- Served format: AVIF, with a fallback for browsers that accept none. Encoding is slower, and it happens once per image at upload; the bytes saved are paid on every visit.
- The browser resizes before uploading, above 2400 px on the long side only. Below it, the file goes as it is. A phone photograph is several megabytes for something served at 1400 px, and the upload is the slowest part of the merchant's day.
- The kept source is that ≤2400 px file, not the untouched original. The design promised re-derivation "without asking the merchant for the files again"; 2400 px keeps that promise for every web use, and gives up a 4K nobody displays. The design says so rather than implying an original it no longer has.
- JPEG only, both directions: the browser converts, the shop accepts nothing else. One decoding branch, less attack surface. Transparency is flattened onto white — and the design records what that costs, so the day a merchant asks why their cut-out product shows a white rectangle in the dark theme, the decision is findable and revisable.
- Minimum 800 px on the long side: below it the large size would be an upscale, and the limit comes from what is served rather than from a round number.
- Limits: 8 MB per file, 10 images per product. Enforced by the shop, never only by the browser — a client sends whatever it wants.
- Derivation happens after the upload answers — reversed on measurement, and recorded in backend ADR 0008. The claim above this line used to read "synchronous: three sizes take under a second"; the whole derivation takes 3.7 s on one core, so ten photographs would be most of a minute of waiting. The state lives in
product_images, one image is encoded at a time, and the shop serves the source until the sizes exist.
Design first
docs/design/catalog.md § 5 carries a TBD on the limit values and says nothing about dimensions, formats, the minimum, or what "original" now means. All of it goes in before the code.
Sub-issues
Sequential: there is nothing to show until there is something stored.
Out of scope
Images on content pages, image cropping or editing in the back office, and any CDN. Also out: the storefront that will display them — its own epic.
A catalogue of soaps without photographs sells nothing, and the storefront that will display it has nothing to show. This is the pipeline the design has described since the start (
docs/design/catalog.md§ 5) and that nothing implements.Already decided, not reopened
ADR 0004 settles the storage: local disk, behind a single-entry storage layer so object storage can be added later. This epic builds that entry, it does not choose it.
Decisions taken 2026-09-03
product_images, one image is encoded at a time, and the shop serves the source until the sizes exist.Design first
docs/design/catalog.md§ 5 carries a TBD on the limit values and says nothing about dimensions, formats, the minimum, or what "original" now means. All of it goes in before the code.Sub-issues
Sequential: there is nothing to show until there is something stored.
Out of scope
Images on content pages, image cropping or editing in the back office, and any CDN. Also out: the storefront that will display them — its own epic.