Post Images & Reposts
Posts can carry images and can be reposted as quote-posts. This page documents the post_images table that stores attached images, the /api/posts/image upload route, the use-post-images hook, and the repost model built on posts.post_tag and post_stats.repost_count.
Earlier version note: A previous version of this page described
article_attachmentsand anarticles.repost_totalcounter column.article_attachmentswas dropped in migration20260430104609andrepost_totalno longer exists on that table. The models below reflect the current codebase.
Post Images
Section titled “Post Images”Images are uploaded before the post exists. POST /api/posts/image takes one composer image per request, runs it through the moderation pipeline, writes it to R2, and inserts a row into post_images. The returned id is collected in the composer and sent as image_ids on the post-create call, which creates the posts row and links the pre-uploaded images to it.
This pre-upload pattern means a moderation rejection names the specific image rather than failing the entire post submission.
The post_images table (schema in src/types/supabase.ts) links each image to its post via post_id and to a shared images record via image_id.
The client side uses use-post-images and uploadModeratedImage from src/lib/images/client.ts. The upload transport (FormData assembly, moderation result normalisation) is documented on Media & Images; the server-side pipeline (moderation gate, R2 write, images insert) is documented on Storage Abstraction & R2 Integration.
Reposts
Section titled “Reposts”A repost is a quote-post: RepostButton calls requestRepost from use-repost.tsx, which opens the composer pre-loaded with the original post. Submitting creates a new posts row with post_tag set to the original post’s id — the repost is a first-class post that embeds its source. The count is stored in post_stats.repost_count and returned as post.stats.repost_count in PublicPost.
Feed components receive repostedPosts (an array of post ids the viewer has reposted) alongside initialPosts. PostsInfiniteFeed turns it into a Set for O(1) per-card resolution of the reposted boolean passed to PostCard.
Failure Modes & Edge Cases
Section titled “Failure Modes & Edge Cases”- One repost per viewer.
RepostButtonreturns early ifrepostedis alreadytrue; the composer context only fires if the viewer has not yet reposted. - Image upload rejection. A moderation
422returns{ status: "rejected", categories }fromuploadModeratedImage; the composer surfaces the rejection message and does not create apost_imagesrow or a post. - Orphaned images. If an image uploads successfully but the post create subsequently fails, the
post_imagesrow is orphaned. There is no automatic cleanup. - Moderation unavailable. A
503from the moderation service returns{ status: "failed" }with the shared “couldn’t complete the content check” wording; the upload does not proceed.
Related Links
Section titled “Related Links”- Posts — feed, composer and pagination
- Media & Images — client upload transport and image URL helpers
- Storage Abstraction & R2 Integration — server-side upload pipeline
/api/posts/imagerouteuse-post-images.tsRepostButton.tsxuse-repost.tsx