Primitive spec
Media
The media-store primitive. Media holds the files and images everything else points at — product photos, site assets, the PDF that goes out for signature — uploaded straight to S3 with a presigned URL, served from a CDN, and referenced by opaque id so no other primitive ever proxies a byte.
PUT and the Demerzel media library browses, copies URLs, and deletes. Image processing (thumbnails, transforms) and multipart upload are the next slices.
What it owns
Media owns one thing: a tenant's blobs and where they live. A MediaAsset row is the durable handle — the S3 object key, content type, size, visibility, and status — and the bytes themselves never flow through the application. The browser uploads them straight to S3 with a short-lived signed URL, and reads come back through a CDN.
Media does not own what an asset means. A product photo's link to a Hardin variant, a signed PDF's link to a Sign envelope, a hero image on a Radiant ui_view — those references live in the owning primitive, which holds only the opaque med_* id.
Concepts
- MediaAsset
- The durable handle for one stored file. Carries
filename,contentType(validated against an allow-list),sizeBytes, a derivedstorageKey({tenant}/{assetId}/{filename}), an optionalchecksum, imagewidthPx/heightPx, and free-formmetadata(alt text, caption). Soft-deletable; referenced elsewhere only by itsmed_*id. - Presigned upload
- Three steps, no byte ever touching the app. The client asks for a ticket (
POST /media/v1/uploads) and gets back a presigned S3PUTURL (15-minute TTL) against a freshPENDINGasset; the browser uploads directly to S3; thenfinalizeflips the asset toREADY. The application is a stateless broker of signatures, not a file proxy. - Visibility
PUBLICassets serve from a stable CloudFront URL (the S3 object stays private, reached via Origin Access Control);PRIVATEassets are never exposed on a stable URL — each read mints a short-lived presignedGET. The default isPRIVATE.- Status
PENDING(row created, presigned URL handed out, upload not yet confirmed) →READY(finalized). A later slice will reconcile the real S3 object (HEAD for size + checksum) rather than trusting the finalize call.- S3 + CloudFront
- The store is an SSE-encrypted, versioned, fully-private S3 bucket; serving is a CloudFront distribution with OAC so only the CDN can read the bucket. Deployed in Terraform alongside the app, wired through
MEDIA_S3_BUCKETandMEDIA_PUBLIC_BASE_URL.
API surface
All endpoints are versioned under /media/v1/, return RFC 7807 problem details on error, and read tenantId from the bearer token. The application never receives the file bytes — only the metadata around them.
Quick reference
| Method | Path | Purpose |
|---|---|---|
| POST | /media/v1/uploads | Request an upload ticket — returns a presigned S3 PUT URL against a new PENDING asset. |
| POST | /media/v1/uploads/{id}/finalize | Confirm the upload completed; flips the asset to READY and returns its serving URL. |
| GET | /media/v1/assets | List assets (filter by ?visibility=, ?contentType=). |
| GET | /media/v1/assets/{id} | Fetch one asset with its resolved serving URL (CDN or presigned). |
| DELETE | /media/v1/assets/{id} | Soft-delete an asset. |
Example: upload an image
Ask for a ticket; you get a presigned URL to PUT the bytes straight to S3:
POST /media/v1/uploads
Content-Type: application/json
Authorization: Bearer <token>
{ "filename": "hero.jpg", "contentType": "image/jpeg", "sizeBytes": 248137, "visibility": "PUBLIC" }
→ 201 Created
{
"assetId": "med_01JC…",
"uploadUrl": "https://…s3…/?X-Amz-Signature=…",
"storageKey": "t_01J8K2…/med_01JC…/hero.jpg",
"expiresInSeconds": 900
}
// browser PUTs the file bytes directly to uploadUrl (S3), then:
POST /media/v1/uploads/med_01JC…/finalize
→ 200 OK { "status": "READY", "url": "https://d111….cloudfront.net/t_01J8K2…/med_01JC…/hero.jpg" }
How it fits with the rest
flowchart LR
Cl[Browser] -- presigned PUT --> S3[(S3 bucket)]
Cl -- request / finalize --> Md(Media)
Md -- presign --> S3
CF[CloudFront] -- OAC read --> S3
Sg[Sign] -. med_* source / signed PDF .-> Md
R[Radiant ui_view] -. med_* .-> Md
Ha[Hardin product image] -. med_* .-> Md
Media is referenced, never depended on. Sign points an envelope at the med_* document to sign and records the completed PDF back as a new asset; Radiant ui_view assets and Hardin product images carry a med_* id; Speaker templates embed one. The contract is the same opaque-id-in convention as everywhere else — the owning primitive holds the reference, Media holds the bytes.