Skip to main content

Overview

Spectra Server acts as a real-time data pipeline, receiving game state from observer clients, processing it through the match orchestration layer, and broadcasting it to connected frontends. This page details the complete data journey.

Observer Client Connection Flow

The primary data flow begins when an observer client connects to port 5100.

Authentication Sequence

1

WebSocket Connection

Client establishes WebSocket connection to wss://server:5100 (or ws:// in insecure mode)
2

Logon Event

Client sends obs_logon event with authentication payload:
{
  type: "authenticate",
  clientVersion: "1.0.0",
  obsName: "Observer Name",
  key: "access-key-here",
  groupCode: "ABCD1234",
  leftTeam: { name: "Team A", tricode: "TMA", ... },
  rightTeam: { name: "Team B", tricode: "TMB", ... },
  toolsData: { /* overlay configurations */ }
}
3

Server Validation

Server validates the request (see src/connector/websocketIncoming.ts:66):
  • Packet validation: Ensures type is DataTypes.AUTH
  • Version check: Calls isCompatibleVersion() to verify client compatibility
  • Key validation: Checks access key via isValidKey() method:
    • If REQUIRE_AUTH_KEY=false, automatically valid
    • If matches AUTH_KEY env var, valid
    • If USE_BACKEND=true, validates against backend API
  • Match creation: Calls MatchController.createMatch()
4

Match Creation Response

On success, server:
  1. Creates new Match instance
  2. Generates 12-character groupSecret for reconnection
  3. Stores match in MatchController.matches registry
  4. Sends obs_logon_ack event:
{
  type: "authenticate",
  value: true,
  reason: "GROUPSECRET123" // or "reconnected"
}
  1. Registers obs_data event listener for game state
Reconnection Support: If a client disconnects and reconnects with the same group code and group secret, the server reuses the existing match instance rather than creating a new one (see src/controller/MatchController.ts:46).

Game State Ingestion

Once authenticated, the observer client begins sending game data through the obs_data event.

Data Processing Pipeline

1

Event Reception

Server receives obs_data event (src/connector/websocketIncoming.ts:249):
ws.on("obs_data", async (msg: any) => {
  const data = JSON.parse(msg.toString());
  if (isAuthedData(data)) {
    await this.matchController.receiveMatchData(data);
  }
});
2

Match Routing

MatchController.receiveMatchData() routes data to the correct match instance (src/controller/MatchController.ts:96):
  • Adds timestamp to data
  • Finds match by groupCode
  • Validates match exists
  • Forwards to Match.receiveMatchSpecificData()
3

Event Type Handling

The Match class processes different event types (src/model/Match.ts:102):Scoreboard Events (DataTypes.SCOREBOARD):
  • Routes to correct team by startTeam ID
  • Detects spike plants (300 credit increase)
  • Updates player stats (K/D/A, money, ult points)
Roster Events (DataTypes.ROSTER):
  • Updates agent selections
  • Tracks agent select start time
  • Updates player positions
Killfeed Events (DataTypes.KILLFEED):
  • Broadcasts to both teams
  • Tracks kills, assists, weapons used
Round Info (DataTypes.ROUND_INFO):
  • Updates round number and phase
  • Handles side switches at round 13 and OT rounds
  • Triggers backend updates on round start
  • Manages timeout grants in overtime
Match Start (DataTypes.MATCH_START):
  • Stores match ID from game
  • Registers match with backend (if enabled)
  • Sets isRunning = true
Score Updates (DataTypes.SCORE):
  • Determines round winner
  • Calculates round end reason (kills/defuse/detonation/timeout)
  • Updates team scores
4

Event Number Increment

After processing each event, Match.eventNumber++ is called. This triggers the broadcast system to detect new data.

Event Type Reference

The server handles these primary event types (defined in src/model/eventData.ts:155):
Event TypePurposeFrequency
SCOREBOARDPlayer stats, money, ult, armor~Every tick
ROSTERAgent selection, player orderAgent select phase
KILLFEEDKill/death notificationsPer elimination
ROUND_INFORound phase, round numberPhase changes
SCORETeam scores after round endPer round
MATCH_STARTMatch UUID from gameOnce per match
MAPMap being playedOnce per match
GAME_MODEBomb/Swift playOnce per match
OBSERVINGCurrently observed playerWhen observer switches
SPIKE_PLANTEDSpike plant notificationPer spike plant
SPIKE_DEFUSEDSpike defusalPer defusal
SPIKE_DETONATEDSpike explosionPer detonation
Spike plant detection has two mechanisms:
  1. Direct event: Client sends explicit SPIKE_PLANTED event via hotkey
  2. Automatic detection: Server detects 300 credit increase in scoreboard data (src/model/Match.ts:443)

Match State Management

The Match class maintains comprehensive game state:

Core State

{
  matchId: string,              // Game-assigned UUID
  groupCode: string,            // User-assigned code (e.g., "ABCD1234")
  groupSecret: string,          // 12-char reconnection token
  roundNumber: number,          // Current round (1-25+)
  roundPhase: string,           // "LOBBY", "shopping", "combat", "end", "game_end"
  isRunning: boolean,           // Match active status
  map: string,                  // Map name
  teams: Team[],                // Two Team instances
  spikeState: {                 // Spike tracking
    planted: boolean,
    detonated: boolean,
    defused: boolean
  },
  timeoutState: {               // Timeout tracking
    techPause: boolean,
    leftTeam: boolean,
    rightTeam: boolean,
    timeRemaining: number
  },
  eventNumber: number,          // Increments on each state change
  // ... plus tools, sponsors, watermark configs
}

State Updates Flow

1

Event Received

New event data arrives via receiveMatchSpecificData()
2

Replay Logging

