# Build a Real-Time Chat (Node + WebSockets)

> Build a real-time chat app with Node and WebSockets - a server that broadcasts messages, a browser client, usernames, and rooms - built and run on your machine.


---

# Build a Real-Time Chat (Node + WebSockets)

You've used chat apps your whole life. A message you type shows up on someone else's screen a heartbeat later, no refresh, no waiting. This weekend you're going to build that yourself - the actual mechanism - and watch two browser tabs talk to each other in real time.

We're building a chat app with **Node** on the server and **WebSockets** as the wire between browsers. By the end you'll have a server that relays messages to everyone connected, a browser page to send and read them, named users, join and leave notices, and separate rooms so two conversations don't bleed into each other.

This one runs **on your machine.** You'll install Node, run a real server process, and open the client in your own browser. Nothing here lives in a sandbox - it's a program you start with `node` and stop with Ctrl+C, the same way you'd run anything in production.

## Why WebSockets

Regular web requests are one-shot: the browser asks, the server answers, the line goes dead. That's fine for loading a page. It's wrong for chat, because the server has no way to push you a message that arrived after your last request. You'd have to keep asking "anything new? anything new?" - wasteful and laggy.

A WebSocket is different. The browser and server shake hands once, then keep the connection open. After that, **either side can send a message at any time.** The server can shove a new chat line down to you the instant it arrives. That open, two-way pipe is the whole reason chat feels instant, and it's what you'll wire up.

```mermaid
graph LR
  A[Browser Tab 1] -- open socket --> S[Node Server]
  B[Browser Tab 2] -- open socket --> S
  C[Browser Tab 3] -- open socket --> S
  S -- broadcast --> A
  S -- broadcast --> B
  S -- broadcast --> C
```

## The stack

| Piece | What it is | Why we use it |
|-------|-----------|---------------|
| Node.js | JavaScript runtime outside the browser | Runs the server; one language front to back |
| `ws` | A small WebSocket library for Node | Handles the handshake and framing so you don't |
| Plain HTML + JS | The client | Browsers speak WebSocket natively - no client library needed |

That's the entire dependency list: one package, `ws`. The browser side uses the built-in `WebSocket` object, so there's nothing to install there at all.

## What you'll need

- **Node.js 18 or newer.** Check with `node --version` in a terminal. If you don't have it, grab the LTS build from nodejs.org.
- A text editor and a terminal.
- A modern browser. Any of them works.

Rough time: a focused afternoon, maybe three to four hours if you read as you go.

## What you'll learn

- How a WebSocket connection opens and stays open
- The broadcast pattern - taking one incoming message and fanning it out to many clients
- Sending structured data (JSON) over a socket instead of raw strings
- Tracking per-connection state, like which user owns which socket and what room they're in
- How to run a server and a client together and debug when they don't connect

## The phases

1. **Setup and a WebSocket Server** - get Node going, install `ws`, stand up a server that accepts connections and logs what it hears.
2. **Broadcasting Messages** - relay a message from one client to everyone else. This is the heart of it.
3. **The Browser Client** - a real HTML page with an input box and a scrolling message list.
4. **Usernames and Join/Leave** - let people pick a name, and announce when someone arrives or drops.
5. **Rooms, and Running It** - split the chat into channels, run the whole thing end to end, and look at where you'd take it next.

Each phase leaves you with something that works. By phase 3 you can already type in one tab and see it in another. By phase 5 you've got a small but genuine chat server. Let's set it up.


---

# Setup and a WebSocket Server

Everything you build this weekend stands on a running Node program. So that's where we start: a folder, one dependency, and a server that listens. By the end of this phase you'll have a process that accepts a WebSocket connection and prints whatever gets sent to it. No browser yet - we'll poke it with a tiny test client so you can see the wire light up.

This is built on your machine. Open a terminal, and let's make the project.

## Make the project

Pick a folder and create the project with npm. Run these one at a time:

```bash
mkdir realtime-chat
cd realtime-chat
npm init -y
```

`npm init -y` writes a `package.json` with default answers - that's the file npm uses to track your dependencies and scripts. The `-y` skips the questionnaire.

Now install the one thing we need:

```bash
npm install ws
```

`ws` is a WebSocket implementation for Node. It handles the protocol's handshake and the byte-level framing of messages so you can think in terms of "a client connected" and "a message arrived" instead of socket plumbing.

