2026-06-02infra
Media storage moved from Cloudinary to Cloudflare R2
- Change: Replaced Cloudinary as the image/music blob store with a Cloudflare R2 bucket (
personal-media, bindingpersonal_media). Uploads now stream through the Worker:add-form.tsxPOSTs each file to a new admin-gatedapp/api/upload/route.ts, which writes to R2 via the binding (env.personal_media.put) and returns the object's publicr2.devURL (R2_PUBLIC_URL). The oldapp/api/sign-uploadroute (Cloudinary signed direct-upload) was deleted. Image resize/format is now handled by the Cloudflare Images binding:app/image-loader.tsrewritesnext/imagesources toapp/api/img/route.ts, which fetches the source over its public URL and transforms it viaenv.IMAGES.input(...).output({ format: "image/webp" }). The Images binding only exists in the Workers runtime, so the route catches thegetCloudflareContext()failure outside it (same pattern aslib/db.ts) and streams the original bytes untransformed undernext dev. Audio is served as the rawr2.devURL. A bucket CORS policy (r2-cors.json, GET/HEAD from*) is required so the browser canfetch()audio bytes cross-origin for waveform decoding (waveform-seekbar.tsx) — Cloudinary sent permissive CORS headers automatically, a fresh R2 bucket does not. Storedposts.image_url/posts.music_urlremain plain strings, so the DB schema is unchanged and legacy Cloudinary URLs still resolve (loader/transform route fall back tofetch). - Why: Consolidate media on the same Cloudflare account the app already runs on (Workers + Hyperdrive + Images binding), remove the external Cloudinary dependency and its credentials, and use native bindings instead of a third-party signed-upload flow.
- Affected Modules:
wrangler.jsonc,cloudflare-env.d.ts,app/api/upload/route.ts(new),app/api/img/route.ts(new),app/api/sign-upload/route.ts(deleted),app/add/add-form.tsx,app/image-loader.ts,.dev.vars,.env - Trade-offs:
- Pro: One vendor for compute + storage + image transforms; no Cloudinary account/keys; uploads and transforms use first-class Worker bindings; r2.dev needs zero domain setup.
- Con: Uploads and image transforms now pass through the Worker (CPU + body-size limits) instead of being offloaded to a SaaS CDN; the
r2.devpublic URL is rate-limited and not intended for high-traffic production (swap to a custom domain later if needed); a one-time bucket-provisioning + public-access step is required.