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.
Phaser 3 runs on its own internal requestAnimationFrame canvas lifecycle, completely outside the React DOM tree.
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.
Instead of binding React state directly to the game tick, I established Zustandas 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.
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}
$transaction) with idempotency keys.Leaderboards in Space Cargo Runner support global rankings, daily tournament ladders, and friend feeds.
ZADD / ZREVRANGE) allowing $O(\log(N))$ score ingestion and rank queries.// 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 });
}
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.
To monitor live game telemetry and tune the economy without redeploying code, I built an integrated Mission Control admin panel accessible at /admin.
npm run make-admin <username>.What patterns do you prefer for synchronizing high-frequency canvas game loops with reactive frontends? Let's discuss in the comments below!