Skip to main content
RapidDev - Software Development Agency

How to Build Chat application with V0

Build a real-time chat application with V0 using Next.js and Supabase Realtime. You'll get group channels, private direct messages, typing indicators, online presence tracking, read receipts, and cursor-based message pagination — all in about 1-2 hours without any local setup.

What you'll build

  • Real-time group and direct message channels using Supabase Realtime subscriptions
  • Message composer with send Button, emoji picker Popover, and auto-scroll ScrollArea
  • Typing indicators and online presence using Supabase Realtime Presence with track() and presenceState()
  • Unread message count Badges on channel sidebar with last_read_at tracking
  • Cursor-based message pagination loading older messages on scroll-up
  • Channel management with creation Dialog, member invite, and role assignment
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Intermediate10 min read1-2 hoursV0 Premium or higherApril 2026RapidDev Engineering Team
TL;DR

Build a real-time chat application with V0 using Next.js and Supabase Realtime. You'll get group channels, private direct messages, typing indicators, online presence tracking, read receipts, and cursor-based message pagination — all in about 1-2 hours without any local setup.

What you're building

Real-time messaging is a core feature for communities, team tools, and social platforms. Users expect instant message delivery, typing indicators, and online presence — the same experience they get from Slack or Discord.

V0 generates the chat interface, channel management, and message components from prompts. Supabase Realtime via the Connect panel provides WebSocket-based message delivery with under 200ms latency, plus Presence channels for typing indicators and online status.

The architecture uses a Next.js App Router layout with a sidebar for channels and a main area for messages. Server Components fetch initial channel and message data. A Client Component subscribes to Supabase Realtime for live message inserts and Presence for typing/online indicators. Messages are paginated with cursor-based loading for performance. Server Actions handle channel creation and member management.

Final result

A complete real-time chat application with group channels, direct messages, typing indicators, online presence, unread badges, message pagination, and channel management.

Tech stack

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

