# Kernel

> Anonymous chat rooms with peer-to-peer voice, where the server only relays the handshake. The take-home for the role I have now.

- Kind: take-home
- Role: Solo
- When: Two days, Sep 2025
- Stack: React, Socket.IO, WebRTC, MongoDB
- Live: https://kernelchat.vercel.app
- kernelchat.vercel.app: https://kernelchat.vercel.app
- Code: https://github.com/rajveeerr/Kernel

Kernel was the take-home for the role I have now. The brief was anonymous, room-based chat; I added peer-to-peer voice calls and delivered it twelve hours before the deadline, built, styled and deployed.

The server's job is to introduce two browsers, then get out of the way. One Socket.IO server handles chat and signalling; for a call it only relays offers, answers and network candidates between two people, and never sees the audio.

## What I built

- **Anonymous rooms.** Room-based chat with the history kept per room, and no accounts at all.
- **Peer-to-peer voice.** Calls I added beyond the brief, with the audio going straight from browser to browser.
- **A signalling server.** One Socket.IO server relays offers, answers and candidates, and never touches the audio.
- **A handshake that can't race.** Candidates that arrive early wait until the other side's description lands.
- **Delivered early.** Built, styled and deployed in two days, twelve hours before the deadline.

## Only the handshake

For a call the server does three things, and each is a relay: the offer goes to the callee, the answer comes back, and network candidates pass between them.

```js
// server/src/index.js
socket.on("call-user", (payload) => {
  const { to, from, offer } = payload;
  io.to(to).emit("call-made", { offer, from });
});

socket.on("make-answer", (data) => {
  const { to, answer } = data;
  activeCalls[socket.roomId] = true;
  io.in(socket.roomId).emit('call_in_progress');
  io.to(to).emit("answer-made", { answer });
});

socket.on("ice-candidate", (data) => {
  const { to, candidate } = data;
  socket.to(to).emit("ice-candidate", { candidate });
});
```

Nothing here reads a stream. Once the peers connect, the server is out of the call.

## Candidates that arrive early

A network candidate can arrive before the other side's description is set, and adding it then fails. So early ones wait in a queue and are applied once the answer lands, and a fast network never races the handshake.

```js
// client/src/pages/ChatRoom.jsx
const iceCandidateQueue = [];

const iceCandidateListener = (data) => {
  if (peerConnectionRef.current) {
    if (
      peerConnectionRef.current.remoteDescription &&
      peerConnectionRef.current.remoteDescription.type
    ) {
      peerConnectionRef.current.addIceCandidate(
        new RTCIceCandidate(data.candidate)
      );
    } else {
      iceCandidateQueue.push(data.candidate);
    }
  }
};

// Process queued ICE candidates after setting the
// remote description
const processIceCandidateQueue = () => {
  while (iceCandidateQueue.length > 0) {
    const candidate = iceCandidateQueue.shift();
    peerConnectionRef.current.addIceCandidate(
      new RTCIceCandidate(candidate)
    );
  }
};
```

Queued, then drained in order the moment the remote description is set.

## What I'd change

It's one-to-one by design. At IABTM I later moved group audio to an SFU, for exactly the reason a mesh doesn't scale.
