ICallOn v2
Part 1: UI refresh and technical enhancements
I released the first iteration of ICallOn in December 2024. It was a throwback to a game I and many others enjoyed playing as kids, and I thought it'd be fun to experience it digitally as a real-time multiplayer game.
In building it, I wasn’t trying to reinvent the game. As such, it was important for me to preserve its core parts which were the way the “caller” rotates each round, and the small power they hold to end a round whenever they choose.
At launch, ICallOn supported the following:
Creating and joining game sessions
Custom word categories
30/60/90 second timers
The core gameplay loop
Selecting a letter each round
Ending the round early as the caller
Grading another player’s answers at the end of each round
Lobby and gameplay notifications
Game results and game history
To pull off the real-time multiplayer, I leaned heavily on WebSockets. It’s a protocol I’d read about, but never properly used in a project before. The tech stack (and deployment) looked like this:
Next.js — UI & API (Vercel)
Postgres — Database (Supabase)
Soketi — WebSockets (DigitalOcean)
In terms of cost, it was refreshingly simple. I only had to pay for the Digital Ocean droplet and the domain (icallon.xyz).
A few months later in May 2025, I hosted a Twitter Space where we (my friends and I) played ICallOn live. The game supports up to 10 players per session, so I had what I thought was a brilliant idea: I’d build a spectator view. That way, people who weren’t playing could still watch the game unfold in real time.
When the space actually happened, we were fewer than 10 people. We didn’t need spectators at all, and the feature was altogether unnecessary. I still think it was a brilliant idea though xD.
After that, I mostly left ICallOn alone. Every now and then, I’d ship a small improvement or fix a bug, and the game worked fine.
Over time, however, the app started to feel like a bare minimum game, mostly because it relied heavily on card components and stacked layouts. For a while, I’d been thinking about a proper upgrade. I wanted a second iteration of ICallOn that would include:
A major UI/UX refresh
Technical enhancements
A Daily Challenge mode
I finally got around to implementing these in the last month, and that’s what makes up ICallOn v2, which you can play here: icallon.xyz.
UI/UX Refresh
ICallOn v1 used Mantine’s default components to a fault. Back then, the goal was to ship something that worked, so I reached for the quickest building blocks available: cards. I whipped out a card for virtually everything. A card for this, a card for that, a card for you, and a card for me.
For v2, the guiding philosophy was to replace the generic UI patterns with more game-native ones. Score cards became leaderboard rows with rank badges. Progress bars became little avatar grids. I added more depth with shadows, improved spacing, and sprinkled in a few animations so the app felt less like paperwork.
Some highlights:
Tabbed gameplay navigation: Instead of stacking Game, Players, Rounds, and Info into one long screen, I moved them into tabs. There’s less clutter, and players can jump to whatever they need in one click.
Custom score buttons: These went through several iterations. First it was a number input with increment(+) and decrement(-) controls. Then a Select with 0, 5, and 10. Now, I’ve made them buttons, and scoring is done with a click.
Better loading experiences: In flows like creating a game or starting one, players now see a loading animation instead of wondering whether the app froze.
Game results: Players always saw confetti when viewing Game Results and now, I’ve added a podium for the top 3 players in that game for a bit more drama.









I hadn’t originally planned to, but I also added some sound effects. You’ll hear them play on button clicks or after certain gameplay state changes (e.g. when a round ends or when time is almost up). You can toggle sound in the header, just beside the theme icon.
Technical Enhancements
One of the reasons I delayed working on v2 was that I wasn’t looking forward to cleaning up some of the bad code I’d written and correcting a few poor architecture decisions.
I group these issues into three buckets:
Ghost Events
God Classes
State Management
Ghost Events
In v1, the WebSocket events were emitted inside database transactions. When a game is started, for example, the database is updated. As part of that transaction, the game:started event is also emitted. This sounds fine, but if the transaction rolls back after the event has fired, players get notified even though the game state did not change.
To address this, I introduced the Event Outbox Pattern. Instead of emitting events inside transactions, I write them to an outbox table in the same transaction as the state change. If the transaction fails, no event record exists. If it succeeds, the event is guaranteed to be recorded. It is then emitted over the WebSocket.
God Classes
The dread I had for the v2 upgrade was mostly due to v1’s poor maintainability. On the backend, I had a GameService with ~2000 lines of code that handled everything: sessions, rounds, scoring, and event emission. God classes like this make debugging difficult. Reading the code feels like a chore, and you want to get out as soon as you’re in. It wasn’t code that was written by someone who cared.
In v2, the responsibilities of GameService were split into five focused services, the largest having just over 500 LOC. The original GameService became a thin facade (~200 loc) that delegates to these services.
Similarly, on the frontend, v1 had a GamePage component with several responsibilities, including WebSocket event handling, notifications, server actions, and rendering. I also decomposed it into more focused components, and the codebase looks much better.
State Management
In v1, I relied heavily on Next.js router.refresh() to retrieve the latest game state from the database whenever an event was consumed. I didn’t need to make this round trip. Silly me. The WebSocket events already contain the data the client needed to display, and I made the most of this in v2, reducing the round-trips to the server.
Additionally, state management on the client was a mess in v1. It was the classic case of prop drilling, which made the application buggy. Refetching data from the DB helped address these bugs, but it wasn’t the most efficient solution. In v2, I introduced a Zustand store for managing the game state. The server provides the initial state when the page is loaded, and after that, the client owns the state.
Wrapping Up
That was the bulk of the v2 upgrade, which wasn’t really about adding features. It was about making ICallOn feel like something you’d want to return to, and making the internals stable enough that I could build on top of them without fear.
In Part 2, I’ll talk about the new Daily Challenge mode, and the two questions it forced me to answer: how do you validate submissions without a human judge, and how do you score duplicates fairly when everyone is playing the same game?
In the meantime, you can check out the game here or tweet at icallon_xyz.
- Wolemercy
Thanks for reading! Hope you hang around for more Software Engineering posts.





