This guide walks through building a crash game frontend with Ember.js. We cover the architecture decisions, key components, real-time WebSocket integration, Canvas-based multiplier rendering, and client-side provably fair verification. This is a frontend tutorial — the crash point generation and game logic run server-side, which we cover architecturally but don’t implement here.
If you’re unfamiliar with how crash games work mechanically, start with our algorithm explainer and hash verification guide. This tutorial assumes you know Ember.js fundamentals (components, services, tracked properties) and basic WebSocket concepts.
What we’re building
A multiplayer crash game frontend with: real-time multiplier graph (Canvas), bet placement and auto-cashout, live player bet feed, round history, and client-side hash verification. The backend is a Node.js WebSocket server that handles crash point generation, bet processing, and round state.
Architecture Overview: What Runs Where
A crash game has a clear client-server split. Getting this wrong — specifically, putting game logic on the client — is the most common mistake in crash game development and the reason many early clones were exploitable.
Server-side (Node.js + WebSocket)
The server is responsible for everything that determines outcomes and money: generating hash chains (pre-committed crash points), broadcasting round state (betting → running → crashed), processing bets and cashouts with atomic balance updates, revealing hashes after each round, and managing player authentication and balances. The algorithm guide covers how hash chains and HMAC-SHA256 produce crash points. The server never reveals a hash before the round ends.
Client-side (Ember.js)
The Ember app handles presentation and user interaction: rendering the multiplier graph on Canvas, displaying bet controls and auto-cashout settings, showing the live player feed (who bet, who cashed out), managing local UI state (is the user currently in a round?), and verifying hashes after rounds complete. No game logic runs on the client. The client receives state updates via WebSocket and renders them.
Communication layer
WebSocket (not HTTP polling) is essential for crash games because the multiplier updates every ~100ms during a round. Socket.io or the native WebSocket API both work. The protocol typically includes these message types:
{ type: ‘ROUND_START’, roundId, hashCommitment }
{ type: ‘TICK’, multiplier: 1.43, elapsed: 2150 }
{ type: ‘PLAYER_BET’, username, amount }
{ type: ‘PLAYER_CASHOUT’, username, multiplier, profit }
{ type: ‘CRASH’, multiplier: 3.21, hash: ‘7f4d…’, serverSeed: ‘a3b2…’ }
{ type: ‘HISTORY’, rounds: […last20] }
// Client → Server messages
{ type: ‘PLACE_BET’, amount: 100, autoCashout: 2.0 }
{ type: ‘CASHOUT’ }
Ember Project Structure
A crash game fits naturally into Ember’s conventions. Here’s how the pieces map:
services/
websocket.js // WebSocket connection + message routing
game-state.js // Round state, bets, history (tracked)
player.js // Auth, balance, current bet
components/
crash-graph.js // Canvas multiplier rendering
bet-controls.js // Bet amount, auto-cashout, place/cashout buttons
player-list.js // Live feed of bets and cashouts
round-history.js // Previous round results (color-coded)
hash-verifier.js // Client-side provably fair check
utils/
crash-math.js // Hash→multiplier conversion, verification
graph-renderer.js // Canvas drawing logic (separated from component)
The two services (game-state and websocket) are the backbone. Ember services are singletons that persist across route transitions — perfect for maintaining a WebSocket connection and game state. Components consume service data via injection and re-render automatically when tracked properties change.
Key Component: The WebSocket Service
The WebSocket service manages the connection lifecycle and routes incoming messages to the game-state service. Here’s the structural pattern:
import Service, { inject as service } from ‘@ember/service’;
import { tracked } from ‘@glimmer/tracking’;
export default class WebsocketService extends Service {
@service gameState;
@tracked isConnected = false;
socket = null;
connect(url) {
this.socket = new WebSocket(url);
this.socket.onopen = () => this.isConnected = true;
this.socket.onclose = () => this._reconnect(url);
this.socket.onmessage = (e) => this._handleMessage(JSON.parse(e.data));
}
_handleMessage(msg) {
switch (msg.type) {
case ‘ROUND_START’: this.gameState.startRound(msg); break;
case ‘TICK’: this.gameState.updateMultiplier(msg.multiplier); break;
case ‘CRASH’: this.gameState.endRound(msg); break;
case ‘PLAYER_BET’: this.gameState.addPlayerBet(msg); break;
case ‘PLAYER_CASHOUT’: this.gameState.addPlayerCashout(msg); break;
}
}
send(msg) { this.socket?.send(JSON.stringify(msg)); }
_reconnect(url) { setTimeout(() => this.connect(url), 2000); }
}
The reconnection logic is critical — crash games have sessions lasting hours, and WebSocket connections drop frequently on mobile networks. Exponential backoff with jitter is better than a fixed 2-second retry in production, but the pattern above shows the structure.
Key Component: Game State Service
The game-state service is the single source of truth for everything the UI needs to render. Ember’s @tracked decorator ensures components re-render when state changes:
import Service from ‘@ember/service’;
import { tracked } from ‘@glimmer/tracking’;
import { TrackedArray } from ‘tracked-built-ins’;
export default class GameStateService extends Service {
@tracked phase = ‘betting’; // ‘betting’ | ‘running’ | ‘crashed’
@tracked currentMultiplier = 1.00;
@tracked crashPoint = null;
@tracked roundId = null;
@tracked hashCommitment = null;
@tracked revealedHash = null;
players = new TrackedArray([]);
history = new TrackedArray([]);
startRound({ roundId, hashCommitment }) {
this.phase = ‘running’;
this.currentMultiplier = 1.00;
this.crashPoint = null;
this.roundId = roundId;
this.hashCommitment = hashCommitment;
this.players.splice(0, this.players.length);
}
updateMultiplier(m) { this.currentMultiplier = m; }
endRound({ multiplier, hash }) {
this.phase = ‘crashed’;
this.crashPoint = multiplier;
this.revealedHash = hash;
this.history.unshift({ roundId: this.roundId, crashPoint: multiplier });
if (this.history.length > 20) this.history.pop();
}
}
Using TrackedArray from tracked-built-ins is important — native arrays don’t trigger Ember’s reactivity when mutated. This is a common gotcha for developers new to Ember Octane.
The Multiplier Graph: Canvas Rendering in Ember
The multiplier graph is the visual centerpiece of any crash game. It’s a curve that rises exponentially until the crash. Here’s how to integrate Canvas with an Ember component:
import Component from ‘@glimmer/component’;
import { inject as service } from ‘@ember/service’;
import { action } from ‘@ember/object’;
export default class CrashGraphComponent extends Component {
@service gameState;
canvas = null;
ctx = null;
animFrameId = null;
@action setupCanvas(element) {
this.canvas = element;
this.ctx = element.getContext(‘2d’);
this._startRenderLoop();
}
_startRenderLoop() {
const render = () => {
this._drawFrame();
this.animFrameId = requestAnimationFrame(render);
};
render();
}
_drawFrame() {
const { ctx, canvas } = this;
const { currentMultiplier, phase } = this.gameState;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw axes, grid, curve
this._drawGrid(ctx, canvas);
this._drawCurve(ctx, canvas, currentMultiplier);
this._drawMultiplierText(ctx, canvas, currentMultiplier, phase);
}
willDestroy() { cancelAnimationFrame(this.animFrameId); }
}
The template uses the {{did-insert}} modifier to pass the Canvas element to the component:
<canvas
{{did-insert this.setupCanvas}}
width=”800″ height=”400″
class=”crash-canvas”
></canvas>
Performance matters: the render loop runs at 60fps. Keep Canvas draw calls minimal — avoid creating new objects in the loop, cache gradients and fonts, and use ctx.save() / ctx.restore() sparingly. For the curve itself, an exponential function like y = height - (Math.log(multiplier) * scale) produces the characteristic crash game visual.
Bet Controls and State Machine
The bet control component manages a state machine with clear transitions:
| Current State | Event | Next State | Button Shows |
|---|---|---|---|
| IDLE | Click “Bet” | BET_PLACED | “Cancel Bet” |
| BET_PLACED | Round starts | IN_ROUND | “Cash Out (1.43x)” |
| IN_ROUND | Click “Cash Out” | CASHED_OUT | “Won 1.43x” (disabled) |
| IN_ROUND | Crash | LOST | “Busted” (disabled) |
| CASHED_OUT / LOST | New round begins | IDLE | “Place Bet” |
The cashout button during IN_ROUND should display the live multiplier and update in real-time. This is where Ember’s reactivity shines — the button text can bind directly to this.gameState.currentMultiplier and update automatically on every tick.
Auto-cashout is implemented by watching multiplier updates in the game-state service and sending a CASHOUT message when the target is reached. This should happen server-side too (the client sends the auto-cashout target with the bet, and the server enforces it). Never trust the client for cashout timing — network latency means the client’s multiplier display is always slightly behind the server’s true state.
Client-Side Hash Verification
After each round, the server reveals the game hash. The Ember client can verify it using the Web Crypto API:
export async function sha256(message) {
const encoder = new TextEncoder();
const data = encoder.encode(message);
const hashBuffer = await crypto.subtle.digest(‘SHA-256’, data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, ‘0’)).join(”);
}
export async function verifyHashChain(currentHash, previousHash) {
const computed = await sha256(currentHash);
return computed === previousHash;
}
export function hashToCrashPoint(hash, houseEdgePct) {
// Extract 52 bits (13 hex chars) from hash
const r = parseInt(hash.slice(0, 13), 16);
const e = Math.pow(2, 52);
const edge = (100 – houseEdgePct);
return Math.max(1, Math.floor((edge * e – r) / (e – r)) / 100);
}
The hash-verifier component lets users paste a game hash and see the expected crash point, or verify the chain integrity for any past round. This is the same logic described in our hash verification guide, just implemented in JavaScript within the Ember application.
Common Pitfalls in Crash Game Development
Trusting the client for anything financial. The client should never determine whether a cashout is valid. Even if the client displays 2.15x when the user clicks “Cash Out,” the server must check the actual multiplier at the time the message arrives. Network latency means these values diverge.
Race conditions in bet placement. Players can try to place bets after the round has started (the “round_start” message hasn’t reached them yet). The server must reject bets placed after the betting window closes. The Ember client should optimistically disable the bet button at round start but cannot enforce this alone.
Memory leaks in Canvas rendering. Always cancel requestAnimationFrame in willDestroy(). If the component is destroyed while the loop is running (e.g., the user navigates away), the callback will try to access destroyed objects. This is a common Ember lifecycle issue with Canvas.
Overloading Ember’s reactivity. Updating a @tracked property 10 times per second (multiplier ticks) causes Ember to re-render all consuming components on every tick. For the multiplier text display, this is fine. For the Canvas graph, avoid binding Canvas drawing to tracked properties directly — use the requestAnimationFrame loop to read the value instead of reacting to it.
Not handling disconnections gracefully. If the WebSocket drops mid-round and the player has an active bet, what happens? The server should auto-cashout at a configurable target (or let the round play out). The client should show a clear “reconnecting” state, and on reconnect, request the current round state from the server.
Framework Comparison for Crash Game Frontends
| Factor | Ember.js | React | Vue | Vanilla JS |
|---|---|---|---|---|
| State management | Built-in (services) | External (Redux/Zustand) | Built-in (Pinia) | Manual |
| Real-time rendering | Good (tracked + rAF) | Good (refs + rAF) | Good (refs + rAF) | Best (no overhead) |
| Async complexity | Ember Concurrency | useEffect cleanup | Composition API | Manual |
| Crash game examples | Few | Many | Some | Many |
| Bundle size | ~130KB (gzipped) | ~45KB (gzipped) | ~33KB (gzipped) | 0KB |
| Convention strength | Very strong | Flexible | Moderate | None |
Ember’s strength for crash games is its opinionated structure: services for state, components for UI, and clear lifecycle hooks for cleanup. Its weakness is the smaller ecosystem — fewer crash-game-specific tutorials and libraries compared to React. For teams already using Ember (or building the crash game as part of a larger Ember application), it’s a strong choice. For greenfield crash game projects, React or Vue may offer faster time-to-prototype due to more community examples.
Scaling Considerations
A single crash game instance typically handles 100-500 concurrent players. Beyond that, you’ll need to consider: WebSocket connection limits per server (typically 10,000-65,000), horizontal scaling with a message broker (Redis Pub/Sub) between server instances, and rate-limiting WebSocket messages from clients (prevent message flooding). On the Ember side, the main performance concern is Canvas rendering with many players in the bet feed. Virtual scrolling or limiting the visible player list to 50-100 entries keeps frame rates smooth.
For a deeper look at the server-side architecture, including hash chain generation and RTP configuration, see our crash game software guide. For the mathematical foundations that the server implements, see the algorithm explainer.
⚠️ Legal and ethical note: Building a crash game involves real-money gambling, which is regulated in most jurisdictions. Before deploying, ensure compliance with local gambling laws, obtain necessary licenses, implement responsible gambling features (deposit limits, self-exclusion, session timers), and have your provably fair implementation independently audited. The code patterns in this tutorial are for educational purposes. If you’re operating commercially, consult legal counsel and consider established crash game providers whose software is already licensed and audited.
Related Guides
- Crash Game Algorithm Explained — the server-side math this frontend renders
- Hash Verification Guide — how the client-side verifier works
- Crash Game Software — commercial providers and white-label solutions
- Crash Game Odds — the probability math behind multipliers
- Bustabit Review — open-source crash game with published code
- Provably Fair Explained — cryptographic foundations for game fairness
- Crash Game RTP Comparison — how house edge is configured