Prerequisites

  • A V0 account (Premium plan recommended for real-time features)
  • A Supabase project with Realtime enabled (free tier works — connect via V0's Connect panel)
  • Understanding of channel-based messaging concepts (groups, direct messages)

Build steps

1

Set up the database schema with channels and messages

Create a new V0 project, connect Supabase, and create the channels, members, messages, and presence tables. Enable Realtime on the messages table for instant delivery.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a real-time chat app with Supabase. Create these tables:
3// 1. channels: id (uuid PK), name (text), type (text CHECK in 'group','direct'), created_by (uuid FK to auth.users), created_at (timestamptz)
4// 2. channel_members: channel_id (uuid FK), user_id (uuid FK), role (text default 'member'), joined_at (timestamptz), last_read_at (timestamptz), PRIMARY KEY (channel_id, user_id)
5// 3. messages: id (uuid PK), channel_id (uuid FK), sender_id (uuid FK to auth.users), content (text), type (text default 'text'), metadata (jsonb), created_at (timestamptz), updated_at (timestamptz)
6// Enable Realtime on the messages table.
7// Add RLS: only channel members can read/write messages in their channels.

Pro tip: Enable Realtime on the messages table in Supabase Dashboard under Database then Replication. Without this, the client subscription will connect but never receive message events.

Expected result: Supabase is connected with chat tables created, Realtime enabled on messages, and RLS policies restricting access to channel members.

2

Build the chat layout with channel sidebar and message area

Create the main chat layout with a sidebar showing channels with unread counts and a main area for the active channel's messages. The layout uses Server Components for initial data and a Client Component wrapper for real-time updates.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a chat layout at app/chat/layout.tsx.
3// Requirements:
4// - Left sidebar showing channel list with:
5// - Channel name and type icon (group/direct)
6// - Unread message count Badge (compare last_read_at with latest message created_at)
7// - Active channel highlighted
8// - "New Channel" Button at top
9// - Main area on the right for message thread (rendered by child pages)
10// - Use shadcn/ui Sidebar for the channel list, Badge for unread counts, Avatar for user icons in DMs
11// - Server Component fetches channels for the current user from channel_members join channels
12// - Responsive: sidebar collapses to Sheet on mobile

Expected result: The chat layout shows channels in a sidebar with unread Badges. Clicking a channel loads the message thread in the main area.

3

Create the real-time message thread with Supabase Realtime

Build the message thread component that loads initial messages from Supabase and subscribes to real-time inserts. New messages appear instantly for all channel members. Include auto-scroll to bottom and date group separators.

app/api/messages/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function POST(req: NextRequest) {
10 const { channelId, senderId, content } = await req.json()
11
12 const { data: member } = await supabase
13 .from('channel_members')
14 .select('user_id')
15 .eq('channel_id', channelId)
16 .eq('user_id', senderId)
17 .single()
18
19 if (!member) {
20 return NextResponse.json({ error: 'Not a channel member' }, { status: 403 })
21 }
22
23 const { data: message, error } = await supabase
24 .from('messages')
25 .insert({
26 channel_id: channelId,
27 sender_id: senderId,
28 content,
29 })
30 .select()
31 .single()
32
33 if (error) return NextResponse.json({ error: error.message }, { status: 500 })
34
35 await supabase
36 .from('channel_members')
37 .update({ last_read_at: new Date().toISOString() })
38 .eq('channel_id', channelId)
39 .eq('user_id', senderId)
40
41 return NextResponse.json({ message })
42}

Expected result: Sending a message inserts it into Supabase and updates the sender's last_read_at. The Realtime subscription delivers the message to all channel members instantly.

4

Add typing indicators and online presence

Implement typing indicators and online status using Supabase Realtime Presence. When a user starts typing, they broadcast their presence to the channel. Other members see who is typing in real-time.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a real-time typing indicator and presence component for the chat.
3// Requirements:
4// - 'use client' component that wraps the message thread
5// - Subscribe to Supabase Realtime channel `chat:${channelId}`
6// - Listen for INSERT events on the messages table filtered by channel_id for new messages
7// - Use Presence to track online users and typing status:
8// - On message input focus/typing, call channel.track({ user_id, typing: true })
9// - On blur or 3-second debounce, call channel.track({ user_id, typing: false })
10// - Read presenceState() to show which users are typing and who is online
11// - Display typing indicator text: "Alice is typing..." or "Alice and Bob are typing..."
12// - Show green dot indicator next to online users in the channel member list
13// - Merge incoming messages with the loaded list without duplicates (check message id)
14// - Auto-scroll to bottom on new messages unless the user has scrolled up

Pro tip: Subscribe to both Realtime table changes (for messages) and Presence (for typing/online) on the same Supabase channel. Use channel.on('postgres_changes', ...) for messages and channel.on('presence', ...) for typing state.

Expected result: Users see typing indicators when others type. Online presence dots show who is currently active in the channel. New messages appear instantly.

5

Add message pagination and channel management

Implement cursor-based pagination for loading older messages on scroll-up, and add channel creation with member invitations. Use an IntersectionObserver to detect when the user scrolls to the top of the message list.

app/actions/chat.ts
1'use server'
2
3import { createClient } from '@supabase/supabase-js'
4import { revalidatePath } from 'next/cache'
5
6const supabase = createClient(
7 process.env.SUPABASE_URL!,
8 process.env.SUPABASE_SERVICE_ROLE_KEY!
9)
10
11export async function createChannel(
12 name: string,
13 type: 'group' | 'direct',
14 createdBy: string,
15 memberIds: string[]
16) {
17 const { data: channel, error } = await supabase
18 .from('channels')
19 .insert({ name, type, created_by: createdBy })
20 .select()
21 .single()
22
23 if (error) throw new Error(error.message)
24
25 const members = [createdBy, ...memberIds].map((userId) => ({
26 channel_id: channel.id,
27 user_id: userId,
28 role: userId === createdBy ? 'admin' : 'member',
29 }))
30
31 await supabase.from('channel_members').insert(members)
32 revalidatePath('/chat')
33 return channel
34}
35
36export async function loadOlderMessages(
37 channelId: string,
38 beforeTimestamp: string,
39 limit: number = 50
40) {
41 const { data } = await supabase
42 .from('messages')
43 .select('*')
44 .eq('channel_id', channelId)
45 .lt('created_at', beforeTimestamp)
46 .order('created_at', { ascending: false })
47 .limit(limit)
48
49 return data?.reverse() ?? []
50}

Expected result: Scrolling to the top of the message list loads older messages. Users can create new channels and invite members via a Dialog form.

Complete code

app/api/messages/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function POST(req: NextRequest) {
10 const { channelId, senderId, content } = await req.json()
11
12 const { data: member } = await supabase
13 .from('channel_members')
14 .select('user_id')
15 .eq('channel_id', channelId)
16 .eq('user_id', senderId)
17 .single()
18
19 if (!member) {
20 return NextResponse.json(
21 { error: 'Not a channel member' },
22 { status: 403 }
23 )
24 }
25
26 const { data: message, error } = await supabase
27 .from('messages')
28 .insert({ channel_id: channelId, sender_id: senderId, content })
29 .select('id, channel_id, sender_id, content, created_at')
30 .single()
31
32 if (error) {
33 return NextResponse.json({ error: error.message }, { status: 500 })
34 }
35
36 await supabase
37 .from('channel_members')
38 .update({ last_read_at: new Date().toISOString() })
39 .eq('channel_id', channelId)
40 .eq('user_id', senderId)
41
42 return NextResponse.json({ message })
43}
44
45export async function GET(req: NextRequest) {
46 const { searchParams } = new URL(req.url)
47 const channelId = searchParams.get('channel_id')!
48 const before = searchParams.get('before')
49 const limit = parseInt(searchParams.get('limit') ?? '50')
50
51 let query = supabase
52 .from('messages')
53 .select('*')
54 .eq('channel_id', channelId)
55 .order('created_at', { ascending: false })
56 .limit(limit)
57
58 if (before) query = query.lt('created_at', before)
59
60 const { data } = await query
61 return NextResponse.json({ messages: data?.reverse() ?? [] })
62}

Customization ideas

Add file sharing

Upload files to Supabase Storage and send them as message attachments with preview thumbnails for images and download links for documents.

Add message reactions

Add an emoji reaction system where users can react to messages with emojis stored in a reactions table, displayed as small badges below messages.

Add message search

Implement full-text search across messages using Supabase's tsvector with a search Input in the sidebar that highlights matching messages in the thread.

Add thread replies

Add a replies feature where users can start a thread on any message, displayed in a Sheet slide-over with its own Realtime subscription.

Common pitfalls

Pitfall: Not enabling Realtime replication on the messages table

How to avoid: Go to Supabase Dashboard, navigate to Database then Replication, and enable Realtime for the messages table. Also enable it for channel_members if you want real-time unread count updates.

Pitfall: Appending duplicate messages from Realtime subscription

How to avoid: Check the message id before appending to the local list. If the id already exists (from optimistic insert), skip the duplicate. Use a Set or Map keyed by message id.

Pitfall: Loading all messages at once without pagination

How to avoid: Load only the latest 50 messages initially. Use cursor-based pagination (keyset on created_at) to load older messages when the user scrolls to the top. Use IntersectionObserver to detect scroll position.

Best practices

  • Enable Realtime on the messages table in Supabase Dashboard under Database then Replication before subscribing
  • Use cursor-based pagination (keyset on created_at) for message loading — never OFFSET/LIMIT for chat messages
  • Deduplicate messages by checking id before appending from Realtime subscriptions to prevent sender duplicates
  • Use Supabase Realtime Presence for typing indicators and online status — track() to broadcast, presenceState() to read
  • Use Server Components for the channel layout and initial data fetch, Client Components only for the real-time message thread
  • Use Design Mode (Option+D) to adjust message bubble styling, sidebar width, and avatar sizes without spending credits
  • Update last_read_at when the user sends a message or scrolls to the bottom to accurately track unread counts
  • Use RLS policies so only channel members can read and send messages in their channels

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a real-time chat app with Next.js App Router and Supabase Realtime. I need group channels, message pagination, typing indicators via Presence, and online status tracking. Help me design the Realtime subscription pattern that handles both message inserts and Presence events on the same channel.

Build Prompt

Build a Supabase Realtime chat component that subscribes to INSERT events on the messages table filtered by channel_id, and uses Presence to track typing and online status. The component should: load initial 50 messages, subscribe to new inserts and merge without duplicates, track typing state with debounced channel.track(), display typing indicator text, and auto-scroll to new messages unless the user has scrolled up.

Frequently asked questions

How fast are messages delivered with Supabase Realtime?

Supabase Realtime delivers messages within 200ms on average. It uses WebSocket connections maintained by the client, so there is no polling delay. Messages appear nearly instantly for all connected channel members.

How many concurrent users can the free Supabase tier handle?

The free tier supports 200 concurrent Realtime connections. This is enough for development and small teams. For production chat apps with more concurrent users, upgrade to Supabase Pro.

What V0 plan do I need for a chat app?

V0 Premium is recommended because the chat app requires real-time features, multiple components (sidebar, message thread, composer, presence), and Supabase Realtime integration.

How do I handle message history for new channel members?

When a user joins a channel, they see all message history from the beginning. If you want to limit this, add a joined_at timestamp to channel_members and filter messages to only show those created after the member joined.

How do I deploy the chat app?

Click Share then Publish to Production in V0. Supabase Realtime works automatically with the production URL. No additional webhook registration or configuration is needed.

Can RapidDev help build a custom chat application?

Yes. RapidDev has built 600+ apps including real-time messaging platforms with end-to-end encryption, media sharing, and scalable WebSocket infrastructure. Book a free consultation to discuss your chat requirements.

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.