The website builder’s next honest need is images. But an image pipeline that trusts an upload is worse than none:
the declared Content-Type is a claim, not a fact. This step builds the lifecycle in two halves — quarantine and
proof, then reference and publish — held to the same bar as everything else.
What I built
- Quarantine, then proof.
POST {P}/assetstakes the raw body (bounded), a file name and an idempotency key; the bytes go toquarantine/{project}/{env}/{asset}before a transaction opens — no database transaction spans the object store — then the asset row and its job commit together. A worker claims the job on its own lease and verifies on a dedicated image pool: sniff the actual signature (PNG, JPEG, WebP only), decode under hard byte and pixel limits, re-encode the proven image with metadata stripped, hash it, and promote it topublic/...— state and job outcome in one commit, so a lost lease writes nothing stale. - The
assetfield. Content types gain a field kind that names uploads: one, or a bounded list of distinct ids — areferencewithout a target collection. Ids are canonical UUIDs collected wherever fields nest (objects, lists, blocks), and every revision’s references persist inasset_references, which is what retention will read the other way later. - Publish gating. A draft may name a quarantined or even a rejected upload — content is written before the
verifier finishes — but
bad_assetjoins the changeset’s items throughasset_referencestoassetsand the first non-usablerow fails the release:asset_not_usable, with “still being verified” versus “replace the upload” depending on state. Scheduled publishes fail the same way. - The media origin.
GET /media/v1/{project}/{environment}/{asset}streams thepublic/...object straight from the store: no sign-in, no asset-row read — object existence is the proof it verified, because the worker is the only writer and only promotes bytes it re-encoded. The detectedContent-Typerides as an object attribute; the answer carries immutableCache-Control,nosniffand the object’s ETag. Quarantined, rejected, missing and malformed ids all answer 404, and nothing underquarantine/can be addressed at all.
Does it actually work?
- The end-to-end spec tells the story: fake upload → draft saves fine → changeset approves → publish refused
asset_not_usable, refused again after the rejection settles, origin 404. Then a real upload → verified usable → publish succeeds → the origin answers 200image/png, immutable,nosniff, PNG magic bytes — to a request with no cookie at all. A made-up asset id is refused at save withasset_not_found. - The Studio builds it: the field picker offers “Uploaded file” with one-or-list options, and the entry editor takes an upload’s id (the real media picker is the next step).
- The load run covers it: the assets scenario now reads the public origin alongside uploads — 10,991
requests at 1,048/s, p99 116ms against a 500ms budget, zero failed, including
media 200and the immutable header check. - 317 Rust tests pass, and the testbed drives the whole lifecycle through a real browser: upload, quarantine,
usable: image/png, thenserved: 200 image/pngfrom the origin — andserved: 404for the fake.
What surprised me
- Row locks have a permission price. Mirroring the entry-reference precheck with
FOR KEY SHAREfailed with permission denied — the API role may not UPDATEassets(state changes are the worker’s). The fix is better than the copy would have been: no lock at all. Uploads have no delete path yet, andON DELETE RESTRICTis the guarantee when retention arrives. - A refusal can race the proof. The first test published while the asset was quarantined — and the worker beat it to usable, so the refusal never fired. The deterministic version uses a fake: quarantined-then-rejected is non-usable either way, so the refusal isn’t a race condition in the test.