Build Guide
How to Build a Chat App
Building a chat app in 2026 takes 2 days with the right stack — the hard part is handling concurrent connections, not the UI.
Real-time messaging is one of the most requested features in every product category. Build a chat app and you'll understand WebSockets, presence detection, message queues, and push notifications.
Data model
The core tables you'll need before writing any UI.
Build order
The sequence that minimises rewrites — build in this order.
Static message list UI
Build the message list and input box with hardcoded messages. Focus on layout — message bubbles, timestamps, sender avatars. No logic yet.
Set up Supabase Realtime
Create the messages table and subscribe to inserts using supabase.channel(). Send a message via INSERT and watch it appear in real time. This is the core of the app.
Add auth and user presence
Add Clerk or Supabase Auth. Create a users table. Track online presence by updating last_seen_at every 30 seconds and showing a green dot for users active in the last 2 minutes.
Build DM and group rooms
Create the rooms and room_members tables. Add a room sidebar. For DMs, find or create a room with exactly the two user IDs. For groups, allow adding multiple members.
Read receipts and typing indicators
Track last_read_at per room_member and update it when the user scrolls to the bottom. For typing, use a Supabase Realtime broadcast channel (not database) — ephemeral, no DB writes.
File and image sharing
Upload files to Supabase Storage or Cloudinary. Store the URL in the message content field with a type of "image" or "file". Render images inline, files as download links.
Push notifications
Use Expo Notifications (mobile) or the Web Push API (browser). Store device tokens per user. Send a push when a new message arrives in a room the user is not currently viewing.
Done when
Observable behaviors that confirm V1 is complete — verify each one before you ship.
- ✓
User A sends a message; User B sees it appear in real time without refreshing
- ✓
Typing indicator appears within 500ms of the other user starting to type
- ✓
User can create a DM with another user and the conversation persists across sessions
- ✓
Unread badge appears in the sidebar for rooms with new messages
- ✓
User can upload an image and see it rendered inline in the chat
First Run Requirement
Seed a General public room with 5 demo messages from 2 bot users so the app does not open to silence. Demo users are pre-created — no invite flow needed to demo the UI.
Build it with AI — Architect Prompt
Paste this into Claude, Cursor, Windsurf, or any AI coding tool. It includes the full context of this guide — data model, build order, done conditions, and pitfalls — so the AI starts with everything it needs.
<context>
App: Chat App
Difficulty: Intermediate
Estimated build time: 3–7 days
Tech stack (intermediate): Next.js + Supabase Realtime + Clerk — self-hostable, Postgres-backed, production-ready
Data model:
User: id, username, avatar_url, last_seen_at
Room: id, name, type (direct/group), created_by, created_at
RoomMember: room_id, user_id, joined_at, last_read_at
Message: id, room_id, user_id, content, type (text/image/file), created_at, edited_at
Reaction: message_id, user_id, emoji, created_at
FIRST RUN REQUIREMENT:
Seed a General public room with 5 demo messages from 2 bot users so the app does not open to silence. Demo users are pre-created — no invite flow needed to demo the UI.
DONE WHEN — verify each before marking V1 complete:
□ User A sends a message; User B sees it appear in real time without refreshing
□ Typing indicator appears within 500ms of the other user starting to type
□ User can create a DM with another user and the conversation persists across sessions
□ Unread badge appears in the sidebar for rooms with new messages
□ User can upload an image and see it rendered inline in the chat
Recommended build order:
1. Define API contract + schema + seed data (always first)
2. Static message list UI — Build the message list and input box with hardcoded messages. Focus on layout — message bubbles, timestamps, sender avatars. No logic yet.
3. Set up Supabase Realtime — Create the messages table and subscribe to inserts using supabase.channel(). Send a message via INSERT and watch it appear in real time. This is the core of the app.
4. Add auth and user presence — Add Clerk or Supabase Auth. Create a users table. Track online presence by updating last_seen_at every 30 seconds and showing a green dot for users active in the last 2 minutes.
5. Build DM and group rooms — Create the rooms and room_members tables. Add a room sidebar. For DMs, find or create a room with exactly the two user IDs. For groups, allow adding multiple members.
6. Read receipts and typing indicators — Track last_read_at per room_member and update it when the user scrolls to the bottom. For typing, use a Supabase Realtime broadcast channel (not database) — ephemeral, no DB writes.
7. File and image sharing — Upload files to Supabase Storage or Cloudinary. Store the URL in the message content field with a type of "image" or "file". Render images inline, files as download links.
8. Push notifications — Use Expo Notifications (mobile) or the Web Push API (browser). Store device tokens per user. Send a push when a new message arrives in a room the user is not currently viewing.
9. End-to-end verification — walk every Done When condition above (always last)
Known pitfalls to avoid:
- Polling instead of WebSockets — setInterval fetch every 2 seconds feels laggy and hammers your database. Use Supabase Realtime or Socket.io from day one.
- Storing typing state in the database — every keypress becomes a DB write for every user in the room. Use a Realtime broadcast channel (in-memory, not persisted) for typing indicators.
- Not paginating message history — loading 10,000 messages on room join will crash mobile browsers. Fetch the last 50 messages and load more as the user scrolls up.
- Forgetting message ordering under concurrent inserts — sort by created_at, but add an id (ulid or snowflake) as a tiebreaker for messages sent in the same millisecond.
</context>
<role>
You are a Senior Full-Stack Engineer and product architect who has shipped production Chat Apps before. You know exactly where developers get stuck and how to structure the project to avoid rewrites.
</role>
<task id="step-1-clarify">
Before writing any code or spec, ask me 3–5 clarifying questions that will meaningfully change the architecture. Focus on: scale expectations, auth requirements, platform (web / mobile / both), must-have vs nice-to-have features for the MVP, and any hard constraints (budget, deadline, existing infrastructure).
<example>
BAD: "What tech stack do you want to use?" — too broad, doesn't change architecture decisions.
GOOD: "Do you need real-time sync across devices, or is single-device with periodic refresh acceptable? This decides whether we use WebSockets or simple REST polling and significantly affects infrastructure complexity."
</example>
⚠️ Do NOT start planning or writing code until I answer. Present the questions, then stop and wait.
</task>
<task id="step-2-architect">
After I answer your questions, produce the following in order:
1. TECHNICAL SPECIFICATION
- System architecture using → to show data and event flow
- Core data model (refer to data model in <context>)
- API contract — list ALL routes with request/response shapes BEFORE any implementation
WHY: agreeing on contracts first prevents rewrites when frontend and backend shapes diverge
- External APIs and integration points
2. MVP PLAN in three phases:
SETUP ✦ Step 1 (always): API contract + schema migration + seed data
Done when: schema is migrated, seed script runs, app starts with demo data, zero manual setup.
CORE FEATURES — build in this exact order:
2. Static message list UI
3. Set up Supabase Realtime
4. Add auth and user presence
5. Build DM and group rooms
6. Read receipts and typing indicators
7. File and image sharing
8. Push notifications
DONE WHEN — one observable condition per feature:
□ User A sends a message; User B sees it appear in real time without refreshing
□ Typing indicator appears within 500ms of the other user starting to type
□ User can create a DM with another user and the conversation persists across sessions
□ Unread badge appears in the sidebar for rooms with new messages
□ User can upload an image and see it rendered inline in the chat
STRETCH GOALS — post-launch additions that are explicitly out of V1 scope.
3. BLOCKER ANALYSIS
Flag: API rate limits, auth complexity, scope risks, cold-start problems, top 2–3 failure modes.
FIRST RUN REQUIREMENT: Seed a General public room with 5 demo messages from 2 bot users so the app does not open to silence. Demo users are pre-created — no invite flow needed to demo the UI.
✓ Done when: app launches with seed data and the full user journey works without any manual setup.
<self_check>
Before presenting your output, verify:
□ Every answer I gave to clarifying questions is reflected in the spec
□ Step 1 is ALWAYS: API contract + schema + seed — never UI first
□ Done When criteria are observable user behaviors, not internal states
□ The plan is realistic for one person to ship within 3–7 days
□ All known pitfalls from <context> are addressed in the spec
</self_check>
</task>
<task id="step-2.5-agents-md">
Generate an AGENTS.md file at the project root. This file is read automatically by Claude Code, Cursor, Windsurf, and all major AI coding tools at the start of every session.
Include:
- Project overview (2–3 sentences)
- Tech stack with exact version numbers
- Folder structure with one-line descriptions per directory
- Key architectural decisions and the WHY behind each
- Coding conventions: camelCase components, kebab-case files, max 200 lines per file, one concern per file
- Available commands: dev, build, test, lint, db:migrate, db:seed
- MVP scope boundaries — features explicitly out of V1
Note in the file: "Cursor users: symlink .cursorrules → AGENTS.md. Claude Code users: symlink CLAUDE.md → AGENTS.md."
</task>
<task id="step-3-implement">
Implement the full project in the exact order from step 2. For each step:
- Write complete code (no placeholders or TODOs)
- Confirm the step works before moving to the next
<constraints>
- Step 1 is ALWAYS: define all API routes + TypeScript request/response interfaces + run schema migration
WHY: agreeing on contracts before writing a single component prevents shape mismatches that require rewrites
- Final step is ALWAYS: start the app and walk every Done When condition from <context> end-to-end
WHY: a spec that passes unit tests but breaks the core user journey is not done
- Max 200 lines per file. WHY: every file must fit in one AI context window for complete reasoning
- One concern per file. WHY: mixing auth logic into API routes makes it impossible to reuse — extract to lib/auth.ts
- TypeScript strict mode, no `any` types. WHY: catches data shape mismatches at compile time, not in production
- All database queries in server components or API routes only. WHY: keeps credentials server-side
- All environment variables documented in .env.example. WHY: next developer sets up in under 5 minutes
</constraints>
</task>How to use this prompt
- 1.Copy the prompt above
- 2.Open Claude, Cursor (Cmd+L), or Windsurf
- 3.Paste the prompt and send — the AI will ask 3–5 clarifying questions
- 4.Answer the questions, then it generates your full project spec
- 5.Continue in the same session to implement step by step
Common mistakes
What trips up most developers building this for the first time.
Polling instead of WebSockets — setInterval fetch every 2 seconds feels laggy and hammers your database. Use Supabase Realtime or Socket.io from day one.
Storing typing state in the database — every keypress becomes a DB write for every user in the room. Use a Realtime broadcast channel (in-memory, not persisted) for typing indicators.
Not paginating message history — loading 10,000 messages on room join will crash mobile browsers. Fetch the last 50 messages and load more as the user scrolls up.
Forgetting message ordering under concurrent inserts — sort by created_at, but add an id (ulid or snowflake) as a tiebreaker for messages sent in the same millisecond.
Recommended tech stack
Pick the level that matches your experience.
Firebase Realtime Database + React — handles WebSocket complexity automatically
Next.js + Supabase Realtime + Clerk — self-hostable, Postgres-backed, production-ready
Node.js + Socket.io + Redis pub/sub + Postgres + S3 — full control over scaling
Take it further — related ideas
Each comes with revenue math, a full build guide, and a prompt to paste into Claude or Cursor.