If your game client tells your backend what score the player achieved, your game is already hacked.
When building Space Cargo Runner — a cyberpunk-themed, real-time arcade runner bridging traditional browser gaming with Web3 token economies — the biggest engineering challenge wasn't rendering 60 FPS in the browser.
It was building a tamper-proof, server-authoritative backend architecture that could synchronize live multiplayer state without dropping a single frame.
Here is an under-the-hood deep dive into the system design, anti-cheat mechanisms, and state bridging architecture.
1. Decoupled 60 FPS Game Loop with Zustand
Phaser 3 runs on its own internal requestAnimationFrame canvas lifecycle, completely outside the React DOM tree.
The Problem
If you attempt to sync game state (coordinates, current speed, fuel levels, shield integrity) by triggering standard React useState hooks on every frame (60–120 times per second), React’s reconciliation engine creates catastrophic CPU bottlenecks and dropped frames.
The Solution: Zustand as a Decoupled State Mediator
Instead of binding React state directly to the game tick, I established Zustand as a decoupled state bridge:
// packages/shared/src/store/gameBridge.ts
import { create } from 'zustand';
interface TelemetryState {
fuel: number;
health: number;
cargoCount: number;
multiplier: number;
updateTelemetry: (data: Partial<TelemetryState>) => void;
}
export const useGameBridge = create<TelemetryState>((set) => ({
fuel: 100,
health: 100,
cargoCount: 0,
multiplier: 1.0,
updateTelemetry: (data) => set((state) => ({ ...state, ...data })),
}));
In Phaser's update() loop:
// Inside Phaser Scene update loop
this.gameBridge.updateTelemetry({
fuel: this.player.fuel,
health: this.player.health,
cargoCount: this.sessionCargo,
});
React UI HUD overlays subscribe selectively only to the fields they render, allowing the Canvas to run at a buttery 60 FPS with zero DOM jank.
2. Server-Authoritative Anti-Cheat & Score Verification
In single-player web games, malicious players can open Chrome DevTools, inspect memory, modify local variables, or dispatch forged HTTP POST requests (POST /api/score { score: 9999999 }).
When real token rewards and cryptocurrency withdrawals are at stake on the SecureChain (SCAI) network, client trust is fatal.
+----------------+ 1. Timestamped Inputs & Seed +---------------------+
| Phaser Client | -----------------------------------------> | Node.js Express API |
+----------------+ +---------------------+
| |
| 2. Gameplay Loop | 3. Replay Engine
v v
[Local Visuals] [Deterministic Check]
|
v
{Score Valid? Yes/No}
The Validation Engine
- Input Vector Recording: The client streams a compressed array of timestamped player actions (directional shifts, boost activations, power-up triggers) along with the initial PRNG seed.
- Deterministic Server Replay: Upon run completion, the Node.js backend simulates the procedural obstacle and cargo positions generated by the seed and verifies that the claimed cargo count and survival time match the mathematical bounds.
-
Atomic Ledger Posting: Validated credits are credited to the player's account using Prisma interactive transactions (
$transaction) with idempotency keys.
3. Low-Latency Leaderboard Fan-Out (<100ms)
Leaderboards in Space Cargo Runner support global rankings, daily tournament ladders, and friend feeds.
-
Storage Layer: Backed by Redis Sorted Sets (
ZADD/ZREVRANGE) allowing $O(\log(N))$ score ingestion and rank queries. - Real-Time Distribution: When a validated high score is posted, the backend broadcasts WebSocket updates via Socket.io rooms with sub-100ms fan-out latency across active game sessions.
// Socket.io Leaderboard Ingestion & Fanout
export async function handleScoreSubmission(io: Server, socket: Socket, payload: RunSubmission) {
const isValid = await verifyRunSimulation(payload);
if (!isValid) {
socket.emit('error', { message: 'Anti-cheat flag: Invalid run telemetry.' });
return;
}
// Atomic Redis Rank Update
await redis.zadd('leaderboard:global', payload.finalScore, payload.username);
const topTen = await redis.zrevrange('leaderboard:global', 0, 9, 'WITHSCORES');
// Broadcast to all active pilots
io.emit('leaderboard:update', { topTen });
}
4. End-to-End Type Safety in an npm Monorepo
The codebase is organized as an npm workspaces monorepo:
space-cargo-runner/
├── apps/
│ ├── frontend/ # Vite + React 18 + Phaser 3 + Tailwind CSS
│ └── backend/ # Node.js + Express + Socket.io + Prisma ORM
├── packages/
│ ├── shared/ # Shared TypeScript interfaces & Socket payloads
│ └── contracts/ # Solidity Smart Contracts (Hardhat + OpenZeppelin)
└── docs/ # System architecture & technical specs
By maintaining all API contract interfaces in packages/shared, any schema change in backend endpoints immediately causes TypeScript compile-time errors in the frontend if contracts drift.
5. Role-Gated "Mission Control" Admin Panel
To monitor live game telemetry and tune the economy without redeploying code, I built an integrated Mission Control admin panel accessible at /admin.
- Access Security: Protected by database-backed Role-Based Access Control (RBAC).
-
CLI Provisioning: Admins are designated via backend script:
npm run make-admin <username>. -
Live Controls:
- Ban bad actors / sybil accounts.
- Inspect live game sessions and WebSocket client connections.
- Tune drop rates, fuel burn multipliers, and token withdrawal thresholds in real time.
Key Takeaways
- Never trust the client: Send inputs and seeds, not scores.
- Decouple Canvas loops from DOM frameworks: Zustand or external event buses keep 60 FPS silky smooth.
- Use Monorepos for Fullstack Games: Shared types between client, server, and smart contracts eliminate runtime contract mismatch bugs.
Links & Resources
- Play Space Cargo Runner Live: https://krrish41.github.io/space-cargo-runner/
- Explore the GitHub Repository: https://github.com/Krrish41/space-cargo-runner
What patterns do you prefer for synchronizing high-frequency canvas game loops with reactive frontends? Let's discuss in the comments below!
Top comments (0)