Stop 19 · Build Full-Stack Portfolio ProjectsProjects · 4/12

Real-time Chat Application

Part of the Full Stack Developer Roadmap

In short

Chat feels simple until you try it: messages must appear instantly for everyone in the room, survive refresh, and arrive in order. It's the classic real-time full stack project.

A complete chat app with rooms and persistence — bringing together WebSockets, auth, and database-backed message history into one polished full-stack product.

The purple blob talks into a microphone in a commentary box while radios light up around an open scorebook.

Think of it like this

The cricket commentary box

The commentator speaks once and every radio tuned to that match hears it live. The scorer writes every ball in the scorebook, so someone tuning in late can catch up. Live broadcast plus a written record: that's chat.

Watch it happen

One message, saved and broadcast

Riya's tab
Socket server
Database
Arjun's phone

connect (auth token)

1/6The browser opens a WebSocket and sends its token. The server checks it before allowing anything.

The big ideas

What you need to know

One open connection

WebSockets keep a two-way pipe open, so the server can push new messages without the browser asking.

Save, then broadcast

Store the message before sending it out. Anyone who reconnects can load what they missed.

Rooms limit who hears

Each chat is a room. Check the user is a member before letting them join it.

Plan for drops

Phones lose signal. On reconnect, fetch messages after the last id you have.

See it in code

Walk through the code

ts
1import { Server } from "socket.io";2const io = new Server(httpServer, { cors: { origin: process.env.WEB_URL } });3io.use(async (socket, next) => {4  socket.data.user = await verifyToken(socket.handshake.auth.token);5  next(socket.data.user ? undefined : new Error("unauthorised"));6});7io.on("connection", (socket) => {8  socket.on("join", async (roomId: string, ack: (msgs: unknown[]) => void) => {9    socket.join(roomId);10    ack(await db.message.findMany({ where: { roomId }, orderBy: { id: "desc" }, take: 50 }));11  });12  socket.on("send", async ({ roomId, text }: { roomId: string; text: string }) => {13    const msg = await db.message.create({ data: { roomId, text, userId: socket.data.user.id } });14    io.to(roomId).emit("message", msg);15  });16});

1/3Connection middleware: no valid token, no socket. This runs once per connection.

A Socket.IO server sitting next to your API. It checks the token once on connect, sends history on join, and saves every message before broadcasting it.

Save yourself the pain

Common mistakes

Oops

Letting any connected user join any room id they send.

Do this instead

Check membership in the database inside the join handler before calling socket.join.

Oops

Keeping messages only in server memory.

Do this instead

Persist every message. Memory is lost on restart and isn't shared when you run two servers.

Your mission

First milestone: two tabs talking

~40 min

Quick check

Why should the server save a chat message to the database before broadcasting it?

Go deeper

  1. 1Support multiple chat rooms or one-to-one conversations.
  2. 2Persist messages so history survives a refresh or reconnect.
  3. 3Add typing indicators and online/offline presence.

More projects at this stop

Stuck on this topic? Ask an Insider

Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.

Download
InsideEdge