← Headless Host

HBInit and BOLTInit reference

Both initializers support the normal HaxBall configuration and room API, and both return a room with the room.bolt API. HBInit enables match statistics and vote-kick. Use BOLTInit when you want to configure the BOLT features yourself.

const room = HBInit({
  roomName: "My room",
  maxPlayers: 12,
  public: true,
  noPlayer: true
});

Quick example

const room = BOLTInit({
  roomName: "My room",
  maxPlayers: 12,
  public: true,
  noPlayer: true,

  // Optional. Add a fresh HaxBall headless token to publish there too.
  token: "YOUR_HAXBALL_TOKEN",

  bolt: {
    afk: true,
    bots: true,
    captain: true,
    voteKick: true,
    statistics: true,
    ratingLabel: "My ELO"
  }
});

room.onPlayerJoin = async (player) => {
  const rating = await loadMyRating(player.auth);
  room.bolt.setRating(player.id, rating);
};

BOLTInit configuration

Put these fields inside bolt. Every boolean feature is enabled by default. Set it to false when your script provides that feature itself.

FieldTypeWhat it enables
afkbooleanAFK controls and AFK state in the BOLT room menu.
botsbooleanBOLT bot controls. Up to 16 bots when captain picking is disabled.
captainbooleanCaptain picking and its balancing bot. This also requires the bot runtime.
voteKickbooleanVote creation, voting, results, and automatic kicks.
statisticsbooleanLive possession, goals, assists, saves, passing, and performance statistics.
ratingLabelstringLabel shown beside room-owned ratings. It is trimmed to 30 characters and defaults to ROOM ELO.

The normal token field is optional. Without it, the room is published only on BOLT. With a fresh HaxBall headless token, the same room is also published on HaxBall. Share the BOLT room link in both cases.

If registration fails, the headless page shows the reason and closes the incomplete room. Invalid or expired HaxBall tokens are called out directly. Fix the problem and call HBInit or BOLTInit again; refreshing the page is not required.

room.bolt methods

Lifecycle at a glance

  1. Initialization: HBInit and BOLTInit return the room with room.bolt ready. Register command listeners now.
  2. Room link: BOLT publishes the enabled feature list after the room is registered. You do not need to trigger this.
  3. Player activity: Set ratings when players join or their ratings change. Publish custom UI state whenever that state changes.
  4. Shutdown: In the browser runtime, keep the page open while hosting. In boltgame.js, keep the Node.js process running. Closing either host closes the room and removes its listeners and temporary state.

setRating(gameId, rating)

When: Call it after you know a player's rating, normally inside or after room.onPlayerJoin, and call it again whenever that rating changes. The player must use their current HaxBall player ID as gameId.

What happens: The local rating snapshot is updated and immediately sent to connected BOLT clients. Future BOLT clients receive the latest snapshot when they connect. The rating is display-only and is not stored by BOLT or added to the official leaderboard. Invalid values are ignored, decimals are truncated, and the entry is removed automatically when that player leaves.

setRatings([{ gameId, rating }])

When: Call it when you load or recalculate ratings for the whole current roster. It is useful after room startup, a database refresh, or a batch ELO update. Pass every rating that should remain visible.

What happens: The existing local list is replaced, not merged, and one complete snapshot is sent to BOLT clients. Any player omitted from the array loses their displayed room rating. Invalid entries are skipped. An empty array clears every room rating.

ratings()

When: Call it whenever your script needs to inspect the ratings currently held by the partner runtime. It is synchronous and does not contact the platform.

What happens: It returns a new array in the form [{ gameId, rating }]. Changing the returned array does not change the stored ratings; use setRating or setRatings to make changes.

setAfkPlayers(playerIds)

When: Call it whenever you want to update the list of players marked as AFK. Pass an array of player game IDs (e.g. [3, 8]).

What happens: Sends the AFK snapshot directly to connected BOLT clients to display the AFK badge in the room menu.

setBotPlayers(botIds)

When: Call it whenever you manage custom bots and want to update the list of bot player IDs (e.g. [20]).

What happens: Connected BOLT clients display the bot badge and bot UI controls for those player IDs.

setCaptainState(options)

When: Call it whenever captain picking state or candidate availability changes in custom captain rooms.

room.bolt.setCaptainState({
  pick: {
    team: 1,
    captainId: 3,
    candidateIds: [7, 8, 9],
    needsCount: 1,
    deadlineAt: Date.now() + 20000,
  },
  rated: false,
});

setVoteState(options)

When: Call it to open, update, or close a custom vote-kick or moderation vote.

room.bolt.setVoteState({
  vote: {
    targetName: "Player",
    reason: null,
    yes: 2,
    need: 3,
    endsAt: Date.now() + 30000,
  },
  outcome: null,
});

