Skip to main content
RapidDev - Software Development Agency

How to Build Social media feed with V0

Build a social media feed with V0 featuring posts, likes, comments, infinite scroll, and real-time updates using Next.js, Supabase, and Supabase Realtime. You'll create an optimistic like system, cursor-based pagination, user profiles, and live post streaming — all in about 1-2 hours.

What you'll build

  • Infinite-scrolling post feed with cursor-based pagination and Skeleton loading placeholders
  • Post creation with Textarea and image upload support using Supabase Storage
  • Optimistic like toggling using useOptimistic from React 19 with instant UI updates
  • Comment threads on posts with real-time comment count updates
  • User profiles with Avatar, display name, and post history
  • Supabase Realtime subscription that prepends new posts to the feed without page refresh
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate11 min read1-2 hoursV0 FreeApril 2026RapidDev Engineering Team
TL;DR

Build a social media feed with V0 featuring posts, likes, comments, infinite scroll, and real-time updates using Next.js, Supabase, and Supabase Realtime. You'll create an optimistic like system, cursor-based pagination, user profiles, and live post streaming — all in about 1-2 hours.

What you're building

Social feeds are the backbone of community platforms — whether it's a niche interest group, a company internal feed, or a neighborhood forum. Users expect to post updates, like and comment, see new content appear instantly, and scroll endlessly without page loads.

V0 generates the feed components, post cards, and interaction logic from prompts. Supabase handles the database, auth, file storage for images, and Realtime subscriptions for live updates. The entire stack runs on Vercel serverless with no infrastructure to manage.

The architecture uses cursor-based pagination via an API route for efficient infinite scroll, Server Actions for mutations (create post, toggle like, add comment), Supabase Realtime for live post streaming in a client component, and Server Components for user profile pages.

Final result

A fully interactive social media feed with post creation, likes, comments, infinite scroll, user profiles, and real-time updates powered by Supabase Realtime.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
Supabase RealtimeLive Updates

Prerequisites

  • A V0 account (free tier works for this project)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • Supabase Auth configured for user registration and login
  • Basic understanding of social media feed interactions (posts, likes, comments)

Build steps

1

Set up the social feed database schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Create the profiles, posts, likes, and comments tables with proper indexes and foreign key relationships.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a Supabase schema for a social media feed:
3// 1. profiles table: id (uuid PK references auth.users), username (text UNIQUE), display_name (text), avatar_url (text), bio (text)
4// 2. posts table: id (uuid PK), author_id (uuid FK to profiles), content (text), image_url (text nullable), likes_count (int DEFAULT 0), comments_count (int DEFAULT 0), created_at (timestamptz)
5// 3. likes table: id (uuid PK), post_id (uuid FK), user_id (uuid FK), UNIQUE(post_id, user_id)
6// 4. comments table: id (uuid PK), post_id (uuid FK), author_id (uuid FK), content (text), created_at (timestamptz)
7// Add index on posts.created_at for cursor-based pagination.
8// RLS: authenticated users can create posts/likes/comments, public can read.
9// Enable Realtime replication on the posts table.
10// Seed 20 sample posts with varied timestamps.

Pro tip: Enable Supabase Realtime on the posts table in your Supabase Dashboard under Database > Replication. This is required for live post streaming in the feed.

Expected result: Four tables created with indexes, RLS policies, Realtime enabled on posts, and 20 sample posts seeded for testing the feed.

2

Build the post feed with cursor-based pagination

Create the main feed page with an API route that supports cursor-based pagination. The feed loads 20 posts at a time, with each response including a cursor for the next page. This is more efficient than offset pagination for social feeds.

app/api/feed/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function GET(req: NextRequest) {
10 const { searchParams } = new URL(req.url)
11 const cursor = searchParams.get('cursor')
12 const limit = 20
13
14 let query = supabase
15 .from('posts')
16 .select('*, profiles(username, display_name, avatar_url)')
17 .order('created_at', { ascending: false })
18 .limit(limit + 1)
19
20 if (cursor) {
21 query = query.lt('created_at', cursor)
22 }
23
24 const { data: posts, error } = await query
25
26 if (error) {
27 return NextResponse.json({ error: error.message }, { status: 500 })
28 }
29
30 const hasMore = (posts?.length ?? 0) > limit
31 const items = hasMore ? posts!.slice(0, limit) : posts!
32 const nextCursor = hasMore ? items[items.length - 1].created_at : null
33
34 return NextResponse.json({ posts: items, nextCursor })
35}