One small thing that saves headaches later: tell Node we're writing modern module syntax. Open `package.json` and add a `"type"` line so it looks roughly like this:

```json
{
  "name": "realtime-chat",
  "version": "1.0.0",
  "type": "module",
  "main": "server.js",
  "dependencies": {
    "ws": "^8.18.0"
  }
}
```

The `"type": "module"` lets you use `import` instead of `require`. Your exact version numbers may differ - that's fine.

## Write the server

Create a file called `server.js` next to `package.json`:

```javascript
import { WebSocketServer } from "ws";

const PORT = 8080;
const wss = new WebSocketServer({ port: PORT });

console.log(`Chat server listening on ws://localhost:${PORT}`);

wss.on("connection", (socket) => {
  console.log("A client connected.");

  socket.on("message", (data) => {
    const text = data.toString();
    console.log("Received:", text);
  });

  socket.on("close", () => {
    console.log("A client disconnected.");
  });
});
```

Let's read it top to bottom, because every line here comes back later.

`new WebSocketServer({ port: 8080 })` starts a server listening on port 8080. It speaks the WebSocket protocol, not HTTP, which is why the address is `ws://` and not `http://`.

`wss.on("connection", ...)` runs once for **each** client that connects. The `socket` it hands you represents that one client's open pipe. Hold onto that idea - in the next phase you'll keep a list of these sockets so you can talk to all of them at once.

Inside, we listen for three things on that socket:

- `"message"` fires when the client sends data. WebSocket data arrives as a buffer, so we call `.toString()` to read it as text.
- `"close"` fires when the client goes away - tab closed, network dropped, whatever.

Right now we don't reply to anything. We're confirming the connection works and that messages reach us.

## Run it

Start the server:

```bash
node server.js
```

You should see:

```
Chat server listening on ws://localhost:8080
```

Leave that terminal running. The server stays up until you stop it with Ctrl+C.

## Poke it with a test client

A server with nothing connected is hard to believe in. Open a **second** terminal in the same folder and write a throwaway client, `test-client.js`:

```javascript
import { WebSocket } from "ws";

const socket = new WebSocket("ws://localhost:8080");

socket.on("open", () => {
  console.log("Connected to server.");
  socket.send("hello from the test client");
  setTimeout(() => socket.close(), 500);
});
```

Run it:

```bash
node test-client.js
```

In the **test client** terminal you'll see `Connected to server.` and then the process ends. In the **server** terminal you'll see this appear:

```
A client connected.
Received: hello from the test client
A client disconnected.
```

That's the whole loop working: a client opened a socket, sent a string, the server received and logged it, then the client closed and the server noticed. The connection is real and bidirectional - we tested one direction, and the next phase uses the other.

## What you have now

A Node process that accepts WebSocket connections and reports every message. It doesn't do anything *with* those messages yet - it can hear, but it can't speak to the room.

Keep `test-client.js` around for quick checks, but the real client is a browser page we'll build in phase 3. Next up: take a message that lands on one socket and send it out to every other connected client. That's broadcasting, and it's what turns a logger into a chat server.


---

# Broadcasting Messages

Right now your server hears a message and logs it. A real chat does something more useful: it takes that message and sends it back out to everyone in the room. One person types, everyone reads. That fan-out is called **broadcasting**, and it's the single most important pattern in the whole project.

This phase turns your logger into a relay. By the end, two connected clients will see each other's messages - and you'll prove it with two test clients before we ever touch a browser.

Built on your machine, same as before. Keep that server terminal handy.

## The idea

When a message lands on one socket, you want to send it to all the *other* sockets. To do that, the server needs to know who's connected. The good news: `ws` already keeps that list for you.

Your `WebSocketServer` has a `.clients` property - a `Set` of every socket currently connected. To broadcast, you loop over that set and call `.send()` on each one.

```mermaid
graph TD
  C1[Client 1 sends 'hi'] --> S[Server]
  S --> O1[Client 1]
  S --> O2[Client 2]
  S --> O3[Client 3]
```

There's one decision to make: do you echo the message back to the sender too? Most chat UIs *do* show your own message, but they usually render it locally the moment you hit send rather than waiting for it to round-trip. To keep the server simple and the client lean, we'll broadcast to **everyone except the sender**, and let each client show its own messages. You can flip this later in two lines.

