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
Fill Form
Images (via
ImagePickerStep), title, caption, camera model, film simulation, tags, optional recipe link (via ContentSelector), optional collection assignment.User Flow — View
Tap post card →app/post/[id].tsx → PostDetailView 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)).
Submit → supabase.rpc('update_post', {...}).
Screens & Components
| Screen/Component | Purpose |
|---|---|
app/post/new.tsx | Post creation |
app/post/[id].tsx | Post detail |
app/post/[id]/edit.tsx | Post edit flow |
PostDetailView | Detail view component |
FeedPostCard | Post card in feeds |
ContentCard | Generic 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)
| Hook | Supabase Call | Purpose |
|---|---|---|
useCreatePost | supabase.rpc('create_post', {...}) | Atomic post creation |
usePost(id) | supabase.rpc('get_post_detail', {...}) | Fetch post detail |
useUpdatePost(id) | supabase.rpc('update_post', {...}) | Update post |
useDeletePost | supabase.rpc('delete_post', {...}) | Soft delete |
usePosts | supabase.rpc('get_follow_content_list', {...}) | Following feed |
useTogglePostLike | supabase.from('post_favorites').insert/delete | Like toggle |
Web API Routes (Phase 2)
| Method | Route | Purpose |
|---|---|---|
POST | /api/posts | Create 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/list | List posts with filters |
GET | /api/posts/featured | Featured/trending posts |
Optimistic Updates & Cross-Screen Sync
useTogglePostLike follows the same pattern as recipe favorites:
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)mutationFn: Direct Supabase insert/delete onpost_favoritesonError: Rollback all caches to snapshotsonSettled: NO invalidation — optimistic update is the final state
RPC Functions
create_post
create_post
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)update_post
update_post
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)delete_post
delete_post
Parameters:
p_post_id, p_user_idReturns: TABLE(id, deleted_at)get_post_detail
get_post_detail
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.
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
| Field | Constraint |
|---|---|
| Post title | 1–100 characters |
| Post caption | 0–2000 characters |
| Tags | Max 10 per post, each 1–30 characters |
| Images | 1–10 per post, JPEG/PNG/WebP only |
Sort Option Differences
| Content Type | Available Sorts |
|---|---|
| Recipes | recent, trending, popular |
| Posts | recent, trending, following (no ‘popular’) |
Database Tables
| Table | Key Columns | Notes |
|---|---|---|
posts | id, 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_count | Posts can optionally link to a recipe via recipe_id |
post_images | id, post_id (FK→posts), image_url, is_cover, display_order | Same pattern as recipe_images |
post_tags | id, post_id (FK→posts), tag | Same pattern as recipe_tags |
post_tagged_users | post_id (FK→posts), tagged_user_id (FK→profiles) | Composite PK |
RLS Policies
- SELECT:
is_deleted = false - INSERT/UPDATE/DELETE:
auth.uid() = user_id
Triggers
| Trigger | Table | Events | Function |
|---|---|---|---|
trigger_update_profile_post_count | posts | INSERT, UPDATE, DELETE | update_profile_post_count() |
trigger_post_tag_notification | post_tagged_users | INSERT | create_post_tag_notification() |
trigger_delete_post_tag_notification | post_tagged_users | DELETE | delete_post_tag_notification() |
trigger_delete_saved_items_on_post_delete | posts | DELETE | delete_saved_items_on_post_delete() |
trigger_delete_saved_items_on_post_soft_delete | posts | UPDATE | delete_saved_items_on_post_delete() |
posts_updated_at | posts | UPDATE | update_updated_at_column() |
Storage
Bucket:post-images (public: true). Same path/policy pattern as recipe-images.