Skip to content
Back to skills

Game Backend Architecture

ASecurity

Use when building game servers, WebSocket connections, room management, game tick loops, or server-authoritative architecture with Elysia. Triggers: game server, WebSocket, rooms, tick loop, server architecture, multiplayer backend.

  • 4 stars
  • 0 votes
  • 0 copies
  • 6 views
  • Added September 12, 2026
businesstypescriptrustbashdatabasefrontendbackenddocumentation

Works with

  • cli

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned September 12, 2026

npx -y skills add aibot88/sec_skill_store --skill game-backend-architecture --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Game Backend Architecture?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Game Backend Architecture
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/aibot88-game-backend-architecture/badge)](https://www.skillsdirectory.com/skills/aibot88-game-backend-architecture)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: engineering-game-backend-architecture
description: >-
  Use when building game servers, WebSocket connections, room management, game tick loops, or server-authoritative architecture with Elysia. Triggers: game server, WebSocket, rooms, tick loop, server architecture, multiplayer backend.
---

# Game Backend Architecture

## Purpose

WebSocket + REST patterns, room/session management, game tick loops, genre-agnostic server design using Elysia + Bun. Covers server-authoritative architecture, fixed-timestep simulation, room lifecycle, typed message protocols, and horizontal scaling via Redis pub/sub.

## When to Use

Trigger: game server, backend architecture, WebSocket, room management, game loop, tick system, server-authoritative, real-time, Elysia game server, multiplayer backend, session management, game tick, fixed timestep server

## Prerequisites

- `postgres-game-schema` — database layer for persistent game state
- `redis-game-patterns` — caching, pub/sub, presence, and ephemeral state

## Core Principles

> "I'm not interested in creating a game that does things for the player. I want to create a simulation that responds to the player." — Will Wright

> "A game is a series of interesting decisions." — Sid Meier

The server is a simulation system that responds to player intentions and produces meaningful state changes. Every design decision should preserve determinism, enforce authority, and keep the door open for any genre.

1. **Server-authoritative**: the client sends intentions (actions), the server validates and executes. Never trust the client.
2. **Separate simulation loop from I/O**: the game tick runs on a fixed timestep independent of network or render cycles.
3. **Fixed timestep for deterministic game logic**: same inputs at the same tick always produce the same output, enabling replays, rollback, and offline catchup.
4. **Room-based architecture for multiplayer isolation**: each room encapsulates its own state, tick loop, and player set. Rooms are the unit of scaling.
5. **Stateless REST for CRUD, WebSocket for real-time state**: use REST for durable operations (create account, save inventory, fetch leaderboard) and WebSocket for ephemeral, high-frequency updates (game state, actions, presence).
6. **Event-driven**: game actions produce events, systems consume them. Decouple producers from consumers for testability and extensibility.
7. **Horizontal scaling via Redis pub/sub**: multiple Elysia instances share state through Redis. Any instance can handle any player; cross-server communication uses pub/sub channels.

## Step-by-Step Instructions

### 1. Define the Message Protocol

Start with `templates/message-types.ts`. Define discriminated unions for every client-to-server and server-to-client message. This is the contract between frontend and backend — get it right first.

```typescript
import type { ClientMessage, ServerMessage } from './templates/message-types';
```

### 2. Set Up the Elysia Server

Use `boilerplate/server.ts` as the entry point. It wires together:
- REST routes for CRUD operations (players, sessions, inventory)
- A WebSocket endpoint with JWT authentication
- Message routing to the room manager

```bash
bun run boilerplate/server.ts
```

### 3. Implement Room Management

Use `boilerplate/room-manager.ts`. The room manager handles:
- Creating and destroying rooms
- Joining and leaving players
- Broadcasting messages to room members
- Room lifecycle transitions (created -> active -> closing -> closed)

Each room owns its game state and tick loop instance.

### 4. Configure the Game Loop

Use `boilerplate/game-loop.ts`. The `GameLoop` class provides:
- Fixed-timestep accumulator pattern
- Configurable tick rate (default 20 TPS for most genres)
- Speed multiplier for fast-forward or slow-motion
- Delta time overflow protection (caps accumulated time to prevent spiral of death)

Attach one `GameLoop` per room. The tick callback receives `dt` in seconds and updates room state.

### 5. Wire Room State to WebSocket Broadcasts

After each tick, diff the room state and broadcast deltas to all room members:

```typescript
room.gameLoop.onTick = (dt) => {
  room.state = updateGameState(room.state, dt);
  const delta = computeDelta(room.previousState, room.state);
  if (delta) {
    room.broadcast({ type: 'state_update', roomId: room.id, state: delta, tick: room.tick });
  }
  room.previousState = structuredClone(room.state);
  room.tick++;
};
```

### 6. Add Redis Pub/Sub for Scaling

When running multiple Elysia instances behind a load balancer:

```typescript
import { Redis } from 'ioredis';

const pub = new Redis(process.env.REDIS_URL!);
const sub = new Redis(process.env.REDIS_URL!);

sub.subscribe('game:broadcast', 'game:direct');
sub.on('message', (channel, message) => {
  const parsed = JSON.parse(message);
  if (channel === 'game:broadcast') {
    server.publish(parsed.channel, parsed.data);
  }
  if (channel === 'game:direct') {
    const ws = connections.get(parsed.userId);
    if (ws) ws.send(parsed.data);
  }
});
```

### 7. Implement Presence and Heartbeat

- Client sends `ping` every 30 seconds
- Server responds with `pong` + server timestamp
- If no ping within 90 seconds, mark player offline and clean up room membership
- Store presence in Redis with TTL for automatic expiry

## Code Examples

### Fixed-Timestep Server Loop

```typescript
const TICK_RATE = 20;
const TICK_DURATION_MS = 1000 / TICK_RATE;

const loop = new GameLoop(TICK_RATE, (dt) => {
  for (const room of roomManager.getActiveRooms()) {
    room.update(dt);
    room.broadcastState();
  }
});

loop.start();
```

### Server-Authoritative Action Processing

```typescript
const processAction = (
  room: Room,
  playerId: string,
  action: { type: string; payload: Record<string, unknown> },
): ActionResult => {
  // 1. Validate: is this action legal in current state?
  const validation = validateAction(room.state, playerId, action);
  if (!validation.valid) {
    return { success: false, error: validation.reason };
  }

  // 2. Execute: apply to authoritative state
  const events = applyAction(room.state, playerId, action);

  // 3. Broadcast: notify all room members of resulting events
  for (const event of events) {
    room.broadcast({
      type: 'game_event',
      event: event.type,
      data: event.data,
      tick: room.tick,
    });
  }

  return { success: true, events };
};
```

### Room-Scoped State Update

```typescript
const updateRoomState = (room: Room, dt: number) => {
  // Run all registered systems in order
  for (const system of room.systems) {
    system.update(room.state, dt);
  }

  // Process queued player actions
  while (room.actionQueue.length > 0) {
    const { playerId, action } = room.actionQueue.shift()!;
    processAction(room, playerId, action);
  }

  room.tick++;
};
```

### WebSocket Message Routing

```typescript
const handleMessage = (ws: GameWebSocket, data: ClientMessage) => {
  const { playerId } = ws.data;

  switch (data.type) {
    case 'join_room':
      roomManager.joinRoom(data.roomId, playerId, ws);
      break;
    case 'leave_room':
      roomManager.leaveRoom(data.roomId, playerId);
      break;
    case 'action':
      roomManager.queueAction(data.roomId, playerId, data.action);
      break;
    case 'ping':
      ws.send(JSON.stringify({ type: 'pong', serverTime: Date.now() }));
      break;
  }
};
```

### Delta State Synchronization

```typescript
const computeDelta = (
  previous: Record<string, unknown>,
  current: Record<string, unknown>,
): Record<string, unknown> | null => {
  const delta: Record<string, unknown> = {};
  let hasChanges = false;

  for (const key of Object.keys(current)) {
    if (JSON.stringify(previous[key]) !== JSON.stringify(current[key])) {
      delta[key] = current[key];
      hasChanges = true;
    }
  }

  return hasChanges ? delta : null;
};
```

## Cross-References

- **`redis-game-patterns`** — presence storage, pub/sub for cross-server communication, room state caching, rate limiting
- **`bullmq-game-queues`** — offload heavy computations (matchmaking, leaderboard recalc, scheduled events) to background workers
- **`game-state-sync`** — client-side state reconciliation, interpolation, optimistic updates, rollback
- **`postgres-game-schema`** — persistent storage for player profiles, inventory, match history, room configurations

## Pitfalls & Anti-Patterns

### 1. Client-Authoritative State
**Wrong**: client sends "my position is (100, 200)" and server trusts it.
**Right**: client sends "move north" intention, server validates movement speed, collision, and updates position.

### 2. Variable Timestep for Game Logic
**Wrong**: using wall-clock delta time directly in game formulas.
**Right**: fixed-timestep accumulator — game logic always sees the same `dt`, regardless of how fast the server runs.

### 3. Global State Instead of Room Isolation
**Wrong**: single shared game state object for all players.
**Right**: each room owns its state, tick loop, and player set. Rooms are independent simulation contexts.

### 4. Synchronous Database Writes in the Tick Loop
**Wrong**: `await db.update(players).set(...)` inside the tick callback.
**Right**: batch state changes and persist asynchronously outside the tick loop (e.g., every N ticks or on room close).

### 5. Broadcasting Full State Every Tick
**Wrong**: sending the entire game state to every player on every tick.
**Right**: compute deltas and only send what changed. Use interest management to filter updates per player.

### 6. No Overflow Protection
**Wrong**: accumulator grows unbounded when server lags, causing hundreds of catch-up ticks.
**Right**: cap accumulated time (e.g., 1 second max) to prevent spiral of death.

### 7. Tight Coupling Between Message Types and Game Logic
**Wrong**: game systems import WebSocket types and send messages directly.
**Right**: game systems produce events, a separate broadcast layer translates events to WebSocket messages.

### 8. No Graceful Room Shutdown
**Wrong**: rooms disappear instantly when the last player leaves, losing unsaved state.
**Right**: transition through closing state — persist state, notify players, clean up resources, then destroy.

## Designer Philosophy

**Will Wright's Simulation Thinking**: the server is not a script executor — it is a simulation. Players interact with systems that have emergent behavior. The server defines rules and constraints; the interesting outcomes arise from player interactions within those rules. Design your tick loop as a simulation step, not a sequence of hardcoded events.

**Sid Meier's Interesting Decisions**: every message from the client represents a decision. The server's job is to make that decision meaningful by enforcing constraints (can you afford this action?), producing consequences (what happens as a result?), and communicating outcomes (what changed?). If an action has no validation and no consequence, it is not an interesting decision — remove it or make it matter.

**Practical Implications**:
- Validation is gameplay. Rejection messages should explain *why* an action failed, giving players information for their next decision.
- Events are the language of consequence. When an action produces events that other systems consume, you get emergent complexity without writing combinatorial logic.
- The tick loop is your simulation clock. Everything that "happens" in the game happens during a tick. Between ticks, the game world is frozen and consistent.

## Sources

- Glenn Fiedler, "Fix Your Timestep!" — https://gafferongames.com/post/fix_your_timestep/
- Gabriel Gambetta, "Fast-Paced Multiplayer" — https://www.gabrielgambetta.com/client-server-game-architecture.html
- Elysia documentation — https://elysiajs.com/
- Redis pub/sub documentation — https://redis.io/docs/manual/pubsub/
- Will Wright, GDC talks on simulation design
- Sid Meier, GDC 2012, "Interesting Decisions"

Files in this skill

  • SKILL.md11.7 KB
  • source.json796 B

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.