Expected result: GET /api/feed returns 20 posts with author profiles. GET /api/feed?cursor=2025-01-01T00:00:00Z returns the next 20 posts older than the cursor.

3

Create the feed UI with infinite scroll and Realtime

Build a client component that renders the post feed, handles infinite scroll loading, and subscribes to Supabase Realtime for new posts appearing at the top without refresh.

components/feed-list.tsx
1'use client'
2
3import { useEffect, useState, useCallback, useOptimistic } from 'react'
4import { createClient } from '@/lib/supabase/client'
5import { Card, CardContent } from '@/components/ui/card'
6import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
7import { Button } from '@/components/ui/button'
8import { Skeleton } from '@/components/ui/skeleton'
9import { Separator } from '@/components/ui/separator'
10import { Heart } from 'lucide-react'
11import { toggleLike } from '@/app/actions/feed'
12
13type Post = {
14 id: string
15 content: string
16 image_url: string | null
17 likes_count: number
18 comments_count: number
19 created_at: string
20 profiles: { username: string; display_name: string; avatar_url: string }
21}
22
23export function FeedList({ initialPosts }: { initialPosts: Post[] }) {
24 const [posts, setPosts] = useState<Post[]>(initialPosts)
25 const [cursor, setCursor] = useState<string | null>(
26 initialPosts.length > 0 ? initialPosts[initialPosts.length - 1].created_at : null
27 )
28 const [loading, setLoading] = useState(false)
29 const supabase = createClient()
30
31 useEffect(() => {
32 const channel = supabase
33 .channel('feed')
34 .on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'posts' }, (payload) => {
35 setPosts((prev) => [payload.new as Post, ...prev])
36 })
37 .subscribe()
38 return () => { supabase.removeChannel(channel) }
39 }, [supabase])
40
41 const loadMore = useCallback(async () => {
42 if (!cursor || loading) return
43 setLoading(true)
44 const res = await fetch(`/api/feed?cursor=${cursor}`)
45 const data = await res.json()
46 setPosts((prev) => [...prev, ...data.posts])
47 setCursor(data.nextCursor)
48 setLoading(false)
49 }, [cursor, loading])
50
51 return (
52 <div className="max-w-2xl mx-auto space-y-4">
53 {posts.map((post) => (
54 <Card key={post.id}>
55 <CardContent className="p-4">
56 <div className="flex items-center gap-3 mb-3">
57 <Avatar>
58 <AvatarImage src={post.profiles?.avatar_url} />
59 <AvatarFallback>{post.profiles?.display_name?.[0]}</AvatarFallback>
60 </Avatar>
61 <div>
62 <p className="font-semibold">{post.profiles?.display_name}</p>
63 <p className="text-sm text-muted-foreground">
64 {new Date(post.created_at).toLocaleDateString()}
65 </p>
66 </div>
67 </div>
68 <p className="mb-3">{post.content}</p>
69 {post.image_url && (
70 <img src={post.image_url} alt="" className="rounded-lg mb-3 w-full" />
71 )}
72 <Separator className="my-3" />
73 <div className="flex gap-4">
74 <form action={toggleLike}>
75 <input type="hidden" name="postId" value={post.id} />
76 <Button variant="ghost" size="sm" type="submit">
77 <Heart className="w-4 h-4 mr-1" /> {post.likes_count}
78 </Button>
79 </form>
80 <Button variant="ghost" size="sm">
81 {post.comments_count} comments
82 </Button>
83 </div>
84 </CardContent>
85 </Card>
86 ))}
87 {loading && <Skeleton className="h-40 w-full" />}
88 {cursor && (
89 <Button variant="outline" className="w-full" onClick={loadMore} disabled={loading}>
90 Load more
91 </Button>
92 )}
93 </div>
94 )
95}

Expected result: A social media feed with post Cards showing author Avatar, content, image, like/comment buttons. New posts appear at the top in real-time. Clicking 'Load more' fetches older posts.

4

Create Server Actions for post creation and like toggling

Build Server Actions for creating posts and toggling likes. The like toggle uses an upsert/delete pattern with atomic counter updates to prevent race conditions.