## Update the server

Open `server.js` and change the `"message"` handler so it relays instead of only logging:

```javascript
import { WebSocketServer } from "ws";

const PORT = 8080;
const wss = new WebSocketServer({ port: PORT });

console.log(`Chat server listening on ws://localhost:${PORT}`);

function broadcast(message, sender) {
  for (const client of wss.clients) {
    const isOpen = client.readyState === client.OPEN;
    if (isOpen && client !== sender) {
      client.send(message);
    }
  }
}

wss.on("connection", (socket) => {
  console.log("A client connected.");

  socket.on("message", (data) => {
    const text = data.toString();
    console.log("Relaying:", text);
    broadcast(text, socket);
  });

  socket.on("close", () => {
    console.log("A client disconnected.");
  });
});
```

The new part is the `broadcast` function. Walk through it:

- `wss.clients` is the set of every connected socket. We loop over all of them.
- `client.readyState === client.OPEN` checks the socket is actually ready to receive. A connection that's mid-close will throw if you `send()` to it, so we skip anything that isn't open. This guard prevents real crashes - don't drop it.
- `client !== sender` skips the person who sent the message, so they don't get their own words bounced back.

That's the broadcast pattern in full. Every chat server you've ever used has a version of this loop at its core.

## Why the readyState check matters

It's tempting to write `for (const client of wss.clients) client.send(message)` and call it done. Then someone closes their tab at the exact moment another person sends a message, and your server tries to write to a half-dead socket. Depending on timing, that's an exception that can take the whole process down.

Checking `readyState === OPEN` before sending is the cheap insurance. The set can contain sockets that are connecting or closing, not only open ones, and only open ones can take a message.

## Test it with two clients

This is where it gets satisfying. We'll run two test clients at once and watch a message cross from one to the other.

Replace `test-client.js` with this. It takes a label and a message from the command line, listens for anything the server relays to it, then sends its own message:

```javascript
import { WebSocket } from "ws";

const label = process.argv[2] || "client";
const message = process.argv[3] || "hello";

const socket = new WebSocket("ws://localhost:8080");

socket.on("open", () => {
  console.log(`[${label}] connected`);
  setTimeout(() => socket.send(`${label} says: ${message}`), 300);
});

socket.on("message", (data) => {
  console.log(`[${label}] received:`, data.toString());
});

setTimeout(() => process.exit(0), 2000);
```

Make sure the server is running (`node server.js` in its own terminal). Then, in two more terminals, start two clients close together:

In terminal A:

```bash
node test-client.js alice "good morning"
```

And quickly, in terminal B:

```bash
node test-client.js bob "morning back"
```

Because both connect within a couple of seconds, each one's message reaches the other. You'll see something like this in terminal A:

```
[alice] connected
[alice] received: bob says: morning back
```

And in terminal B:

```
[bob] connected
[bob] received: alice says: good morning
```

Notice what *didn't* happen: alice never received "alice says: good morning" back. That's the `client !== sender` guard doing its job.

In the **server** terminal you'll see it relaying both:

```
A client connected.
A client connected.
Relaying: alice says: good morning
Relaying: bob says: morning back
```

## What you have now

A working relay. Whatever one client sends, every other client receives. That is, functionally, a chat server - the only thing missing is a pleasant way for humans to use it instead of throwaway scripts.

That's the next phase: a browser page with an input box and a message list, talking to this exact server over a WebSocket. The server you've got won't change at all - it already does the hard part.


---

# The Browser Client

You've been talking to your server with throwaway scripts. Time to give it a real face. This phase builds a single HTML page - input box, send button, scrolling message list - that connects to your server over a WebSocket. When you finish, you'll open this page in two browser tabs and chat between them.

The browser is the best WebSocket client there is, because it ships with one built in. No library, no install. You write `new WebSocket(...)` and it works.

Built on your machine. Make sure your server from phase 2 is running.

## Your turn: addMessage

Before the full page, here's one piece you can write yourself. The rest of this phase is new - `WebSocket`, `addEventListener`, `event.data` - but rendering a line of text into a list is plain DOM work you already know.

`addMessage` takes a string, builds an `<li>` holding that text, appends it to the `#messages` list, and scrolls the list so the newest line stays visible.

