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
Pick Images
User picks images via
ImagePickerStep component (uses expo-image-picker, full-screen with album selection).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().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' }).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
typeparameter: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
| Bucket | Public | Purpose | Path Pattern |
|---|---|---|---|
avatars | true | User profile photos | {user_id}/{filename} |
recipe-images | true | Recipe sample photos | {user_id}/{filename} |
post-images | true | Post photos | {user_id}/{filename} |
cover-photos | true | Profile 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 viaauth.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
| Utility | Purpose |
|---|---|
src/utils/imageOptimization.ts | Client-side resize/compress before upload |
src/utils/imagePreloading.ts | Preload images for smooth UX |
src/hooks/useImageUpload.ts | Upload hook with progress tracking |
src/hooks/useImageDimensions.ts | Calculate display dimensions |
Mobile Components
| Component | Purpose |
|---|---|
ImagePickerStep | Full-screen image picker step (album selection, multi-select, reorder) |
ImagePicker | Image selection UI |
ProfileImagePicker | Avatar/cover photo picker |
DraggableImageList | Reorderable image list |
EditableImageCarousel | Carousel with edit controls |
ReadOnlyImageGallery | Display-only gallery |