app/actions/feed.ts
1'use server'
2
3import { createClient } from '@/lib/supabase/server'
4import { revalidatePath } from 'next/cache'
5
6export async function createPost(formData: FormData) {
7 const supabase = await createClient()
8 const { data: { user } } = await supabase.auth.getUser()
9 if (!user) return { error: 'Not authenticated' }
10
11 const content = formData.get('content') as string
12 const imageUrl = formData.get('imageUrl') as string | null
13
14 const { error } = await supabase.from('posts').insert({
15 author_id: user.id,
16 content,
17 image_url: imageUrl || null,
18 })
19
20 if (error) return { error: error.message }
21 revalidatePath('/feed')
22 return { success: true }
23}
24
25export async function toggleLike(formData: FormData) {
26 const supabase = await createClient()
27 const { data: { user } } = await supabase.auth.getUser()
28 if (!user) return { error: 'Not authenticated' }
29
30 const postId = formData.get('postId') as string
31
32 const { data: existing } = await supabase
33 .from('likes')
34 .select('id')
35 .eq('post_id', postId)
36 .eq('user_id', user.id)
37 .maybeSingle()
38
39 if (existing) {
40 await supabase.from('likes').delete().eq('id', existing.id)
41 await supabase.rpc('decrement_likes', { p_post_id: postId })
42 } else {
43 await supabase.from('likes').insert({ post_id: postId, user_id: user.id })
44 await supabase.rpc('increment_likes', { p_post_id: postId })
45 }
46
47 revalidatePath('/feed')
48}
49
50export async function addComment(formData: FormData) {
51 const supabase = await createClient()
52 const { data: { user } } = await supabase.auth.getUser()
53 if (!user) return { error: 'Not authenticated' }
54
55 const postId = formData.get('postId') as string
56 const content = formData.get('content') as string
57
58 await supabase.from('comments').insert({
59 post_id: postId,
60 author_id: user.id,
61 content,
62 })
63
64 await supabase.rpc('increment_comments', { p_post_id: postId })
65 revalidatePath('/feed')
66}

Pro tip: Create Supabase RPC functions for increment_likes, decrement_likes, and increment_comments that atomically update the counter columns. This prevents race conditions when multiple users like the same post simultaneously.

Expected result: Like toggling instantly checks/unchecks in the UI and updates the counter. Comments are added and the count increments. Both actions require authentication.

5

Build the post composer and user profile pages

Create a Textarea-based post composer and user profile pages that show a user's posts and follower counts. The composer supports text and optional image uploads.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build two components for a social media feed:
3// 1. A post composer component at the top of the feed with:
4// - shadcn/ui Textarea for post content
5// - Avatar of the current user next to the textarea
6// - Image upload button (optional)
7// - Post Button that calls createPost Server Action
8// - Character count indicator
9// 2. A user profile page at app/profile/[username]/page.tsx with:
10// - Server Component that fetches the user's profile and posts
11// - Avatar, display_name, username, and bio at the top
12// - Post count, likes received total
13// - Their posts displayed in Card components identical to the feed
14// - Use Tabs to switch between Posts and Liked posts
15// Use Supabase for data and shadcn/ui for all components.

Expected result: A post composer with Textarea and Avatar at the top of the feed, and a user profile page showing the user's information and post history with Tab navigation.

Complete code

app/api/feed/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function GET(req: NextRequest) {
10 const { searchParams } = new URL(req.url)
11 const cursor = searchParams.get('cursor')
12 const limit = 20
13
14 let query = supabase
15 .from('posts')
16 .select(
17 '*, profiles(username, display_name, avatar_url)'
18 )
19 .order('created_at', { ascending: false })
20 .limit(limit + 1)
21
22 if (cursor) {
23 query = query.lt('created_at', cursor)
24 }
25
26 const { data: posts, error } = await query
27
28 if (error) {
29 return NextResponse.json(
30 { error: error.message },
31 { status: 500 }
32 )
33 }
34
35 const hasMore = (posts?.length ?? 0) > limit
36 const items = hasMore ? posts!.slice(0, limit) : posts!
37 const nextCursor = hasMore
38 ? items[items.length - 1].created_at
39 : null
40
41 return NextResponse.json({
42 posts: items,
43 nextCursor,
44 })
45}

Customization ideas