Event written to ReplayLogging for match replay capability
3

Type-Specific Processing

Event processed based on type (scoreboard, killfeed, etc.)
4

Event Number Increment

this.eventNumber++ marks state as changed
5

Broadcast Detection

Send loop detects eventNumber increase and triggers broadcast

Broadcasting to Frontends

Frontend Connection

1

WebSocket Connection

Frontend connects to wss://server:5200
2

Logon Event

Frontend sends simple logon with group code:
ws.emit('logon', JSON.stringify({ groupCode: 'ABCD1234' }));
3

Room Join

Server joins socket to room named by group code (src/connector/websocketOutgoing.ts:62):
ws.join(json.groupCode);
4

Initial State

Server sends current match state via sendMatchDataForLogon() (src/controller/MatchController.ts:134)
5

Logon Success

Server emits confirmation:
ws.emit('logon_success', JSON.stringify({
  groupCode: json.groupCode,
  msg: `Logon succeeded for group code ${json.groupCode}`
}));

Broadcast Loop

The MatchController runs a continuous send loop at 100ms intervals (src/controller/MatchController.ts:160):
this.sendInterval = setInterval(async () => {
  for (const groupCode in this.matches) {
    // Check if event number has increased
    if (this.matches[groupCode].eventNumber > this.eventNumbers[groupCode]) {
      // Broadcast updated match state
      this.outgoingWebsocketServer.sendMatchData(groupCode, this.matches[groupCode]);
      // Update tracked event number
      this.eventNumbers[groupCode] = this.matches[groupCode].eventNumber;
      this.eventTimes[groupCode] = Date.now();
    } else {
      // Check for inactive matches (30 minute timeout)
      if (Date.now() - this.eventTimes[groupCode] > 1000 * 60 * 30) {
        this.removeMatch(groupCode);
      }
    }
  }
}, 100);
Efficient Broadcasting: The server only sends data when eventNumber has increased, preventing redundant broadcasts and reducing bandwidth.

Data Filtering

Before broadcasting, sensitive data is removed (src/connector/websocketOutgoing.ts:88):
const {
  replayLog,           // Internal event log
  eventNumber,         // Internal counter
  groupSecret,         // Reconnection token
  playercamUrl,        // Private playercam data
  timeoutEndTimeout,   // Internal timer
  timeoutRemainingLoop,// Internal timer
  ...formattedData
} = data;

// Also remove playercam secret from tools
if (deepMod.tools?.playercamsInfo) {
  delete deepMod.tools.playercamsInfo.secret;
  delete deepMod.tools.playercamsInfo.endTime;
}

// Broadcast filtered data
this.wss.to(groupCode).emit('match_data', JSON.stringify(deepMod));

Auxiliary Client Data Flow

Auxiliary clients provide additional game data (player abilities, health, etc.) using a separate authentication flow.
1

Auxiliary Connection

Aux client connects to port 5100 and sends aux_logon event (src/connector/websocketIncoming.ts:158):
{
  type: "aux_authenticate",
  clientVersion: "1.0.0",
  name: "Player Name",
  matchId: "uuid-from-game",
  playerId: "player-uuid"
}
2

Match ID Validation

Server finds match by matchId (not group code):
const groupCode = this.matchController.findMatch(authenticationData.matchId);
if (groupCode == null) {
  // Reject - match not found
}
3

Auxiliary Authentication

Server authenticates and registers client, then listens for aux_data events
4

Auxiliary Data Processing

Aux data types include:
  • AUX_SCOREBOARD: Player scoreboard from in-game
  • AUX_SCOREBOARD_TEAM: Full team scoreboard
  • AUX_ABILITIES: Ability charge counts
  • AUX_HEALTH: Player health/armor
  • AUX_ASTRA_TARGETING: Astra star targeting mode
  • AUX_CYPHER_CAM: Cypher camera active state
5

Multi-Match Support

Aux data is sent to all matches with matching matchId, enabling tournament servers to track multiple concurrent games
Disconnection Handling: When an auxiliary client disconnects, the server marks their player as disconnected in the match state (src/connector/websocketIncoming.ts:233), allowing frontends to show disconnected status.

Match Lifecycle

A complete match flow from creation to cleanup:
1

Match Creation

Observer authenticates → Match instance created → Stored in registry
2

Agent Select

Roster events update agent selections → Agent select start time recorded
3

Match Start

Game assigns match ID → Backend registration (if enabled) → Stats tracking begins
4

Round Loop

For each round:
  • Shopping phase → Side switches (if applicable)
  • Combat phase → Scoreboard/killfeed/spike events
  • End phase → Score calculation → Round reasons determined
5

Game End

Round phase becomes game_end → Backend completion → Stats fetch (supporters) → Match removed from registry after 5 seconds
6

Timeout Cleanup

If no events for 30 minutes → Automatic match completion and removal (src/controller/MatchController.ts:168)

Performance Characteristics

Broadcast Efficiency

  • 100ms send interval: Maximum 10 updates/second
  • Room-based broadcasting: Only sends to subscribed frontends
  • Event-driven: Only broadcasts when state changes
  • Compression: perMessageDeflate reduces bandwidth ~60-70%

Scalability

  • Multiple matches: Single server handles many concurrent matches
  • Multiple frontends: Unlimited viewers per match (room-based)
  • Singleton controller: MatchController and WebsocketOutgoing use singleton pattern
  • Automatic cleanup: Inactive matches removed to prevent memory leaks
Production Tip: For high-load scenarios with many concurrent matches, monitor the send loop performance. The 100ms interval processes all active matches sequentially.

Next Steps

System Components

Explore detailed implementation of each component

Architecture Overview

Return to high-level architecture overview

Build docs developers (and LLMs) love