Skip to main content
Early access — new tools and guides added regularly
🔵 Build Real Projects — Guide 14 of 16
View track
>_ claude codeIntermediate40 min

Build a Real-Time Chat App

Build a chat application with WebSockets, message persistence, typing indicators, read receipts, and online presence — the real-time patterns behind every messaging app.

What you will build
A real-time chat application with WebSocket communication and message history

How real-time communication works

Normal web requests follow a request-response pattern: the browser asks the server for something, the server responds, and the connection closes. This works for loading web pages but fails for chat — you cannot keep asking the server are there new messages? every second. WebSockets solve this by establishing a persistent, bidirectional connection between the browser and server. Once connected, either side can send messages at any time without the overhead of new HTTP requests. When Alice sends a message, her browser sends it through the WebSocket to the server. The server receives it and immediately pushes it through Bob's WebSocket connection. Bob sees the message appear instantly, without refreshing or polling. Ask Claude Code: Create a new Next.js project for a real-time chat application. Set up TypeScript and Tailwind CSS. Create a types file at src/lib/types.ts with interfaces for User (id, name, avatar colour, status as online or offline), Message (id, sender id, channel id, text, created at, edited at optional), Channel (id, name, members as user ids, created at), and TypingIndicator (user id, channel id, timestamp). Install the ws package for WebSocket support on the server: npm install ws. Also install @types/ws: npm install -D @types/ws. Create a basic WebSocket server in a separate file server.ts that listens on port 3001. The server should accept connections, log when clients connect and disconnect, and echo back any message received. Test by connecting with a WebSocket client tool or a simple browser script.

Building the WebSocket server

The WebSocket server is the hub that routes messages between connected clients. Ask Claude Code: Build a proper WebSocket server in server.ts with the following features. Maintain a map of connected clients with their user ID and WebSocket connection. When a client connects, they send a join message with their user ID. The server adds them to the connected clients map and broadcasts a presence update to all other clients. When a client sends a chat message, the server validates it, assigns an ID and timestamp, saves it to an in-memory messages array, and broadcasts it to all members of the channel. When a client disconnects, remove them from the map and broadcast an offline presence update. Implement these message types: join (client identifies itself), message (chat message to a channel), typing (user is typing in a channel), stop_typing (user stopped typing), and presence (server broadcasts online/offline status). Each message is a JSON string with a type field and relevant data. Ask Claude Code: Add channel support. Clients send a subscribe message to join a channel. The server tracks which clients are subscribed to which channels. Messages are only broadcast to clients subscribed to the target channel, not to everyone. Create three default channels: general, random, and help. When a client subscribes to a channel, send them the last 50 messages from that channel's history. Test with two browser tabs — open the app in both, send a message from one tab, and verify it appears in the other tab instantly. If messages are not appearing, check the browser console for WebSocket connection errors. Common issues: CORS blocking the WebSocket connection (ensure the server allows the correct origin) and the WebSocket URL being wrong (use ws://localhost:3001 for local development).

Creating the chat interface

Ask Claude Code: Create the chat UI with three components. First, ChannelSidebar at src/components/ChannelSidebar.tsx showing a list of channels with unread message indicators. The active channel should be highlighted. Second, MessageList at src/components/MessageList.tsx showing messages in chronological order. Each message displays the sender name with their avatar colour, the message text, and a timestamp. Messages from the current user should appear on the right side with a different background colour. Group consecutive messages from the same sender without repeating the name. Third, MessageInput at src/components/MessageInput.tsx with a text input, send button, and keyboard shortcut — Enter to send, Shift+Enter for a new line. The input should auto-focus when switching channels. Create the main chat page at src/app/page.tsx that combines all three components in a layout: sidebar on the left (250px wide), message list taking the remaining space, and message input fixed at the bottom. Use a WebSocket hook at src/hooks/useWebSocket.ts that manages the connection, handles reconnection on disconnect, and exposes functions to send messages and subscribe to incoming messages. Ask Claude Code: Add auto-scrolling to the message list. When a new message arrives, scroll to the bottom automatically — but only if the user was already scrolled to the bottom. If they scrolled up to read history, do not auto-scroll and instead show a button at the bottom saying New messages with an arrow. Clicking the button scrolls to the bottom. This pattern respects the user's reading position while indicating new activity.

Typing indicators and presence

Typing indicators show three dots or a message like Alice is typing when someone is composing a message. Ask Claude Code: Add typing indicators to the chat. When the user types in the message input, send a typing event through the WebSocket with the user ID and channel ID. When they stop typing for 2 seconds or send the message, send a stop_typing event. On the receiving end, show a typing indicator below the last message: Alice is typing for one person, Alice and Bob are typing for two, and 3 people are typing for three or more. Use a debounce pattern for the typing event — only send it once when the user starts typing, not on every keystroke. Reset the debounce timer on each keystroke. If no keystroke for 2 seconds, send stop_typing automatically. The server should also clean up stale typing indicators — if a client disconnects without sending stop_typing, remove their indicator after 5 seconds. Ask Claude Code: Add online presence to the channel sidebar. Show a green dot next to each channel member who is currently online. At the top of the sidebar, show a count of online members. Create a user list panel that shows all members of the current channel with their online or offline status, sorted with online users first. The server maintains presence by tracking connected clients. When a client connects with their join message, broadcast their online status. When they disconnect, broadcast offline. Add a heartbeat mechanism: the client sends a ping every 30 seconds, the server responds with a pong. If the server does not receive a ping for 60 seconds, consider the client disconnected and broadcast offline status. This catches cases where the WebSocket connection drops without a clean close event.

Message persistence and history