```javascript
function addMessage(text) {
  // your turn
}
```

`document.createElement`, `.textContent`, and `.appendChild` build and insert the line; setting `messages.scrollTop = messages.scrollHeight` is the trick that scrolls to the bottom. My version is in the full page below - once you've got it wired up and running, type a message and watch it land in the list to check.

## The page

Create a file called `index.html` in your project folder, next to `server.js`. It's all in one file - markup, style, and script - so there's nothing to wire together.

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Realtime Chat</title>
    <style>
      body {
        font-family: system-ui, sans-serif;
        max-width: 600px;
        margin: 2rem auto;
        padding: 0 1rem;
      }
      #messages {
        list-style: none;
        padding: 0;
        height: 320px;
        overflow-y: auto;
        border: 1px solid #ccc;
        border-radius: 8px;
        padding: 0.5rem;
      }
      #messages li {
        padding: 0.25rem 0;
        border-bottom: 1px solid #eee;
      }
      form {
        display: flex;
        gap: 0.5rem;
        margin-top: 0.5rem;
      }
      #input {
        flex: 1;
        padding: 0.5rem;
      }
      button {
        padding: 0.5rem 1rem;
      }
      #status {
        color: #888;
        font-size: 0.85rem;
      }
    </style>
  </head>
  <body>
    <h1>Realtime Chat</h1>
    <p id="status">Connecting…</p>
    <ul id="messages"></ul>
    <form id="form">
      <input id="input" autocomplete="off" placeholder="Type a message…" />
      <button type="submit">Send</button>
    </form>

    <script>
      const status = document.getElementById("status");
      const messages = document.getElementById("messages");
      const form = document.getElementById("form");
      const input = document.getElementById("input");

      const socket = new WebSocket("ws://localhost:8080");

      socket.addEventListener("open", () => {
        status.textContent = "Connected";
      });

      socket.addEventListener("close", () => {
        status.textContent = "Disconnected";
      });

      socket.addEventListener("message", (event) => {
        addMessage(event.data);
      });

      form.addEventListener("submit", (e) => {
        e.preventDefault();
        const text = input.value.trim();
        if (!text) return;
        socket.send(text);
        addMessage("You: " + text);
        input.value = "";
      });

      function addMessage(text) {
        const li = document.createElement("li");
        li.textContent = text;
        messages.appendChild(li);
        messages.scrollTop = messages.scrollHeight;
      }
    </script>
  </body>
</html>
```

It looks like a lot, but most of it is the markup and a little styling. The interesting part is the script, so let's read that.

## How the client works

`const socket = new WebSocket("ws://localhost:8080")` opens a connection to your server the moment the page loads. Same address your test clients used.

The browser's WebSocket uses `addEventListener` for events, mirroring the Node side:

- `"open"` fires once the connection is live. We flip the status text to "Connected" so you get visible proof.
- `"close"` fires if the connection drops - handy when you stop the server and want the page to admit it.
- `"message"` fires for every relayed message. `event.data` is the text the server sent, and we drop it into the list.

The form's `submit` handler is where you send. We `preventDefault()` so the page doesn't reload, grab the trimmed input, and bail out if it's empty. Then `socket.send(text)` ships it to the server.

Here's the piece that ties back to phase 2: right after sending, we also call `addMessage("You: " + text)` locally. Remember, the server broadcasts to everyone *except* the sender - so your own message never comes back to you. The client shows it itself, instantly, with no round trip. That's the convention we set up on purpose.

`addMessage` builds a list item, appends it, and nudges the scroll to the bottom so the newest line is always visible. Using `textContent` (not `innerHTML`) means a message containing something like `<script>` shows up as literal text instead of running - a small habit worth keeping.

## Open it

Here's a subtlety worth knowing. You can open `index.html` by double-clicking it (a `file://` URL) and it'll work, because the page connects to `ws://localhost:8080` regardless of how the page itself was loaded. The WebSocket address is hardcoded, so the page's origin doesn't matter for this.

So: with your server running, open `index.html` in your browser. The status should switch from "Connecting…" to "Connected" within a blink.

Now open the **same file in a second tab.** Two tabs, two clients, both connected to the one server.

Type a message in tab one and hit Send. It appears in tab one as "You: ..." immediately, and a moment later it appears in tab two as the relayed text. Reply from tab two. You're chatting - two browser tabs passing messages through a Node server you wrote.

