Skip to content

Subscriptions & Real-Time Data

How game hooks and the admin panel receive live data from SpacetimeDB.


Overview

SpacetimeDB uses SQL subscriptions over WebSocket connections. Clients declare queries; the database pushes incremental changes (onInsert, onUpdate, onDelete callbacks) whenever matching rows change. There is no polling.

This is the key architectural advantage: SpacetimeDB subscriptions scale as O(changed_rows × log(subscriptions)), not O(total_rows × total_clients). This enables real-time dashboards with thousands of rows and dozens of connected panel sessions without performance degradation.

Subscription Architecture

graph TD
    subgraph STDB["SpacetimeDB Instance"]
        subgraph Tables["Platform Tables (12)"]
            SR["ServerRegistry (all servers)"]
            BE["BanEntry (all bans)"]
            AR["AdminRole (all admins)"]
            SC["ServerConfig (per server_id)"]
            PSP["PlayerServerPresence (all)"]
            PS["PanelSession (all)"]
            PAL["PanelAuditLog (all)"]
        end
    end

    Tables --> HookA
    Tables --> Panel
    Tables --> HookB

    subgraph HookA["Game Hook A (Bridge)"]
        A1["ServerConfig WHERE s_id=A"]
        A2["BanEntry (full table)"]
        A3["AdminRole (full table)"]
        A4["WhitelistEntry (full table)"]
    end

    subgraph Panel["React Panel"]
        P1["ServerRegistry (full table)"]
        P2["BanEntry"]
        P3["AdminRole"]
        P4["WhitelistEntry"]
        P5["PanelAuditLog"]
        P6["PanelSession"]
        P7["PlayerPresence"]
    end

    subgraph HookB["Game Hook B (Bridge)"]
        B1["ServerConfig WHERE s_id=B"]
        B2["BanEntry (full table)"]
        B3["AdminRole (full table)"]
        B4["WhitelistEntry (full table)"]
    end

Game Hook Subscriptions

Each libqsbridge.so instance subscribes to platform tables on startup. These subscriptions inform the bridge about bans, config changes, and admin actions.

Table Query Purpose
ServerConfig WHERE server_id = '{self.server_id}' Local server configuration — MOTD, max players, whitelist status
BanEntry Full table (no filter) Cluster-wide bans — bridge checks on player connect
AdminRole Full table (no filter) Admin permissions — bridge enforces admin commands
WhitelistEntry Full table (no filter) Whitelist — bridge enforces on player connect

Lifecycle hooks: - On init_module: register in ServerRegistry, start 30-second heartbeat timer - On on_connect: upsert PlayerServerPresence, check BanEntry, check dual-login - On on_disconnect: remove PlayerServerPresence, update ServerRegistry.online_count

Game module tables are subscribed separately in the game module's own sub-database. The platform bridge only manages platform subscriptions.

Panel Subscriptions

The React panel connects to SpacetimeDB over WebSocket for read-only live data. All admin mutations go through the API Gateway, never directly from the browser.

Table Query Panel Usage
ServerRegistry Full table Cluster dashboard — server grid with status, player counts, regions
BanEntry Full table Players page — ban status indicators, moderation actions
WhitelistEntry Full table Players page — whitelist status
AdminRole Full table Users page — role management, permission display
PanelAuditLog Full table Audit Log page — chronological admin action timeline
PanelSession Full table Sidebar — active session count, connection status
PlayerServerPresence Full table Player presence across cluster

React Implementation

The panel uses the useTable<T> hook to wrap SpacetimeDB subscription callbacks into React state:

// hooks/use-table.ts
function useTable<T>(tableName: string): T[] {
  const [rows, setRows] = useState<T[]>([]);

  useEffect(() => {
    const table = connection.db.getTable(tableName);
    // Initial rows from SubscribeApplied
    setRows([...table]);
    // Incremental updates
    table.onInsert((row) => setRows(prev => [...prev, row]));
    table.onUpdate((oldRow, newRow) =>
      setRows(prev => prev.map(r => r === oldRow ? newRow : r)));
    table.onDelete((row) =>
      setRows(prev => prev.filter(r => r !== row)));
  }, [tableName]);

  return rows;
}

Subscription Lifecycle

1. Browser opens WebSocket to SpacetimeDB
2. Client sends Subscribe message with SQL queries
3. STDB responds with SubscribeApplied + initial rows (BSATN binary)
4. Client populates local state from initial rows
5. On any table mutation, STDB pushes TransactionUpdate
   with only the changed rows (inserts + deletes)
6. Client applies incremental updates to local state
7. React re-renders affected components

No polling. The panel holds no server state itself — SpacetimeDB subscriptions act as a real-time client-side cache. After the initial page load, only incremental callbacks arrive.

Caching Architecture

The panel uses React Query alongside STDB subscriptions for optimal performance:

Layer Technology Role
STDB Subscriptions SpacetimeDB WebSocket Real-time table data (bans, admins, audit log)
React Query useClusterState() hook Cached cluster state with 10s stale time, 30s polling
Component State React useState UI state (selected tabs, expanded rows, filters)

This three-layer approach ensures: - Live data arrives instantly via subscriptions - Expensive queries are cached and shared across components - UI interactions remain responsive regardless of data volume