Skip to main content

What It Does

Recipes are the atomic unit of Recipe Room — a complete set of Fujifilm camera settings with sample images, tags, and optional Lightroom adjustments. Users create, view, edit, and delete recipes.

User Flow — Create

1

Open Create Modal

Tap ”+” → modal app/create.tsx → select “Recipe” → app/recipe/new.tsx
2

Step 1: Images

Pick up to 10 images via ImagePickerStep (full-screen picker with album selection, multi-select). Reorder with DraggableImageList.
3

Step 2: Metadata

Title, description, camera model (CameraModelPicker — 22 supported models), film simulation (FilmSimulationPicker).
4

Step 3: Fujifilm Settings

FujifilmSettings component with SliderControl for all parameters (dynamic range, grain, white balance, tone curve, color, sharpness, noise reduction, clarity).
5

Step 4: Lightroom (Optional)

LightroomSettings component, toggled via ToggleSwitch.
6

Step 5: Tags & Links

Tags (TagInput with suggestions), tagged users (UserTagSelector), optional collection assignment (ContentSelector).
7

Submit

Images uploaded to Supabase Storage first → then supabase.rpc('create_recipe', {...}) with all data (atomic transaction).

User Flow — View

Tap recipe card anywhere → app/recipe/[id].tsxRecipeDetailView renders:
  • PhotoCarousel for sample images
  • DetailHeader with author info and follow button
  • RecipeSettings showing all Fujifilm parameters
  • DetailActionBar with favorite, repost, save, comment, share actions
  • CommentSection / CommentModal for comments
  • LinkedContentCard if recipe is linked to a post

User Flow — Edit

Owner taps edit → app/recipe/[id]/edit.tsx → pre-populated form (fetched via useRecipe(id)) → can modify all fields including images → supabase.rpc('update_recipe', {...}). Uses KeyboardAvoidingView with platform-specific behavior.

Screens & Components

Screen/ComponentPurpose
app/recipe/new.tsxRecipe creation flow
app/recipe/[id].tsxRecipe detail view
app/recipe/[id]/edit.tsxRecipe edit flow
RecipeDetailViewMain detail view component
FujifilmSettingsCamera settings form/display
LightroomSettingsOptional Lightroom adjustments
RecipeSettingsRead-only settings display
PhotoCarouselImage carousel with pagination dots
DetailActionBarFavorite/repost/save/comment/share bar
DetailHeaderAuthor info + follow button
CommentModalBottom sheet comment modal
ContentCardRecipe card in feeds/grids
ImagePickerStepFull-screen image picker (Step 1)
DraggableImageListReorderable image list
FilmSimulationPickerFilm sim selector
CameraModelPickerCamera model selector
TagInputTag entry with suggestions
UserTagSelector@mention user tagging
SliderControlNumeric slider for settings
ToggleSwitchThemed toggle with haptic feedback
ContentSelectorPicker for linking recipe/collection
TextInputWithDoneTextInput wrapper with iOS “Done” button

Hooks & State

HookPurpose
useRecipes / useDiscoverContentReact Query hook for discover feed (infinite query). useRecipes is a deprecated alias for useDiscoverContent.
useCreateRecipeRecipe creation via create_recipe RPC
useUpdateRecipe(id)Recipe update via update_recipe RPC
useDeleteRecipeHard delete via delete_recipe RPC + storage cleanup
useToggleFavoriteOptimistic favorite toggle on recipe_favorites
useToggleRepostRepost toggle on recipe_reposts
useCommentsComment fetching/creation
useImageUploadImage upload with compression
useImageDimensionsImage dimension calculation
useCommentModalComment modal state
useHapticsHaptic feedback on actions
useSaveStatusSave/bookmark status
useAuthAuth state for ownership checks
useAuthOverlayAuth-gated action handling
useToastToast notifications for success/error feedback
useThemeTheme colors for styled components

Data Access Pattern

The mobile app calls Supabase directly via supabase.rpc() and supabase.from() — it does NOT route through the Next.js API routes in apps/web. The web API routes exist for the web client (Phase 2). The mobile hooks in src/hooks/api/ are the source of truth for how data flows.

Mobile (Direct Supabase)