## When it doesn't connect

If the status sticks on "Connecting…" or jumps to "Disconnected," run through this:

| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| Stuck on "Connecting…" | Server isn't running | Start it: `node server.js` |
| "Disconnected" right away | Wrong port or address | Confirm the page uses `ws://localhost:8080` and the server logs that port |
| Works in one tab, not the other | Second tab opened a stale page | Refresh the second tab |
| Console shows a connection error | Server crashed | Check the server terminal for an exception |

Open your browser's developer console (F12) when in doubt - WebSocket errors show up there with a clear message.

## What you have now

A real chat client. Type, send, read, all live, no refresh. It's anonymous, though - every message is only text with no idea who said it. In the next phase we fix that: each person picks a username when they join, and the room announces arrivals and departures.


---

# Usernames and Join/Leave

Anonymous text is hard to follow. "good morning" - from whom? This phase gives every message an owner. People pick a name when they join, that name rides along with everything they say, and the room announces when someone arrives or drops off.

To do this cleanly, we'll stop sending bare strings and start sending **structured messages** as JSON. That one change unlocks usernames, system notices, and - in the next phase - rooms.

Built on your machine. Server and `index.html` from before.

## From strings to JSON

So far a message is a string. But now a message needs more than text - it needs a type ("is this a chat line or a join announcement?") and a sender. A plain object handles that, and `JSON.stringify` turns it into a string the socket can carry.

We'll use a few message shapes:

| Type | Direction | Fields | Meaning |
|------|-----------|--------|---------|
| `join` | client → server | `name` | "I'm here, call me this" |
| `chat` | client → server | `text` | "broadcast this line" |
| `chat` | server → clients | `name`, `text` | "this person said this" |
| `system` | server → clients | `text` | "so-and-so joined/left" |

The trick that makes usernames work: the server remembers each socket's name. When phase 1 gave you a `socket` per connection, that object is yours to scribble on. We'll store the name right on it - `socket.username` - so the server always knows who owns which pipe.

## Update the server

Open `server.js`. We're replacing the message handling to parse JSON, track names, and send richer broadcasts:

```javascript
import { WebSocketServer } from "ws";

const PORT = 8080;
const wss = new WebSocketServer({ port: PORT });

console.log(`Chat server listening on ws://localhost:${PORT}`);

function broadcast(payload, sender) {
  const message = JSON.stringify(payload);
  for (const client of wss.clients) {
    const isOpen = client.readyState === client.OPEN;
    if (isOpen && client !== sender) {
      client.send(message);
    }
  }
}

wss.on("connection", (socket) => {
  socket.username = null;
  console.log("A client connected.");

  socket.on("message", (data) => {
    let msg;
    try {
      msg = JSON.parse(data.toString());
    } catch {
      return; // ignore anything that isn't valid JSON
    }

    if (msg.type === "join") {
      socket.username = String(msg.name || "anonymous").slice(0, 24);
      console.log(`${socket.username} joined.`);
      broadcast({ type: "system", text: `${socket.username} joined the chat` }, socket);
      return;
    }

    if (msg.type === "chat" && socket.username) {
      broadcast({ type: "chat", name: socket.username, text: String(msg.text) }, socket);
    }
  });

  socket.on("close", () => {
    if (socket.username) {
      console.log(`${socket.username} left.`);
      broadcast({ type: "system", text: `${socket.username} left the chat` }, socket);
    }
  });
});
```

What changed and why:

- **`broadcast` now takes an object** and stringifies it. Every client receives JSON now.
- **The `try/catch` around `JSON.parse`** matters. Anything can connect to a public socket and send garbage; if a non-JSON message arrives, parsing throws, and without the guard that exception crashes the handler. We catch it and ignore the message. This is a trust boundary - keep the guard.
- **`socket.username`** starts as `null` and gets set on `join`. We clamp the name to 24 characters with `.slice(0, 24)` so nobody sends a 10,000-character "name."
- **A `chat` message is only relayed if the socket has a name.** No name, no talking - that stops messages from people who never joined.
- **On `close`,** if the socket had a name, we announce the departure. If they never joined (closed before naming themselves), we stay quiet - there's nobody to say goodbye to.

## Update the client

Now the browser needs to ask for a name, send a `join`, and read the new JSON shapes. Open `index.html` and replace the `<script>` block with this:

```javascript
const status = document.getElementById("status");
const messages = document.getElementById("messages");
const form = document.getElementById("form");
const input = document.getElementById("input");

