Skip to main content

What It Does

Posts are a lighter content type than recipes — a photo post with images, caption, tags, camera metadata, and an optional linked recipe. Think “here’s what I shot today.”

User Flow — Create

1

Open Create Modal

Tap ”+” → modal app/create.tsx → select “Post” → app/post/new.tsx
2

Fill Form

Images (via ImagePickerStep), title, caption, camera model, film simulation, tags, optional recipe link (via ContentSelector), optional collection assignment.
3

Submit

Images uploaded to Supabase Storage → supabase.rpc('create_post', {...})

User Flow — View

Tap post card → app/post/[id].tsxPostDetailView renders images, caption, metadata, action bar, comments.

User Flow — Edit

Owner taps edit → app/post/[id]/edit.tsx → pre-populated form (fetched via usePost(id)).
Post images are immutable — postUpdateSchema explicitly excludes image fields. Only metadata can be updated: title, caption, tags, camera model, film simulation, recipe link, collection.
Submit → supabase.rpc('update_post', {...}).

Screens & Components

Screen/ComponentPurpose
app/post/new.tsxPost creation
app/post/[id].tsxPost detail
app/post/[id]/edit.tsxPost edit flow
PostDetailViewDetail view component
FeedPostCardPost card in feeds
ContentCardGeneric content card

Hooks

usePosts (src/hooks/api/usePosts.ts), usePostImages (lazy-load images for carousel on swipe), useComments, useImageUpload, useSaveStatus

Data Access Pattern

The mobile app calls Supabase directly — it does NOT route through the Next.js API routes. The hooks use supabase.rpc() and supabase.from() directly.

Mobile (Direct Supabase)

HookSupabase CallPurpose
useCreatePostsupabase.rpc('create_post', {...})Atomic post creation
usePost(id)supabase.rpc('get_post_detail', {...})Fetch post detail
useUpdatePost(id)supabase.rpc('update_post', {...})Update post
useDeletePostsupabase.rpc('delete_post', {...})Soft delete
usePostssupabase.rpc('get_follow_content_list', {...})Following feed
useTogglePostLikesupabase.from('post_favorites').insert/deleteLike toggle

Web API Routes (Phase 2)

MethodRoutePurpose
POST/api/postsCreate post (calls create_post RPC)
GET/api/posts/[id]Get post detail (calls get_post_detail RPC)
PUT/api/posts/[id]Update post (calls update_post RPC)
DELETE/api/posts/[id]Soft delete (calls delete_post RPC)
GET/api/posts/listList posts with filters
GET/api/posts/featuredFeatured/trending posts

Optimistic Updates & Cross-Screen Sync

useTogglePostLike follows the same pattern as recipe favorites:
  1. onMutate: Cancel outgoing refetches → snapshot previous state → optimistically update detail view AND all list caches (walks every page of every infinite query for mixed content feeds)
  2. mutationFn: Direct Supabase insert/delete on post_favorites
  3. onError: Rollback all caches to snapshots
  4. onSettled: NO invalidation — optimistic update is the final state
Comment counts also propagate across all list views when a comment is added (see Common Components for the full cache sync pattern).

RPC Functions

Parameters: p_user_id, p_title, p_caption, p_camera_model, p_film_simulation, p_images[], p_cover_index, p_tags[], p_recipe_id, p_collection_idReturns: TABLE(id, title, caption, camera_model, film_simulation, cover_image_url, cover_index, created_at, user_id, recipe_id, collection_id)
Parameters: p_post_id, p_user_id, p_title, p_caption, p_camera_model, p_film_simulation, p_tags[], p_recipe_id, p_collection_idAll parameters except p_post_id and p_user_id are optional (NULL = no change). If p_tags is provided, existing tags are replaced. If p_collection_id is provided, the post is linked to that collection (creates a saved_item if needed, replaces any existing collection link).Returns: TABLE(id, title, caption, camera_model, film_simulation, recipe_id, collection_id, updated_at)
Parameters: p_post_id, p_user_idReturns: TABLE(id, deleted_at)
Parameters: p_post_id, p_current_user_id (optional)Returns: JSON — full post with author, images, tags, tagged users, engagement, user status.

update_post NULL Semantics

All optional parameters use NULL to mean “no change.” If p_recipe_id is NULL, the existing recipe_id is preserved. If p_collection_id is provided, the post is linked to that collection (existing collection links are replaced). Tags are replaced wholesale when p_tags is non-NULL.
There is currently no way to explicitly unlink a recipe from a post via update_post — passing NULL for p_recipe_id means “no change,” not “clear the link.” The storyboard references p_clear_recipe and p_clear_collection boolean flags, but these do not exist in the live database function. This is a known limitation.
Migration 20260221_fix_update_post_ambiguous_id.sql fixed column ambiguity in the update_post function — the RETURNS TABLE column names conflicted with the posts table columns in the UPDATE/SELECT statements. The fix uses table-qualified column references (posts.id, posts.title, etc.).

Validation Constraints

FieldConstraint
Post title1–100 characters
Post caption0–2000 characters
TagsMax 10 per post, each 1–30 characters
Images1–10 per post, JPEG/PNG/WebP only

Sort Option Differences

Content TypeAvailable Sorts
Recipesrecent, trending, popular
Postsrecent, trending, following (no ‘popular’)

Database Tables

TableKey ColumnsNotes
postsid, user_id (FK→profiles), title, caption, camera_model, film_simulation, recipe_id (FK→recipes, nullable), cover_image_url, is_deleted, comment_count, favorite_count, repost_countPosts can optionally link to a recipe via recipe_id
post_imagesid, post_id (FK→posts), image_url, is_cover, display_orderSame pattern as recipe_images
post_tagsid, post_id (FK→posts), tagSame pattern as recipe_tags
post_tagged_userspost_id (FK→posts), tagged_user_id (FK→profiles)Composite PK

RLS Policies

  • SELECT: is_deleted = false
  • INSERT/UPDATE/DELETE: auth.uid() = user_id

Triggers

TriggerTableEventsFunction
trigger_update_profile_post_countpostsINSERT, UPDATE, DELETEupdate_profile_post_count()
trigger_post_tag_notificationpost_tagged_usersINSERTcreate_post_tag_notification()
trigger_delete_post_tag_notificationpost_tagged_usersDELETEdelete_post_tag_notification()
trigger_delete_saved_items_on_post_deletepostsDELETEdelete_saved_items_on_post_delete()
trigger_delete_saved_items_on_post_soft_deletepostsUPDATEdelete_saved_items_on_post_delete()
posts_updated_atpostsUPDATEupdate_updated_at_column()

Storage

Bucket: post-images (public: true). Same path/policy pattern as recipe-images.