Building a Real-Time App
This is the capstone. Every earlier module added one capability; here we assemble them into a single, small, real-time service — a room-based chat with notifications — and run it on a genuine Node ws server. The goal is not new theory. It is to watch the pieces you already know snap together into something that actually holds up: auth on connect, rooms for routing, heartbeats to detect dead links, and a wire shape that survives reconnect.
The design at a glance
Section titled “The design at a glance”A real-time service is a pipeline that each connection flows through. When a socket arrives we authenticate it, attach per-connection state, let it join rooms, route its messages, and keep it alive with heartbeats until it leaves:
flowchart TD
open["Client connects<br/>(token in query / first message)"] --> auth{"Token valid?"}
auth -- "No" --> reject["close(4401)<br/>unauthorized"]
auth -- "Yes" --> attach["Attach state:<br/>userId, rooms, isAlive"]
attach --> ready["Send 'ready'<br/>client can subscribe"]
ready --> route["Route messages:<br/>join / leave / chat"]
route --> beat["Heartbeat:<br/>ping every 30s, expect pong"]
beat -- "no pong" --> dead["terminate dead socket"]
route -- "client gone" --> clean["close -> leave all rooms"] Each box maps to a module you have already done: auth is security, per-connection state and rooms are server design, heartbeats are the connection lifecycle, and the message envelope is message design. The capstone is just the wiring.
The four ingredients
Section titled “The four ingredients”- Auth on connect. Reject unauthenticated sockets immediately with a custom close code so the client knows not to blindly retry. Here we read a simple token from the upgrade request URL; in production it would be a verified session or JWT (see the security module).
- Rooms. Each connection carries a
Setof rooms it has joined. A chat message is delivered only to other sockets in the same room — the pub/sub pattern from earlier in this module, scoped to a group. - Heartbeats. TCP can go silent without an error event — a laptop sleeps, a phone loses signal. A periodic
pingwith an expectedpongis how the server learns a connection is actually dead and reclaims it, instead of broadcasting forever into a void. - Reconnect-friendliness. The server cannot reconnect for the client, but it can make reconnection cheap: a clean, idempotent
joinso a returning client just re-announces its rooms, and stable close codes so the client knows whether to retry (transient) or stop (auth failure).
The wire protocol
Section titled “The wire protocol”Keep it boringly explicit — a type tag and the fields each type needs:
// client -> servertype ClientMessage = | { type: 'join'; room: string } | { type: 'leave'; room: string } | { type: 'chat'; room: string; text: string };
// server -> clienttype ServerMessage = | { type: 'ready'; userId: string } | { type: 'joined'; room: string } | { type: 'chat'; room: string; from: string; text: string } | { type: 'error'; message: string };Run the real server
Section titled “Run the real server”Below is the body of the connection handler for a real ws server — ws (this socket), wss (the server, for room broadcast), and req (the upgrade request) are already in scope. It authenticates from a ?token= query parameter, attaches per-connection state, handles join/leave/chat with room-scoped broadcast, and runs a per-socket heartbeat. Open it in StackBlitz, run npm start, then connect a client (add ?token=alice to the URL) and watch a join, a room-scoped chat, and the heartbeat in the server log.
Needs the Node.js runtime — open in StackBlitz to run.
To exercise rooms properly, connect two clients with different tokens (e.g. ?token=alice and ?token=bob), have both join the same room, and watch a chat from one appear on the other but never echo back to the sender. Disconnect one and the server’s close handler clears its rooms — no ghost membership left behind.
What you have built
Section titled “What you have built”Step back and notice what this small file is: a complete, defensible real-time service. It refuses anonymous sockets, routes by room instead of blasting everyone, survives clients that vanish without a close, and speaks a typed protocol a reconnecting client can resume against. Every one of those properties came from a separate module — and assembling them is exactly the skill this course set out to teach. From here, scaling it across instances is the pub/sub fan-out you already met; everything else is product.