const username = (prompt("Pick a username:") || "anonymous").trim().slice(0, 24);

const socket = new WebSocket("ws://localhost:8080");

socket.addEventListener("open", () => {
  status.textContent = "Connected as " + username;
  socket.send(JSON.stringify({ type: "join", name: username }));
});

socket.addEventListener("close", () => {
  status.textContent = "Disconnected";
});

socket.addEventListener("message", (event) => {
  let msg;
  try {
    msg = JSON.parse(event.data);
  } catch {
    return;
  }
  if (msg.type === "chat") {
    addMessage(msg.name + ": " + msg.text);
  } else if (msg.type === "system") {
    addMessage(msg.text, true);
  }
});

form.addEventListener("submit", (e) => {
  e.preventDefault();
  const text = input.value.trim();
  if (!text) return;
  socket.send(JSON.stringify({ type: "chat", text }));
  addMessage("You: " + text);
  input.value = "";
});

function addMessage(text, isSystem) {
  const li = document.createElement("li");
  li.textContent = text;
  if (isSystem) li.style.color = "#888";
  messages.appendChild(li);
  messages.scrollTop = messages.scrollHeight;
}
```

The flow now:

- On load, the page asks for a name with `prompt()`. (It's the quickest input there is - you'd swap it for a proper form later.)
- When the socket opens, the client sends a `join` message with that name. This is why the server needs `join` separate from `chat`: the name has to register *before* any chatting.
- Incoming messages are parsed as JSON and routed by `type`. A `chat` becomes "name: text"; a `system` notice is shown in gray.
- Sending wraps the text in a `chat` object. Your own line still shows locally as "You: ...", same as before.

## Try it

Restart the server (Ctrl+C, then `node server.js` - it ingests no files, but you changed the code, so it needs a restart). Open `index.html` in two tabs.

Each tab prompts for a name. Call one "alice" and one "bob." As soon as bob joins, alice's window shows a gray line: **"bob joined the chat."** Now messages read "alice: hi" and "bob: hey" - you can tell who's talking. Close bob's tab and alice sees **"bob left the chat."**

## What you have now

A chat with identity. People have names, messages are attributed, and the room reacts when folks come and go - all carried over structured JSON instead of loose strings. That JSON foundation is exactly what we need for the last piece: rooms, so two separate conversations can run on the same server without mixing.


---

# Rooms, and Running It

One big room is fine until two conversations want to happen at once. The fix is **rooms** - named channels where a message only reaches the people in the same channel. This phase adds them, then we run the finished app start to finish and look at the real next steps if you want to keep going.

By the end you'll have a small but real chat server: named users, join and leave notices, and separate rooms, all running on your machine.

Built on your machine, last time.

## How rooms work

A room is a label. Every socket belongs to one room, and broadcasting goes to "everyone in *this* room" instead of "everyone." You already store state on the socket - `socket.username` - so you'll add one more field, `socket.room`, the same way.

The only real change is the broadcast: it now filters by room.

```mermaid
graph TD
  A[alice in 'general'] --> S[Server]
  B[bob in 'general'] --> S
  C[carol in 'random'] --> S
  S -- general only --> A
  S -- general only --> B
  S -- random only --> C
```

bob and alice hear each other. carol's in another room and hears neither.

## Update the server

Open `server.js`. The `join` message gains a `room`, and `broadcast` gains a room filter:

```javascript
import { WebSocketServer } from "ws";

const PORT = 8080;
const wss = new WebSocketServer({ port: PORT });

console.log(`Chat server listening on ws://localhost:${PORT}`);

function broadcast(payload, room, sender) {
  const message = JSON.stringify(payload);
  for (const client of wss.clients) {
    const isOpen = client.readyState === client.OPEN;
    if (isOpen && client !== sender && client.room === room) {
      client.send(message);
    }
  }
}

