Yasin Hassim
All posts
· 6 min read

Kuiz Interaktif: A Kahoot-Style Quiz Game With No Backend

How I built a multiplayer quiz for workshops where the host's browser tab is the game server, and phones connect to it directly over WebRTC.

DevelopmentPersonal Projects

Why I Built This

The starting point was a workshop: the e-NORES session in Kota Kinabalu with Jabatan Tenaga Kerja Sabah, covering the licence to employ non-resident workers. Sessions like that usually open with an ice-breaker and close with a short knowledge check, and the format everyone knows is Kahoot. The host puts questions on the projector, everyone scans a QR code, and they answer on their phones against a timer.

Kahoot would have done the job. Honestly, this was a side project: I wanted to see whether I could build the same experience with no backend at all. No server, no database, no accounts. Just static files on Vercel, and nothing I have to keep running.

The result is Kuiz Interaktif. The whole interface is in Bahasa Melayu, because that’s who it’s for.

Kuiz Interaktif host screen: a QR code and room code 6742 on the left, quiz settings and three joined players on the right
The host screen on the projector: scan the QR code or type the room code. Three players are already in.

How It Works Without a Server

The trick is that the host’s browser tab is the game server.

  • The host opens #/host on the laptop connected to the projector. It shows a 4-digit room code and a QR code.
  • Players scan the QR code, which opens #/join/<code> on their phone, and type a name.
  • Each phone opens a direct WebRTC data channel to the host tab using PeerJS. The public 0.peerjs.com broker is only used so the browsers can find each other. After that, game traffic goes peer to peer.
  • The host tab holds everything: the questions, the timer, every player’s answers, and the scores. Phones only ever receive a snapshot of the state they’re allowed to see.
PeerJS broker 0.peerjs.com · introductions only Host tab (#/host) the laptop on the projector • questions and correct answers • the timer (pings every 3 s) • every player's answer • scores, saved to localStorage Phone · Aisyah #/join/6742 Phone · Daniel #/join/6742 Phone · Mei Ling #/join/6742 WebRTC data channel Phones get a snapshot of what they may see. The answer stays on the host until the reveal.
No server in the middle: the broker only introduces the browsers, then the host tab runs the game.

That last point matters more than it sounds. The correct answer is never sent to phones until the host reveals it. For typed answers, the list of accepted answers never leaves the host at all. Opening dev tools on your phone doesn’t help you cheat.

Phone join form with room code 6742 and the name Aisyah
Join with the code
Phone answer screen with four coloured choices and a countdown
Pick an answer
Phone result screen saying Betul with 908 points
Find out after the reveal

It’s built with Vue 3 and Vite. The full set of dependencies is PeerJS, a QR code generator, and canvas-confetti for the final results screen.

The Parts That Took Real Thought

Surviving a refresh. If the host’s tab dies, the game dies, so the README literally says “don’t close the host tab.” But a refresh shouldn’t be fatal. The host saves the whole game state to localStorage after every change and resumes it if the tab comes back within three hours. Each player gets a random ID stored in sessionStorage, so when their phone reconnects, the host matches them back to their existing name and score instead of creating a duplicate.

Reclaiming the room code. After a refresh, the host tries to re-register the same peer ID, but the PeerJS broker often still thinks the old tab owns it. If the game hasn’t started and nobody has joined, the host just picks a new code. If players are already in, it keeps retrying the same code for about 30 seconds, so their phones can find it again.

A bug I’m glad I caught early. My first version of that retry logic had every event handler refer to “the current peer.” After a retry, late events from the old, destroyed peer would fire and act on the new one, tearing down a perfectly healthy connection. The fix was to have each handler capture its own peer instance and ignore events from any peer that’s no longer the active one. It’s a small change, but it’s the kind of bug that would only show up on bad Wi-Fi, which is exactly what a conference room has.

Knowing the host is still there. The host pings every phone every 3 seconds with the remaining time. If a phone hears nothing for 10 seconds, it assumes the connection is dead and reconnects. The ping also keeps everyone’s countdown in sync with the host’s clock instead of trusting each phone’s own timer.

Scoring. A correct answer is worth 500 points plus up to 500 more for speed, scaled by how much time was left. The host can also manually mark a typed answer right or wrong, for when someone writes something reasonable that the matcher didn’t expect.

Host screen after the reveal: the answer Rumah kampung highlighted, vote counts per choice, and the scoreboard
After the reveal: votes per choice and the scoreboard. Both correct players scored, but not equally: the faster answer earns more.

Typed answers. The ice-breaker quiz supports a “type your answer” mode as well as multiple choice. Answers go through a forgiving matcher: it lowercases, strips accents and punctuation, and allows one or two typos depending on word length, so “kucng” still counts as “kucing” and “durain” counts as “durian”.

My first version of the matcher was a bit too forgiving. It also accepted an answer if it merely contained a correct word anywhere, which meant typing kucing basikal bot rumah would score on any question whose answer was one of those, and bukan kucing (“not a cat”) counted as “cat”. Now the whole answer has to be within typo distance of an accepted answer. While fixing that, I also found that swapped letters (“durain”) counted as two typos rather than one, so a 6-letter word was rejected despite being the exact example in my own code comment. Both are fixed.

Two Quizzes, Easy to Add More

There are currently two quizzes:

  • Teka Imej Dalam Perkataan: 10 picture questions where you spot the image hidden in the letters of a word. Choices are shuffled per game.
  • Kuiz e-NORES: 5 questions on the non-resident worker licence under the Sabah Labour Ordinance, with an explanation shown after each answer.

Each quiz is just a JavaScript file with a list of questions, so adding one means copying an existing file and registering it. The results screen breaks down every question: what percentage got it right, the most common wrong answer, and the average time for correct answers. That turns out to be more useful to a trainer than the leaderboard, because it shows exactly which point the room didn’t understand.

Final results Analisis Soalan tab: 67 percent overall accuracy and a per-question bar of correct and wrong answers
The question analysis tab. With ten questions, this is where the hardest one stands out.

The Honest Limitations

  • WebRTC doesn’t get through every network. Strict corporate firewalls can block it. The workaround in the guide is to ask players to switch to mobile data.
  • It’s sized for one room. One laptop acting as the server for every phone is designed for around 30–50 players. I wouldn’t try 500.
  • Analytics can’t tell host from player. I added Cloudflare Web Analytics, but because the app uses hash routes (#/host, #/join), every visit counts as /.

Where It Stands

The first working version came together in one day, October 14, pair-programming with Claude. That included the game itself, the quiz picker, fullscreen mode and the in-app guide. The next two days went to fixes and polish: the peer-retry bug above, and a review screen so players can go back through every question and their own answers after the game.

It hasn’t been run with a real audience yet, so the 30–50 player figure and the reconnect logic are still untested under load. That’s the real test I’m waiting for.

Next up: a fix for a rare case where a phone can get stuck on “answer sent” if the connection drops at exactly the wrong moment.

You can try it at kuiz-interaktif.vercel.app. Open it on a laptop as the host, then join from your phone.

Keep reading