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, binding personal_media). Uploads now stream through the Worker: add-form.tsx POSTs each file to a new admin-gated app/api/upload/route.ts, which writes to R2 via the binding (env.personal_media.put) and returns the object's public r2.dev URL (R2_PUBLIC_URL). The old app/api/sign-upload route (Cloudinary signed direct-upload) was deleted. Image resize/format is now handled by the Cloudflare Images binding: app/image-loader.ts rewrites next/image sources to app/api/img/route.ts, which fetches the source over its public URL and transforms it via env.IMAGES.input(...).output({ format: "image/webp" }). The Images binding only exists in the Workers runtime, so the route catches the getCloudflareContext() failure outside it (same pattern as lib/db.ts) and streams the original bytes untransformed under next dev. Audio is served as the raw r2.dev URL. A bucket CORS policy (r2-cors.json, GET/HEAD from *) is required so the browser can fetch() audio bytes cross-origin for waveform decoding (waveform-seekbar.tsx) — Cloudinary sent permissive CORS headers automatically, a fresh R2 bucket does not. Stored posts.image_url / posts.music_url remain plain strings, so the DB schema is unchanged and legacy Cloudinary URLs still resolve (loader/transform route fall back to fetch).
  • 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.dev public 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.