Stop 12 · Real-time FeaturesProjects · 1/1

Real-time Collaborative App

Part of the Full Stack Developer Roadmap

In short

Build a board that several people edit at once, with every change showing up live. It pulls together REST, WebSockets, a database and cache sync, which is exactly what interviewers love to dig into.

A shared, live-updating app (a collaborative list, board, or simple document) — bringing together WebSockets, state sync, and multi-user considerations across your whole stack.

The blob character sticks a note on a fridge door while other hands add their own notes.

Think of it like this

The shared fridge door

Flatmates stick notes on the fridge door: 'milk over', 'gas bill paid'. Everyone sees the same door, anyone can add or move a note, and it's still there tomorrow. Your app is that fridge door online: shared, live, and saved in a database.

Watch it happen

Build it in layers

Data model
REST CRUD
Rooms
Live sync
Presence

1/6Tables for boards, members and cards, with column, position and version on each card. Seed one board.

The big ideas

What you need to know

Multi-user boards

Invite members by email. Only members can join a board's room or call its APIs.

Live card moves

Create, edit, move and delete cards, and everyone on the board sees it within a second.

Who's here

Show avatars of people viewing the board right now. Presence is short-lived, so keep it out of the database.

The server decides

Clients send changes, the server validates and saves them, then broadcasts the result. Never broadcast unsaved data.

See it in code

Walk through the code

ts
1io.use(authenticateSocket); // sets socket.data.user from the session cookie2 3io.on("connection", (socket) => {4  socket.on("board:join", async (boardId: string) => {5    if (!(await isMember(socket.data.user.id, boardId))) return;6    socket.join(`board:${boardId}`);7  });8 9  socket.on("card:move", async ({ cardId, column }, ack) => {10    const card = await moveCard(socket.data.user.id, cardId, column); // checks access, bumps version11    if (!card) return ack({ ok: false });12    io.to(`board:${card.boardId}`).emit("card:moved", card);13    ack({ ok: true, card });14  });15});

1/4Every socket is authenticated before any event handler runs.

A Socket.io server. authenticateSocket, isMember and moveCard are your own functions that reuse the same checks as your REST routes.

Save yourself the pain

Common mistakes

Oops

Broadcasting a change before it's saved to the database.

Do this instead

Save first, then emit the saved row. Otherwise a failed write leaves everyone's screen wrong.

Oops

Letting any socket join any room just by sending a board id.

Do this instead

Check membership on the server before socket.join, exactly as you would on a REST route.

Oops

Storing card order as array indexes that clash when two people move cards at once.

Do this instead

Give each card a position number and let the server's saved value win.

Your mission

First milestone: live cards in two tabs

~45 min

Quick check

Two users drag the same card at nearly the same moment. What keeps every screen consistent?

Go deeper

  1. 1Show multiple users' changes appearing live without a page refresh.
  2. 2Handle two users editing the same item at nearly the same time.
  3. 3Add a simple presence indicator showing who else is currently connected.

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