DEV Community

Faizullah
Faizullah

Posted on

How I Built a Real-Time Multiplayer Game with Socket.IO, Firebase & Pakistani Payment Gateways

How I Built a Real-Time Multiplayer Game with Socket.IO, Firebase & Pakistani Payment Gateways

By Faiz Ullah — Full-Stack Developer & Founder of DG Technology


When people think of multiplayer games, they imagine big studios and huge teams. But a real-time, money-handling, cheat-proof multiplayer platform can be built by one engineer who understands the architecture deeply. This is the story of Ludo Battle — a real-time multiplayer Ludo tournament platform I built end-to-end, from the WebSocket game engine to the Android APK to the payment integration.

I'll walk through the hard parts, the decisions that mattered, and the lessons that apply to any real-time application — not just games.


The Challenge

Build a platform where:

  • 2 to 4 players play live Ludo matches with sub-second synchronization
  • Players deposit and withdraw real money through local payment gateways
  • Nobody can cheat — not the dice, not the moves, not the outcomes
  • It runs on the web and as a native Android app
  • It works on Pakistani mobile networks, where strict NATs and unstable connections are the norm

That last constraint shaped a lot of decisions. Building for ideal conditions is easy. Building for real-world 4G in Pakistan is the actual engineering.


Architecture Decision #1: The Server Owns Everything

The single most important principle in any real-money game: the client is a renderer, never a decision-maker.

If the browser decides what the dice rolled, a cheater opens DevTools and rolls a six every time. So in Ludo Battle, the server is the source of truth for every piece of game state. The client sends intentions ("I want to roll", "I want to move this token"), and the server decides what actually happens.

Here's the dice roll — it lives on the server and uses Node's cryptographically secure random generator, not Math.random():

import { randomInt } from 'crypto';

function rollDice() {
  return randomInt(1, 7);  // 1–6, cryptographically secure
}
Enter fullscreen mode Exit fullscreen mode

Math.random() is predictable and can be exploited. crypto.randomInt cannot. For real money, this difference matters.

Every move is validated the same way: when a player sends game:move, the server computes the legal moves from the current board state and rejects anything illegal before applying it. The client literally cannot make an illegal or impossible move stick.


Architecture Decision #2: A Clean Real-Time Event Model

The entire live experience runs over Socket.IO. I designed the events around clear namespaces so the codebase stays readable as it grows:

room:create   room:join   room:leave   room:spectate
game:roll     game:move
chat:send     chat:msg
voice:join    voice:signal   voice:leave
Enter fullscreen mode Exit fullscreen mode

Rooms map directly to Socket.IO rooms, so broadcasting game state or chat to exactly the right players is a one-liner:

io.to(`room:${roomId}`).emit('chat:msg', message);
Enter fullscreen mode Exit fullscreen mode

This structure means game logic, chat, and voice are cleanly separated but share the same connection — no extra sockets, no wasted overhead.


Architecture Decision #3: Voice Chat That Survives Real Networks

I wanted players to talk during games. The naive approach — routing audio through the server — is expensive and laggy. Instead I used peer-to-peer WebRTC, where the audio flows directly between players and the server only helps them find each other (signaling).

The catch: most Pakistani mobile users are behind strict NATs that block direct P2P connections. The fix is a TURN server that relays audio when a direct connection isn't possible. The app fetches TURN credentials per session and falls back gracefully:

  1. Try direct connection (STUN)
  2. If blocked → relay through TURN
  3. Either way → players can talk

This is the difference between voice chat that "works on my machine" and voice chat that works for a farmer on a 4G connection in South Punjab.


Architecture Decision #4: Payments Without the Liability

Handling money means handling risk. I deliberately chose a hosted-redirect payment flow for JazzCash and EasyPaisa so that no card or wallet credentials ever touch my server.

The flow:

  1. User taps "Deposit"
  2. My backend builds a signed payload and returns it
  3. The user is redirected to JazzCash/EasyPaisa's own secure page to approve
  4. The gateway sends a callback to my server
  5. My server verifies the HMAC signature before crediting a single rupee

That HMAC verification step is critical — it's how the server knows a callback genuinely came from the gateway and wasn't forged by someone trying to fake a deposit. Skipping it is how platforms get drained.

I also built a test mode: when no gateway credentials are configured, deposits credit instantly. This let me build and test the entire economy locally without touching real money or waiting on merchant approval.


Architecture Decision #5: One Codebase, Two Platforms

The frontend is React + TypeScript + Vite. To ship it as a native Android app without rewriting everything, I used Capacitor, which wraps the web build into a real APK that I can distribute directly — no Play Store gatekeeping required.

One important real-world gotcha: Android blocks insecure WebSocket connections. The app must talk to an HTTPS backend or the sockets silently fail in the APK. Discovering and solving that kind of platform-specific issue is where real shipping experience comes from.


What I Learned

  • Trust nothing from the client. This one principle prevents an entire category of exploits.
  • Design for the worst network, not the best. Building for Pakistani 4G made the app rock-solid everywhere.
  • Security isn't a feature you add latercrypto.randomInt, HMAC verification, and server-authoritative logic had to be in the foundation.
  • Separation of concerns scales. Clean event namespaces and layered logic kept a complex real-time system maintainable.

The Stack, Summarized

Layer Technology
Frontend React, TypeScript, Vite, Tailwind, Zustand
Backend Node.js, Express, Socket.IO
Auth Firebase Phone OTP + JWT
Payments JazzCash, EasyPaisa (HMAC-verified)
Voice WebRTC + TURN
Mobile Capacitor → Android APK

Closing Thoughts

Ludo Battle taught me that the gap between a "demo" and a "product" is almost entirely in the parts users never see: the anti-cheat, the payment verification, the network resilience. Anyone can render a game board. Making it fair, secure, and reliable on real-world networks is the actual work.

If you're building something real-time, money-handling, or mobile — or you just want to talk architecture — I'd love to connect.

Faiz Ullah
Full-Stack Developer · Cybersecurity Engineer · Founder of DG Technology
🌐 faizullah.pk · 📧 work@faizullah.pk · 💻 github.com/faizullahpk


If you found this useful, follow me here and on GitHub — I write about real-world full-stack engineering, building for emerging markets, and shipping products solo.

Top comments (1)

Collapse
 
hayrullahkar profile image
Hayrullah Kar

The client is a renderer, never a decision-maker' is the golden rule of real-time engineering. Shifting the dice logic to crypto.randomInt and enforcing HMAC-verified hosted redirects for local wallets like JazzCash shows a rock-solid security mindset. Building for real-world 4G networks is where true engineering shines. Stellar work!