Real-time chat

Build a tiny chat: every message a client sends is broadcast to everyone connected. Native WebSockets, no dependencies, the same on Node and Bun.

1. The socket handlers

Handle the three socket events with .socket(event, fn). ctx.socket is the current connection, ctx.sockets is every open one, and on a message ctx.body is what the client sent:

import server, { file } from '@server/next';

export default server()
  .get('/', () => file('./index.html'))
  .socket('open', (ctx) => {
    ctx.socket.send('Welcome!');
  })
  .socket('message', (ctx) => {
    // broadcast to everyone, including the sender
    for (const socket of ctx.sockets) {
      socket.send(ctx.body);
    }
  });

2. The client

Connect with the browser's standard WebSocket, no client library needed:

<!-- index.html -->
<input id="msg" autofocus />
<ul id="log"></ul>
<script>
  const ws = new WebSocket(`ws://${location.host}`);
  ws.onmessage = (e) => (log.innerHTML += `<li>${e.data}</li>`);
  msg.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') {
      ws.send(msg.value);
      msg.value = '';
    }
  });
</script>

Open two tabs and type: each message shows up in both.

3. Only signed-in users (optional)

With auth configured, the connecting user is on ctx.user in socket handlers too. Close unauthenticated connections yourself:

  .socket('open', (ctx) => {
    if (!ctx.user) return ctx.socket.close();
    ctx.socket.send(`Welcome ${ctx.user.name}`);
  });

Next steps

  • Track rooms yourself (a Map of room → sockets) to scope broadcasts.
  • Long-lived sockets run on Node and Bun, not on the edge (Cloudflare, Netlify).