Skip to main content

What It Does

Handles the full image pipeline: client-side resize/compress, upload to Supabase Storage, and URL return for use in recipes, posts, and profiles.

User Flow

1

Pick Images

User picks images via ImagePickerStep component (uses expo-image-picker, full-screen with album selection).
2

Client-Side Processing

All images are converted to JPEG via expo-image-manipulator (handles HEIC/HEIF from iOS). Compressed at 0.8 quality. iOS ph:// URIs resolved to file:// via MediaLibrary.getAssetInfoAsync().
3

Upload to Supabase Storage

useImageUpload hook reads the file as base64 via expo-file-system, converts to Uint8Array, uploads directly to Supabase Storage: supabase.storage.from(bucket).upload(path, uint8Array, { contentType: 'image/jpeg' }).
4

URL Returned

Returns public URL with cache-busting timestamp (?t={timestamp}) for use in recipe/post/profile forms.
The mobile app uploads images directly to Supabase Storage — it does NOT go through the POST /api/upload/image web API route. The web API route exists for the web client (Phase 2). File size validation (max 5MB via MAX_IMAGE_SIZE_BYTES from shared package) happens client-side before upload.

Web API Route (Phase 2)

POST /api/upload/image
  • Validates type parameter: avatar, recipe, post, coverPhoto
  • Validates MIME type (JPEG, PNG, WebP only) and file size
  • Generates unique filename
  • Uploads to appropriate Supabase Storage bucket
  • Returns public URL

Storage Buckets

BucketPublicPurposePath Pattern
avatarstrueUser profile photos{user_id}/{filename}
recipe-imagestrueRecipe sample photos{user_id}/{filename}
post-imagestruePost photos{user_id}/{filename}
cover-photostrueProfile cover photos{user_id}/{filename}
All 4 buckets are public with no file_size_limit or allowed_mime_types restrictions at the bucket level. Validation happens in the API route.

Storage RLS Policies

  • All buckets: Public SELECT (read)
  • avatars, post-images, cover-photos: Owner-only INSERT (upload to own folder via auth.uid() = storage.foldername(name)[1])
  • recipe-images: Authenticated INSERT (any authenticated user can upload — no folder ownership check)
  • All buckets: Owner-only UPDATE and DELETE (via storage.foldername(name)[1] = auth.uid())

Mobile Utilities

UtilityPurpose
src/utils/imageOptimization.tsClient-side resize/compress before upload
src/utils/imagePreloading.tsPreload images for smooth UX
src/hooks/useImageUpload.tsUpload hook with progress tracking
src/hooks/useImageDimensions.tsCalculate display dimensions

Mobile Components

ComponentPurpose
ImagePickerStepFull-screen image picker step (album selection, multi-select, reorder)
ImagePickerImage selection UI
ProfileImagePickerAvatar/cover photo picker
DraggableImageListReorderable image list
EditableImageCarouselCarousel with edit controls
ReadOnlyImageGalleryDisplay-only gallery