wss.on("connection", (socket) => {
  socket.username = null;
  socket.room = null;
  console.log("A client connected.");

  socket.on("message", (data) => {
    let msg;
    try {
      msg = JSON.parse(data.toString());
    } catch {
      return;
    }

    if (msg.type === "join") {
      socket.username = String(msg.name || "anonymous").slice(0, 24);
      socket.room = String(msg.room || "general").slice(0, 24);
      console.log(`${socket.username} joined room "${socket.room}".`);
      broadcast(
        { type: "system", text: `${socket.username} joined #${socket.room}` },
        socket.room,
        socket
      );
      return;
    }

    if (msg.type === "chat" && socket.username) {
      broadcast(
        { type: "chat", name: socket.username, text: String(msg.text) },
        socket.room,
        socket
      );
    }
  });

  socket.on("close", () => {
    if (socket.username) {
      console.log(`${socket.username} left room "${socket.room}".`);
      broadcast(
        { type: "system", text: `${socket.username} left #${socket.room}` },
        socket.room,
        socket
      );
    }
  });
});
```

The differences from phase 4:

- `socket.room` is set on join, defaulting to `"general"` and clamped to 24 characters, same treatment as the name.
- `broadcast` takes a `room` and adds `client.room === room` to its filter. That one clause is the entire room feature - a message only reaches sockets tagged with the matching room.
- Join and leave notices go to the room, not the whole server, so people in other rooms aren't bothered by your comings and goings.

This is the whole point of keeping state on the socket: rooms cost you one extra field and one extra condition.

## Update the client

The page needs to ask which room. Replace the two lines near the top of the `<script>` where the username and join are handled. First, the prompts:

```javascript
const username = (prompt("Pick a username:") || "anonymous").trim().slice(0, 24);
const room = (prompt("Which room? (e.g. general)") || "general").trim().slice(0, 24);
```

Then update the `open` handler to send the room and show it:

```javascript
socket.addEventListener("open", () => {
  status.textContent = `Connected as ${username} in #${room}`;
  socket.send(JSON.stringify({ type: "join", name: username, room }));
});
```

Everything else in the client stays the same - the message handling and form don't care about rooms, because the server already scoped the messages before they arrived.

## Run the whole thing

Here's the complete startup, from a clean terminal:

```bash
# Terminal 1 - the server
cd realtime-chat
node server.js
```

Leave that running. Then open `index.html` in your browser - double-click it, or open it however you like - and do it in **three tabs.**

- Tab 1: name "alice", room "general"
- Tab 2: name "bob", room "general"
- Tab 3: name "carol", room "random"

Now test the boundary. Type in alice's tab: bob sees it, carol does not. Type in carol's tab: nobody in general sees a thing. Each room is its own conversation on the same server. When bob joins, only alice gets the "bob joined #general" notice - carol's window stays quiet.

That's the finished app: a Node WebSocket server broadcasting attributed messages within rooms, and a browser client to use it. You built the whole pipe.

## Where to take it next

You've got the core. These are the real directions to extend it, roughly in the order most people want them:

| Want | What it takes |
|------|--------------|
| **History** | Right now a fresh tab sees nothing that happened before it joined. Keep the last N messages per room in an array on the server and send them to a client right after it joins. |
| **Persistence** | That history vanishes when the server restarts. Write messages to SQLite or a file so they survive a restart. |
| **A real name form** | `prompt()` is a placeholder. Swap it for an HTML form before connecting - nicer, and you can validate names. |
| **Authentication** | Anyone can claim any name. Add login (a token the client sends in the `join` message, checked by the server) so identities are real. |
| **Typing indicators** | Send a lightweight `typing` message type, broadcast to the room, and clear it after a short timeout. The message-type pattern you built handles this with no new plumbing. |
| **Deploying** | Run it on a host so others can connect. You'll serve the HTML over HTTP, put the WebSocket behind the same domain, and switch the client to `wss://` (secure WebSocket) since browsers block plain `ws://` from HTTPS pages. |

Each of those builds on what's here without rearchitecting. The message-type design from phase 4 is what makes adding features cheap - a new type, a new branch, done.

## What you built

A real-time chat server and client, from an empty folder: a server that accepts connections and broadcasts within rooms, a browser client that sends and renders live messages, named users, join and leave announcements, and channels. You wrote every line of the mechanism that makes chat feel instant - the open socket, the fan-out, the per-connection state.

That same shape - keep a connection open, push when something changes, scope it to who should see it - is behind live dashboards, multiplayer games, collaborative editors, and notifications. You now know how it actually works, because you built one.