Add image galleries in posts

Allow multiple images per post using Supabase Storage. Display them in a carousel using a shadcn/ui-compatible image slider component.

Add hashtag support

Parse hashtags from post content, store them in a tags table, and create clickable hashtag links that filter the feed to show only posts with that tag.

Add follow system

Create a follows table (follower_id, following_id) and filter the feed to show posts only from followed users, with a 'Discover' tab for all public posts.

Add post bookmarks

Let users save posts to a bookmarks collection with a bookmark icon. Create a saved posts page at app/saved/page.tsx showing bookmarked content.

Common pitfalls

Pitfall: Using offset-based pagination (OFFSET 20, 40, 60...) for the feed

How to avoid: Use cursor-based pagination with WHERE created_at < $cursor ORDER BY created_at DESC LIMIT 20. The cursor is the timestamp of the last post on the current page.

Pitfall: Updating like counts with separate SELECT + UPDATE instead of atomic RPC

How to avoid: Create Supabase RPC functions that use SET likes_count = likes_count + 1 (atomic increment) instead of reading and writing separately.

Pitfall: Not enabling Supabase Realtime on the posts table

How to avoid: Go to Supabase Dashboard > Database > Replication and add the posts table. The client subscription will then receive INSERT events.

Best practices

  • Use cursor-based pagination for infinite scroll — it performs consistently regardless of feed depth and handles new posts gracefully
  • Create atomic Supabase RPC functions for like/comment count updates to prevent race conditions on popular posts
  • Use Supabase Realtime subscriptions to prepend new posts to the feed without polling, giving a real-time experience
  • Use V0's Connect panel to set up Supabase with one click, then enable Realtime on the posts table in Supabase Dashboard
  • Use Design Mode (Option+D) to visually adjust post Card padding, Avatar sizing, and feed spacing at zero credit cost
  • Use Server Components for profile pages since they don't need real-time updates — better SEO and faster initial load
  • Add Skeleton loading placeholders during infinite scroll to prevent layout shift when new posts load

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a social media feed with Next.js App Router and Supabase. I need: 1) Cursor-based pagination API for infinite scroll, 2) Optimistic like toggling with atomic counter updates, 3) Supabase Realtime subscription for new posts, 4) User profiles with post history. Help me design the schema and avoid race conditions on like counts.

Build Prompt

Implement optimistic like toggling for a social media feed post. When the user clicks the heart icon: 1) Immediately toggle the heart icon fill and increment/decrement the displayed count in local state, 2) Fire a Server Action in the background that checks if a like exists (SELECT from likes), deletes it if found or inserts if not, 3) Call an RPC function to atomically update the likes_count, 4) If the Server Action fails, roll back the UI to the previous state. Use React 19 useOptimistic hook.

Frequently asked questions

How does real-time work in this feed?

Supabase Realtime uses WebSocket connections to push database changes to connected clients. When someone creates a new post, the INSERT event is sent to all clients subscribed to the posts table. The client component prepends the new post to the feed without any page refresh.

Will this scale to thousands of users?

Yes. Cursor-based pagination keeps queries fast at any depth. Atomic RPC functions prevent like count race conditions. Supabase Realtime handles thousands of concurrent WebSocket connections. For very high scale, consider Supabase Pro for connection pooling.

What V0 plan do I need?

V0 Free tier works. The feed uses standard Server Components, Server Actions, and shadcn/ui components. Supabase Realtime is included on Supabase free tier. Design Mode polish is also free.

Can I add image uploads to posts?

Yes. Use Supabase Storage to create a public bucket for post images. Add a file Input to the composer, upload the image via Supabase Storage client, get the public URL, and include it in the post creation Server Action.

How do I deploy the feed to production?

Click Share in V0, then Publish to Production. The Supabase connection is automatically configured from the Connect panel. Make sure Realtime is enabled on the posts table in Supabase Dashboard before deploying.

Can RapidDev help build a custom social platform?

Yes. RapidDev has built 600+ apps including social platforms with feeds, messaging, notifications, and content moderation. Book a free consultation to discuss your community platform requirements.

Can I add a follow system to filter the feed?

Yes. Create a follows table with follower_id and following_id columns. Modify the feed API query to join through follows where follower_id matches the current user. Add a Discover tab that shows all public posts regardless of follow status.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.