setMatchState(options)

When: Call it to notify connected clients whether the current match is rated (e.g. room.bolt.setMatchState(true) or room.bolt.setMatchState({ rated: true })).

setMatchStatistics(statistics)

When: Call it to push live match statistics (possession, passing accuracy, goals, assists, saves) to connected BOLT clients.

sendRatingUpdate(update)

When: Call it after a match concludes to broadcast post-match rating adjustments.

room.bolt.sendRatingUpdate({
  matchPublicId: "match-123",
  redScore: 3,
  blueScore: 1,
  before: 1500,
  after: 1515,
  delta: 15,
});

onCommand(handler)

When: Register the handler immediately after initialization when your script needs to react to BOLT room-menu actions. Keep the returned cleanup function and call it when your feature is disabled or replaced. You do not need this listener only to use BOLT's built-in features.

What happens: The handler runs with the sending player's HaxBall ID and a validated command. Enabled built-in features process the command first, then your handlers run in registration order. Removing the listener stops future callbacks but does not undo actions already applied. All listeners disappear when the hosting page is closed or refreshed.

const stopListening = room.bolt.onCommand((playerId, command) => {
  console.log(playerId, command);
});

// Later:
stopListening();

Commands received by onCommand

The first argument is the HaxBall player ID that sent the command. The second argument has one of these shapes:

{ type: "afk.set", afk: true | false }

{ type: "bot.add" }
{ type: "bot.remove", playerId: 4 }

{ type: "captain.pick", choice: 7 | "top" | "bottom" | "random" }
{ type: "captain.move", playerId: 7, team: 0 | 1 | 2 }

{ type: "vote.open", targetId: 7, reason: "reason" | null }
{ type: "vote.cast", choice: "yes" | "no" }

Presentation methods summary

Use presentation methods only when your script owns the corresponding feature. Disable the matching built-in feature in BOLTInit first so two implementations do not publish competing state.

room.bolt.setAfkPlayers([3, 8]);
room.bolt.setBotPlayers([20]);
room.bolt.setMatchState({ rated: true });

The presentation methods directly update the connected BOLT clients over the room's optional peer channel without mutating game physics. HaxBall clients ignore this optional channel and continue playing normally.

Standard HaxBall listeners are unchanged

Continue using room.onPlayerJoin, room.onPlayerLeave, room.onGameTick, and every other standard headless callback normally. BOLTInit does not replace their public signatures. room.bolt.onCommand is the only additional listener API.

Running in Node.js (boltgame.js)

When running in Node.js 24 or newer, use boltgame.js as the server-side replacement for haxball.js. It uses native WebRTC and does not require Chromium or an open BOLT Headless browser page. Change the package import; standard room configuration, callbacks, and room methods remain the same.

Installation

npm install boltgame.js

Usage

Use the exact same syntax as haxball.js:

import BoltGameJS from 'boltgame.js';

const HBInit = await BoltGameJS();

const room = HBInit({
  roomName: "My BOLT Room",
  maxPlayers: 12,
  public: true,
  noPlayer: true
});

Or configure BOLT features with BOLTInit:

import { BOLTInit } from 'boltgame.js';

const room = BOLTInit({
  roomName: "BOLT Pro Room",
  bolt: {
    statistics: true,
    voteKick: true,
    afk: true,
    captain: true,
    bots: true,
    ratingLabel: "ROOM ELO"
  }
});

To list an existing production room on both BOLT and HaxBall while retaining all room-owned policy, pass a fresh HaxBall token and disable every built-in feature explicitly:

import { BOLTInit } from 'boltgame.js';

const room = BOLTInit({
  roomName: "My Production Room",
  maxPlayers: 16,
  public: true,
  noPlayer: true,
  token: process.env.HAXBALL_TOKEN,
  bolt: {
    afk: false,
    bots: false,
    captain: false,
    voteKick: false,
    statistics: false
  }
});

This creates one room process, not separate BOLT and HaxBall rooms. The token publishes that host upstream to HaxBall and BOLT exposes the same live room. room.bolt remains available for optional display-only ratings and custom UI state. BOLT does not take ownership of the room's AFK, captain, moderation, statistics, ELO, level, or persistence systems.

Gameplay packets remain direct WebRTC traffic between each player and the room process on the owner's VPS. BOLT handles directory and signaling only; it does not relay gameplay. The optional bolt presentation channel is direct to the same host as well, so dual listing introduces no BOLT gameplay hop. Exact latency can still differ by the client's normal ICE/network route.

See the package on npm: https://www.npmjs.com/package/boltgame.js.