LegacyShell WikiLegacyShell Wiki
Back to LegacyShell
Home
Wiki
Plugins
Docs
Back to LegacyShell
Home
Wiki
Plugins
Docs
  • Documentation

    • Getting Started

      • What is LegacyShell?
      • Speed Setup
      • Requirements
      • Installation
      • First Run
      • Config Files
      • Making an Account
      • Troubleshooting (Getting Started)
    • Running a Server

      • Architecture Overview
      • The Database
      • Users and Ranks
      • Adding Game Servers
      • Client Mirrors
      • Perpetual
      • Backups
      • Rate Limiting
      • Moderation
      • Closed Mode
      • Deployment
      • Troubleshooting (Running a Server)
      • Hosting for Someone Else's Instance
    • Content Creation

      • Maps
      • Dealing with Babylon Models
      • Map Blocks
      • Items and Skins
      • Hats and Stamps
      • Sounds
      • Gamemodes
      • Seasonal Events
    • Plugin Development

      • Quickstart
      • I Want To...
      • Anatomy of a Plugin
      • Lifecycle
      • Dependencies
      • Events (Concept)
      • Event Reference

        • services: events
        • game: events — shared logic (src/shell/)
        • game: events — main-thread server process
        • game: events — per-connection client object
        • game: events — room lifecycle & tick loop
        • game: events — in-browser gameplay
        • client: events — client server & build pipeline
      • Commands
      • Client-Side Code
      • Static Assets
      • Content Packs
      • Networking
      • Workers and State
      • Prediction and Authority
      • Recipes

        • Recipe: Killstreaks
        • Recipe: New Pickup Item
        • Recipe: New Gamemode
        • Recipe: Custom Weapon
        • Recipe: UI Modification
        • Recipe: Discord Integration
        • Recipe: Replacing Core Behaviour
        • Recipe: Persistent Plugin Storage
        • Recipe: Rewarding Players with Currency
        • Recipe: Custom Per-Player Data
        • Recipe: Custom Theme
      • Publishing
      • Pitfalls
      • Modifiers
      • Sound and Apollo
    • Codebase Reference

      • Repo Layout
      • Shared Shell Layer
      • Server-Only Markers
      • The ss Object
      • Build Pipeline
      • Stamps and Babylons
      • Game Loop
      • Rooms and Workers
      • Wire Protocol
      • Generated

        • Wire Protocol Opcodes
        • Enums & Lookup Tables
        • Database Schema
        • Config Reference
        • Slash Command Reference
      • Services Internals
      • Catalog and Items
      • Permissions Internals
      • Physics and Collision
      • Known Quirks
      • Codebase Anecdotes
      • Development Timeline
    • Contributing

      • Documentation Style Guide
      • Generators
      • For AI Agents

Architecture Overview

Audience: Server operators · Prereqs: Getting Started

Canonical source: server-services/, server-game/, server-client/, src/shell/general/misc.js

What is LegacyShell? introduced the three servers at a glance. This page goes one level deeper - what you actually need to know to operate a real deployment, especially once it's more than "everything on one machine."

At a glance

Player'sBrowserClient mirrors (many)Client serverClient mirror #2Game servers (many)Game server #1Game server #2Services single instance - SQLite DBaccounts, items, maps, auth_keysHTTP: downloadgame + wikiWebSocket:gameplayrequestConfigpollrequestConfig poll+ auth_key

A visitor's browser only ever talks to a client server (to download the game) and a game server (to actually play) - it never talks to services directly. Both server roles poll services independently, and each caches the last response locally so a brief services outage doesn't take the whole fleet down (see "How they find each other" below).

The three roles, in detail

RoleDirectoryProtocol / default portCan you run more than one?
Servicesserver-services/WebSocket only, :13371No (not without extra work to sync data across instances) - this is the single source of truth.
Gameserver-game/WebSocket (players) :13372, plus an outbound connection to servicesYes - add as many as you want, each registered against your services server.
Clientserver-client/HTTP, :13370Yes - unauthenticated mirrors, point them at the same services server.

Services: the single source of truth

Services owns the SQLite database (server-services/store/LegacyShellData.db) - accounts, sessions, item/map definitions, redemption codes, and the list of authorized game servers. It's the only server that talks to the database directly. There should be exactly one services server per deployment (see The Database).

Its entire external interface is a raw WebSocket server - no HTTP, no REST API. Every message is a small JSON envelope ({ cmd: "...", ... }), and it applies per-IP rate limiting to most commands (see Rate Limiting) - except for connections presenting a valid auth_key, which bypass rate limiting entirely (see Adding Game Servers).

Game: where matches actually happen

Each game server is authoritative for the matches it's running - physics, hit detection, scoring - and pushes state updates to connected players over its own WebSocket. It doesn't touch the database directly; instead it talks to services for anything account-related (recording a kill, checking a session).

Internally, each game room (a single ongoing match) runs in its own dedicated worker thread, isolated from every other room on the same server - useful to know if you ever get into plugin development, covered in Codebase Reference, but not something you need to think about just to operate a server.

A game server must be authorized by the services server owner before it will do anything useful - see Adding Game Servers.

Client: what your browser actually downloads

The client server is a plain Express web server, serving the built browser game (HTML/JS/assets) plus the integrated wiki. It's unauthenticated by design - anyone can run a client mirror pointed at your services server, the same way anyone could run a mirror of a normal website. It builds its own JavaScript bundle on startup (merging the shared game-logic code that's also used server-side - not something you need to touch as an operator, but if a first boot seems to take a while, this is why).

How they find each other

Game and client servers don't just start talking to a services server blindly - each one polls services on startup (and periodically afterward) with a requestConfig request, asking for the current maps, items, list of authorized servers, and any live config services wants to push out. Both cache the last response to local disk (store/maps.json, items.json, servers.json), so if services is briefly unreachable, a game or client server can still boot using the last-known-good data rather than refusing to start.

If services restarts, every connected game/client server notices (it reports a newer startTime on the next poll) and restarts itself to pick up the change - this is what the Perpetual process manager's auto-restart is for.

A single-machine deployment

Running everything on one computer (what Getting Started walks through) is just the smallest valid version of this architecture: one services, one game, one client, all pointed at localhost. Nothing about the architecture changes - there's just nothing to distribute yet.

A multi-region deployment

Scaling out looks like:

  1. One services server, somewhere central.
  2. Multiple game servers, one per region you want to offer, each added to services' game_servers table with its own auth_key (see Adding Game Servers).
  3. One or more client mirrors, each pointed at the same services server via sync_server in client.yaml (see Client Mirrors).

Players pick a game server from the in-game server list (which services assembles from the game_servers table and each server's live player counts), but always download the game itself from whichever client mirror they happened to visit.


This page was drafted with AI assistance and reviewed for accuracy. If something looks wrong, please open a PR or flag it.

Edit this page on GitHub
Next
The Database