Skip to content

Multi-Server Clusters

How QS-Bridge manages N game servers from a single SpacetimeDB instance.


Overview

Multi-server clustering is the centrepiece of QS-Bridge. A single SpacetimeDB module serves the platform layer of an entire cluster. Platform tables like ServerConfig carry a server_id: String column for per-server partitioning; global tables like BanEntry are shared across all servers.

Game-specific tables (positions, vitals, buildings, AI, etc.) live in separate sub-databases owned by each game module — not in the platform module.

Topology

graph TD
    subgraph Servers["Game Servers"]
        A["Game Server A<br/>Game Process + libqsbridge.so 8 MB<br/>QSB_SERVER_ID=us-east-pvp-1"]
        B["Game Server B<br/>Game Process + libqsbridge.so 8 MB<br/>QSB_SERVER_ID=eu-west-pve-1"]
        N["Game Server N<br/>Game Process + libqsbridge.so 8 MB<br/>QSB_SERVER_ID=au-east-pvp-2"]
    end

    subgraph STDB["SpacetimeDB Instance"]
        SR["ServerRegistry — one row per server, the glue table"]
        subgraph Global["Global Tables"]
            G1["BanEntry"]
            G2["WhitelistEntry"]
            G3["AdminRole"]
            G4["ModuleConfig"]
            G5["PanelSession"]
            G6["PanelAuditLog"]
            G7["PlayerServerPresence"]
            G8["GlobalVault"]
            G9["ServerTransferQueue"]
            G10["CrossServerEvent"]
        end
        subgraph Local["Local Tables (server_id column)"]
            L1["ServerConfig"]
        end
        subgraph GameDB["Game Module Sub-Databases"]
            GDB1["Each game module gets its own STDB<br/>sub-database with game-specific tables"]
        end
    end

    A -->|"WebSocket (BSATN binary)"| STDB
    B -->|"WebSocket (BSATN binary)"| STDB
    N -->|"WebSocket (BSATN binary)"| STDB

    STDB -->|"WebSocket"| Panel
    STDB -->|"HTTPS"| Bisect

    Panel["React Panel<br/>panel.qs-zuq.com<br/>Cluster Dash / Server Detail / Player Passport"]
    Bisect["BisectHosting Starbase API<br/>server lifecycle<br/>start/stop/restart/wipe/auto-scale"]

Server Identity Strategy

Two identifiers per server, both stored in ServerRegistry:

Field Type Purpose Example
server_id String (PK) User-facing display name, set via QSB_SERVER_ID env var "us-east-pvp-1"
stdb_identity Identity (unique) Backend STDB identity of the connecting bridge process Identity(0xabcd…)

The bridge reads server_id from the QSB_SERVER_ID environment variable. On startup, it calls server_register which upserts the ServerRegistry row, binding the human-readable name to the cryptographic identity.

Table Classification

Every platform table is classified by its multi-server scope:

🌍 Global Tables

Global tables have no server_id column. All servers see the same data. Changes are instantly visible cluster-wide.

  • BanEntry — ban on any server → visible everywhere
  • WhitelistEntry — whitelist is cluster-wide
  • AdminRole — admin permissions span all servers
  • ModuleConfig — platform singleton
  • PanelSession — panel sessions are global
  • PanelAuditLog — audit trail spans all servers
  • PlayerServerPresence — tracks which server each player is on
  • GlobalVault — cross-server item bank
  • ServerTransferQueue — character transfers between servers
  • CrossServerEvent — cluster-spanning meta-events
  • ServerRegistry — the glue table itself

🔵 Local Tables

Local tables have a server_id column. Game hooks subscribe with WHERE server_id = '{self.server_id}' to see only their own data.

  • ServerConfig — each server has its own name, MOTD, password, whitelist setting

Cross-Server Capabilities

# Capability Description Tables Used
1 Global ban/whitelist/admin sync Ban on any server → instantly visible cluster-wide BanEntry, WhitelistEntry, AdminRole
2 Player presence tracking Know which server every player is on, prevent dual-login PlayerServerPresence
3 Zero-downtime character transfer Move characters between servers with data intact ServerTransferQueue, GlobalVault, PlayerServerPresence
4 Cross-server item vault Deposit/withdraw items across servers (game-agnostic JSON blobs) GlobalVault
5 Cross-server events Meta-events spanning multiple servers CrossServerEvent
6 Hosting API integration Start/stop/restart/wipe from panel or reducer ServerRegistry, BisectHosting API
7 Live cluster dashboard Real-time server grid with status, player counts, health ServerRegistry (panel subscription)
8 Population flow analysis Track player movement between servers over time PlayerServerPresence, ServerRegistry

Bridge Registration Flow

flowchart TD
    Start["Bridge Process Start"] --> S1
    S1["1. Read QSB_SERVER_ID from environment"] --> S2
    S2["2. Connect to SpacetimeDB via WebSocket"] --> S3
    S3["3. Receive Identity from STDB (ctx.sender)"] --> S4
    S4["4. Call server_register reducer<br/>Upsert ServerRegistry row (server_id ↔ stdb_identity)<br/>Set status = online"] --> S5
    S5["5. Subscribe to platform tables<br/>ServerConfig WHERE server_id = self<br/>BanEntry (full), AdminRole (full), WhitelistEntry (full)"] --> S6
    S6["6. Start heartbeat timer (30s interval)<br/>Calls server_heartbeat reducer<br/>Updates last_heartbeat + online_count"] --> S7
    S7["7. Begin processing game events"]

One Account → Many Servers

The primary deployment target is community server operators running multiple game servers under a single BisectHosting account. The BisectHosting Starbase API key maps to one account that owns all servers. ServerRegistry.hosting_id links each STDB server row to its BisectHosting server UUID for lifecycle API calls.

graph TD
    Account["BisectHosting Account"] --> S1["Server UUID: abc-123<br/>ServerRegistry: us-east-pvp-1"]
    Account --> S2["Server UUID: def-456<br/>ServerRegistry: eu-west-pve-1"]
    Account --> S3["Server UUID: ghi-789<br/>ServerRegistry: au-east-pvp-2"]

Per-Server Mod Selection

Each server independently chooses which mods to run. The InstalledMod table in the Mod Registry tracks (server_id, project_id, version, status). The same mod can be installed on multiple servers with different versions and configurations.