DEV Community

Cover image for I built a real-time multiplayer brain-games platform on nothing but Firebase and vanilla JS
Nobin Debnath
Nobin Debnath

Posted on

I built a real-time multiplayer brain-games platform on nothing but Firebase and vanilla JS

I've been building Mognota — a free, real-time multiplayer hub of head-to-head brain games (chess variants, word games, mazes) — solo, with no backend game server. Just vanilla JS on the client and Firebase Realtime Database doing all the synchronization. Wanted to share what that architecture actually looks like in practice, because "no server" sounds simpler than it is.

What it is

No download, no forced signup — pick a name, click play, you're matched against a live opponent in seconds. About 15 game modes, each leaning on a different skill:

  • Chess variants — standard 1v1, plus a 2v2 mode with four kings on one board and voice chat between teammates
  • LexArena — build words from a shared hex letter-hive under a center-letter constraint, race on word count
  • Anagram Duel — unscramble words against a shared clock, skip the hard ones and come back
  • Knowledge Duel — hidden general-knowledge words in a word-search grid, 1v1 race
  • Maze Race — full-visibility and fog-of-war variants, pure spatial speed

The actual hard part: state without a server

With no authoritative game server, every "who won" decision is really "which of two clients wrote to the database last." That sounds like a minor implementation detail until you hit it in practice — I had bugs where a match-timeout timer on one client and a "found the last word" event on the other could both fire near-simultaneously, and a plain update() call meant whichever write landed last silently overwrote the correct result.

The fix that actually held up: every path that could plausibly declare a winner goes through a Firebase transaction on that single field, not a blind update. The transaction only commits if the winner field is still unset — so the first legitimate write wins, and every other path becomes a no-op instead of a race.

Disconnects were the other big one. A player force-quitting the tab or losing wifi mid-match shouldn't strand the other player forever. onDisconnect() handles the write-on-drop case, but it doesn't distinguish "cleanly closed" from "network died and never told the server" — for things like a live presence counter, that gap meant stale entries could accumulate over time if I didn't also handle staleness with heartbeats/timestamps, not just a single boolean flag.

Stack, for the curious

  • Vanilla JS, no framework, no build step — just static HTML/JS served from Firebase Hosting
  • Firebase Realtime Database for matchmaking, rooms, presence, friends, and a weekly league/XP system
  • Firebase Auth (anonymous guest accounts, upgradeable to Google sign-in without losing progress)
  • A couple of Cloudflare Workers for the handful of things that needed a real backend (avatar uploads, abuse reports)

One thing I learned the hard way: keep your database security rules in version control from day one. Mine lived only in the Firebase console for a while, unreviewed and un-diffed, which is a bad place for the one thing standing between your app and anyone with a REST client. Worth fixing before you have real users, not after.

Where it's at

Solo project, genuinely early. If you try it, I'd love to know: does the real-time-without-a-server thing actually feel responsive, or does the transaction-based conflict resolution ever feel laggy? And if anyone's dealt with the Firebase-rules-in-version-control problem more elegantly than "just remember to export them," I'm all ears.

https://mognota.com

Top comments (0)