DocHub
Image migration from Firebase Storage to Cloudflare R2 with optimization and server-side upload endpoint

Cloudflare R2 Image Storage

Overview

Migrated all image storage from Firebase Storage to Cloudflare R2. Includes image optimization, database URL updates, and a server-side upload endpoint.

Storage Details

  • Provider: Cloudflare R2
  • Bucket: ricoya-images
  • Region: Asia Pacific
  • Storage Class: Standard
  • Free Tier: 10GB storage, 10 million reads/month

Image Inventory

Directory Files Purpose
menu-items/ 304 Food item photos
restaurants/ 20 Restaurant logos and banners
uploads/ 24 Delivery proofs, miscellaneous
Total 348

Optimization

All images processed before upload:

  • Images wider than 1200px resized to 1200px width (maintaining aspect ratio)
  • JPEG quality set to 85%
  • Small images left untouched
  • Result: 409MB original reduced to 247MB (40% reduction)

Database URL Migration

Updated all Firebase Storage URLs to R2 URLs via SQL:

  • 220 menu_items image_url rows updated
  • 10 restaurants image_url rows updated
  • 24 file_uploads download_url rows updated

Upload Endpoint

All file uploads go through the server-side endpoint at /api/uploads/route.ts.

Flow:

  1. Client sends file as FormData with optional recordUpload=true flag
  2. Server authenticates request via Supabase JWT
  3. File uploaded to R2 via uploadToR2(key, buffer, contentType)
  4. If recordUpload=true, server inserts a row into file_uploads table via supabaseAdmin
  5. Returns { url, key, fileUploadId }

Why server-side: Originally, fileUploadsRepo.ts inserted into file_uploads client-side using the browser Supabase client. This failed on Safari due to connection issues. Moved the insert to the server endpoint (March 2, 2026) which resolved the issue and centralizes all R2 operations.

Key fields stored in file_uploads:

  • name, size_bytes, content_type
  • storage_path (R2 key)
  • download_url (public R2 URL)
  • uploaded_by_uid, uploaded_by_email
  • description, uploaded_at

Note: The storage_type column was removed from the FileUpload model — it never existed in the database.

Access Configuration

  • Public access enabled via R2.dev subdomain
  • AWS CLI configured with dedicated profile for management
  • CORS configured in Cloudflare R2 dashboard (Settings > CORS Policy) to allow cross-origin image loading:
    • Allowed Origins: https://ricoya.ipnoelp.com, https://ricoya.net, http://localhost:3000
    • Allowed Methods: GET, HEAD
    • Allowed Headers: Content-Type, Range
    • Max Age: 86400s

Ownership

Account created under development credentials. Plan to transfer Cloudflare account ownership to client when ready for production handover.