In-memory storage loses all messages when the server restarts. Ask Claude Code: Add SQLite persistence to the chat server. Create a database with tables for users (id, name, avatar_colour, created_at), channels (id, name, created_at), channel_members (channel_id, user_id, joined_at), and messages (id, channel_id, sender_id, text, created_at, edited_at). Install better-sqlite3: npm install better-sqlite3. Update the WebSocket server to save messages to the database when they are sent and load history from the database when a client subscribes to a channel. Load the last 50 messages by default. When the user scrolls to the top of the message list, load the next 50 messages — this is called infinite scroll or pagination. Ask Claude Code: Implement infinite scroll in the MessageList component. When the user scrolls to the top, send a load_history request through the WebSocket with the channel ID and the oldest message ID currently displayed. The server responds with the next 50 messages older than that ID. Prepend them to the message list without changing the scroll position — the user should not jump to a different place. Show a loading spinner at the top while messages are being fetched. Stop loading when there are no more messages. Ask Claude Code: Add message editing and deletion. Double-clicking a message opens an inline editor. Pressing Enter saves the edit, Escape cancels. The server broadcasts the edit to all channel subscribers. Show an edited indicator next to edited messages. For deletion, add a context menu on right-click with a Delete option. Deleted messages show as This message was deleted in italic grey text. Only the message sender can edit or delete their messages. Store edits and deletions in the database so they persist across sessions.

Read receipts and notifications

Users need to know if their messages have been seen. Ask Claude Code: Add read receipt tracking. When a user views a channel, send a mark_read event with the channel ID and the ID of the last message they have seen. The server stores the last-read message ID per user per channel. When requesting channel data, include the unread count calculated as messages in the channel with an ID greater than the user's last-read message ID. Show unread counts as badges on channel names in the sidebar. Show a blue dot next to channels with unread messages. When entering a channel, automatically mark it as read. For individual message receipts, track which users have seen each message. Below each message sent by the current user, show small avatar circles of users who have read it — similar to iMessage or WhatsApp. Limit the display to 5 avatars with a +N indicator for more. Ask Claude Code: Add browser notifications for messages in channels the user is not currently viewing. Use the Notification API: first request permission with Notification.requestPermission(), then create notifications with new Notification(title, { body, icon }). Only show notifications when the browser tab is not focused — check with document.hidden. Include the sender name and a preview of the message text truncated to 50 characters. Clicking the notification should focus the chat window and switch to the relevant channel. Ask Claude Code: Add a sound notification for new messages. Use the Web Audio API to play a short, subtle notification sound. Allow users to mute sounds with a toggle in the sidebar. Store the mute preference in localStorage. Do not play sounds for the user's own messages. These notification features make the chat app feel professional and ensure users do not miss important messages.

Rooms, direct messages, and threads

A complete chat app supports multiple conversation types. Ask Claude Code: Add direct messaging. Create a way to start a private conversation between two users. Direct messages appear in the sidebar under a Direct Messages heading, separate from channels. The DM entry shows the other person's name and avatar. Implement DMs as private channels with exactly two members. When a user clicks another user's name in the member list, open or create a DM channel with that person. DM channels should not appear in the public channel list. Ask Claude Code: Add message threads. When a user hovers over a message, show a Reply in thread button. Clicking it opens a thread panel on the right side of the screen showing the original message and any replies. Thread replies are messages with a parent_id pointing to the original message. In the main channel view, threaded messages show a reply count link like 3 replies — last reply 2 minutes ago. Clicking the link opens the thread panel. Thread replies do not appear in the main channel timeline to keep the main conversation clean. The data model change is simple: add an optional parent_id to the Message interface. Messages with no parent_id are top-level channel messages. Messages with a parent_id are thread replies. The server routes thread replies only to users who have the thread open, plus the original message author. Ask Claude Code: Add the ability to create new channels. Add a plus button in the sidebar that opens a form for channel name and description. The creator is automatically added as a member. Add an invite flow where channel members can add other users. Add a leave channel option. These features round out the chat application with the same functionality users expect from tools like Slack or Discord.

Scaling and deployment considerations

Deploying a WebSocket application requires different considerations than a typical web app. Ask Claude Code: Prepare the chat application for production deployment. Address these concerns. First, separate the WebSocket server from the Next.js app. The Next.js app deploys to Vercel for the UI, while the WebSocket server deploys to Railway or Fly.io as a standalone Node.js process. Update the client to connect to the WebSocket server URL from an environment variable. Second, replace SQLite with PostgreSQL for production. SQLite does not support concurrent writes from multiple server instances. Create a migration script that sets up the PostgreSQL schema. Use connection pooling with a library like pg-pool. Third, add authentication. The WebSocket handshake should include a JWT token in the connection URL or as a cookie. The server validates the token before accepting the connection. Reject connections with invalid or expired tokens. Ask Claude Code: Add horizontal scaling support. When you run multiple WebSocket server instances behind a load balancer, a message sent to one instance needs to reach clients connected to other instances. Use Redis pub/sub as a message broker. When a server receives a message, it publishes it to a Redis channel. All server instances subscribe to Redis and forward messages to their local clients. Install ioredis and update the message routing. Test by running two server instances on different ports, connecting a client to each, and verifying messages flow between them. Ask Claude Code: Add connection recovery. When the WebSocket disconnects, the client should automatically reconnect with exponential backoff. After reconnecting, it should request messages it missed during the disconnection. Track the last received message ID and request a gap fill from the server. This ensures users never miss messages even on unreliable networks.

Related Lesson

Real-Time Application Architecture

This guide is hands-on and practical. The full curriculum covers the conceptual foundations in depth with structured lessons and quizzes.

Go to lesson