Cloudflare R2 (resource media)
Resource media is large and public, so it lives in the emaw-public R2 bucket (served from r2.everymanawarrior.cc with a 1-year edge cache and no egress fees) rather than in Azure Blob. Cover images stay in Azure Blob.
| Code | resources/r2.py (presign), resources/staff_views.py, resources/views.py; admin app src/api/directUpload.ts |
| Runbooks | resources/README.md, docs/r2-setup.md (bucket public access, CORS JSON, cache rule, troubleshooting) |
| Env | R2_ACCOUNT_ID, R2_BUCKET, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY (API token with Object Read & Write on the bucket), R2_PUBLIC_BASE_URL (default https://r2.everymanawarrior.cc) |
Upload flow
- The backend never proxies the bytes; the browser uploads directly, so the bucket's CORS rule must allow
PUTfrom the admin origins. - Public payloads carry the full
media.url; the website never builds R2 URLs and needs no R2 env var. - Until all four credentials are set, the presign endpoint returns 503 and the admin app shows an upload error.
Keys are immutable
Replacing a file uploads a new key. Never overwrite a published key: the edge cache keeps serving the old bytes for up to a year, and purging per-URL is manual. The admin app follows this automatically; the rule matters if you touch the bucket with wrangler r2 object put by hand.
Changing the public domain
Set R2_PUBLIC_BASE_URL; existing resources are re-serialised with the new base because only the key is stored.