HookSupabase CallPurpose
useCreateRecipesupabase.rpc('create_recipe', {...})Atomic recipe creation
useRecipe(id)supabase.rpc('get_recipe_detail', {...})Fetch recipe detail
useUpdateRecipe(id)supabase.rpc('update_recipe', {...})Update recipe
useDeleteRecipesupabase.rpc('delete_recipe', {...})Hard delete (cascading)
useDiscoverContentsupabase.rpc('get_discover_content_list', {...})Feed listing
useToggleFavoritesupabase.from('recipe_favorites').insert/deleteFavorite toggle
useToggleRepostsupabase.from('recipe_reposts').insert/deleteRepost toggle

Web API Routes (Phase 2 — used by web client)

MethodRoutePurpose
POST/api/recipesCreate recipe (calls create_recipe RPC)
GET/api/recipes/[id]Get recipe detail (calls get_recipe_detail RPC)
PUT/api/recipes/[id]Update recipe (calls update_recipe RPC)
DELETE/api/recipes/[id]Hard delete recipe (calls delete_recipe RPC)
GET/api/recipes/listList recipes with filters
GET/api/recipes/featuredFeatured/trending recipes
POST/api/upload/imageUpload recipe images to storage

Optimistic Updates & Cross-Screen Sync

When a user favorites, reposts, or comments on a recipe, the change must appear instantly across every screen — detail views, feed cards, profile grids — without a full refetch.

How useToggleFavorite Works

User taps heart
  ├─ onMutate (before API call):
  │   ├─ Cancel all outgoing refetches for this recipe
  │   ├─ Snapshot previous state (for rollback)
  │   ├─ Update detail view cache: toggle isFavorited, ±1 favoriteCount
  │   ├─ Update ALL recipe list caches: walk every page of every infinite query
  │   └─ Update ALL post list caches: walk mixed content feeds too
  ├─ mutationFn: supabase.from('recipe_favorites').insert/delete
  ├─ onError: rollback all caches to snapshots
  └─ onSettled: NO invalidation (optimistic update is sufficient)
The key insight: onSettled deliberately does NOT call invalidateQueries. Invalidating would cause the masonry grid to re-render and items to jump/disappear. The optimistic cache update is the final state — data will naturally refresh on next mount/focus.

Cache Keys Affected

ActionCaches Updated
Favorite reciperecipes.detail(id), ALL recipes.all queries, ALL posts.all queries (mixed feeds)
Favorite postposts.detail(id), ALL posts.all queries
Add commentComment list cache, recipes.detail(id) or posts.detail(id) comment count, ALL list caches comment count
Save itemsaved.status(type, id), saved.items()
Toggle collectionsaved.status(type, id), collections.detail(collectionId)
Follow userusers.profile, users.followers
Create/delete contentrecipes.all, collections.all, saved.all

Comment Count Propagation

When a comment is added via useAddComment:
  1. Optimistic comment inserted into comment list cache (with temp ID)
  2. On success: temp comment replaced with real server response
  3. Comment count incremented in ALL list view caches (walks every page of every infinite query)
  4. Comment count incremented in detail view cache

Save Status Propagation

useSaveStatus manages bookmark state per-item:
  • Optimistic toggle on useToggleStandalone (save/unsave)
  • Optimistic toggle on useToggleCollectionMembership (add/remove from collection)
  • On settle: narrow invalidation of specific item status + saved items list (not all queries)

RPC Functions

Parameters: p_user_id, p_title, p_description, p_camera_model, p_images[], p_cover_index, p_tags[], p_tagged_user_ids[], p_collection_id, + all Fujifilm settings + optional Lightroom settings (p_include_lightroom, p_lr_exposure, p_lr_contrast, etc.)Returns: TABLE(id, title, description, camera_model, cover_image_url, film_simulation, created_at, user_id, collection_id)Atomic creation: inserts recipe + recipe_images + recipe_tags + recipe_tagged_users + lightroom_settings (if included) + collection assignment (if provided). All in one transaction.
Same parameters as create but all optional (NULL = no change).Returns: TABLE(id, title, description, camera_model, cover_image_url, film_simulation, updated_at, collection_id)Atomic update: replaces images, tags, tagged users, lightroom settings as needed.
Parameters: p_recipe_id, p_user_idReturns: TABLE(id, image_urls[], deleted_at)Hard delete: cascading deletion of all related data (images, tags, tagged users, comments, comment mentions, favorites, reposts, lightroom settings, saved items, notifications) then deletes the recipe itself. Returns image URLs so the client can clean up Supabase Storage.
Parameters: p_recipe_id, p_current_user_id (optional)Returns: JSON — full recipe with author profile, images, tags, tagged users, lightroom settings, engagement counts, and current user’s favorite/repost/save status.

