- take-home
- 2025
- WebRTC
- Solo
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.



- Type
- take-home
- Role
- Solo
- When
- Two days, Sep 2025
- Stack
- React, Socket.IO, WebRTC, MongoDB
- Links
- kernelchat.vercel.appCode
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.
How it worksA signalling server
One Socket.IO server relays offers, answers and candidates, and never touches the audio.
How it worksA handshake that can't race
Candidates that arrive early wait until the other side's description lands.
How it worksDelivered early
Built, styled and deployed in two days, twelve hours before the deadline.
How it fits together
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.
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 });});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.
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 descriptionconst processIceCandidateQueue = () => { while (iceCandidateQueue.length > 0) { const candidate = iceCandidateQueue.shift(); peerConnectionRef.current.addIceCandidate( new RTCIceCandidate(candidate) ); }};handshake via the server
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.