Fujifilm Camera Models

22 supported models (from shared constants): X-T5, X-T4, X-T3, X-T2, X-T30 II, X-T30, X-T20, X-Pro3, X-Pro2, X-E4, X-E3, X-S20, X-S10, X-H2S, X-H2, X-H1, X100V, X100F, X100VI, GFX 100S, GFX 100 II, GFX 50S II, Other.

Database Tables

ColumnTypeConstraints
iduuid PK
user_idFK→profiles
titletext
descriptiontext
camera_modeltext
cover_image_urltext
film_simulationtextCHECK: Provia, Velvia, Astia, Classic Chrome, Pro Neg Hi, Pro Neg Std, Classic Neg, Eterna, Eterna Bleach Bypass, Acros, Acros+Ye/R/G, Monochrome, Monochrome+Ye/R/G, Sepia, Nostalgic Neg, Reala Ace
dynamic_rangetextCHECK: DR100, DR200, DR400, Auto
grain_effecttextCHECK: Off, Weak Small, Weak Large, Strong Small, Strong Large
color_chrome_effecttextCHECK: Off, Weak, Strong
color_chrome_effect_bluetextCHECK: Off, Weak, Strong
white_balance_modetextCHECK: Auto, Daylight, Shade, Fluorescent 1/2/3, Incandescent, Underwater, Kelvin
white_balance_kelvinint2500–10000
white_balance_redint-9 to 9
white_balance_blueint-9 to 9
highlightint-2 to 4
shadowint-2 to 4
colorint-4 to 4
sharpnessint-4 to 4
noise_reductionint-4 to 4
clarityint-5 to 5
iso_settingtext
exposure_compensationtext
is_deletedbooldefault false
comment_countintdenormalized
favorite_countintdenormalized
repost_countintdenormalized
id (uuid PK), recipe_id (FK→recipes), image_url, is_cover (bool), display_order (int). Multiple images per recipe, ordered.
id (uuid PK), recipe_id (FK→recipes), tag (text). Free-text tags.
recipe_id (FK→recipes), tagged_user_id (FK→profiles). Composite PK.
One-to-one with recipe (recipe_id UNIQUE FK). Fields: exposure (-5.0 to 5.0), contrast (-100 to 100), highlights, shadows, whites, blacks, temperature, tint, vibrance, saturation, clarity, dehaze, texture (all -100 to 100), sharpening (0 to 150), noise_reduction (0 to 100). All have CHECK constraints.

Validation Constraints (Zod)

FieldConstraint
Recipe title1–100 characters
Recipe description0–2000 characters
TagsMax 10 per recipe, each 1–30 characters
Tagged usersMax 10 per recipe
Images1–10 per recipe, JPEG/PNG/WebP only

RLS Policies

  • SELECT: (is_deleted = false) OR (auth.uid() = user_id) — everyone sees non-deleted; owners see their own deleted
  • INSERT: auth.uid() = user_id
  • UPDATE: auth.uid() = user_id
  • DELETE: auth.uid() = user_id
Recipe images and tags: SELECT public, INSERT/UPDATE/DELETE owner check via JOIN to recipes.

Triggers

TriggerTableEventsFunction
trigger_update_profile_recipe_countrecipesINSERT, UPDATE, DELETEupdate_profile_recipe_count()
trigger_recipe_tag_notificationrecipe_tagged_usersINSERTcreate_recipe_tag_notification()
trigger_delete_recipe_tag_notificationrecipe_tagged_usersDELETEdelete_recipe_tag_notification()
trigger_delete_saved_items_on_recipe_deleterecipesDELETEdelete_saved_items_on_recipe_delete()
trigger_delete_saved_items_on_recipe_soft_deleterecipesUPDATEdelete_saved_items_on_recipe_delete()
update_recipes_updated_atrecipesUPDATEupdate_updated_at_column()
update_lightroom_settings_updated_atlightroom_settingsUPDATEupdate_updated_at_column()

Storage

  • Bucket: recipe-images (public: true, no file_size_limit or MIME restrictions at bucket level — validation in API route)
  • Path pattern: {user_id}/{filename}
  • Policies: Public read, authenticated upload, owner-only update/delete (via storage.foldername(name)[1] = auth.uid())