BeatKhana Development

Custom overlay packets

Register typed events and presets for tournament-specific broadcast integrations.

Custom packets let an overlay developer add tournament-specific events without changing BeatKhana's standard picks and bans flow. Definitions belong to an individual overlay. They do not change the match map pool, picks, bans, or results themselves.

Configure a packet

Open the Hosting panel, select an overlay, and expand Advanced Overlay Settings. Organisers, tournament admins, staff, stream hosts, and users with global overlay management permissions can add, edit, and remove definitions.

Give the packet a display name and a unique event type, such as pickCategory. Built-in event names are reserved. Define the minimum payload using a TypeScript type:

{
    category: "accuracy" | "historical" | "tech" | "speed" | "balanced";
    teamGuid: string;
    teamName?: string;
}

Supported types are string, number, boolean, null, any, unknown, literal values and unions, nested objects, optional properties, arrays, Array<T>, ReadonlyArray<T>, and Record<string, T> or string index signatures. A single type Payload = …; declaration is also accepted. Imports, functions, named external types, generics, and executable code are rejected. This is a JSON-compatible subset of TypeScript; it never executes a definition.

Required declared fields must exist with the correct types. Extra JSON keys are accepted and preserved. any accepts any JSON value, including objects, arrays and primitives. Payloads must be finite JSON, no more than 64 KiB or 20 nested levels.

Hosts and tournament management can always send. The additional-role dropdown grants coordinators and/or referees access separately. When the match requires assigned coordinators or referees, only assigned people have that role's access. Coordinators receiving referee controls through an overlay setting do not automatically gain custom referee-packet permission.

Presets and placeholders

Presets provide named JSON starting points. They may be partial: the sender fills remaining required fields before sending. For example:

{
    "teamGuid": "{{team1.guid}}",
    "teamName": "{{team1.name}}"
}

Available placeholders are overlay.guid, tournament.guid, tournament.name, match.guid, match.number, team1.guid, team1.name, team2.guid, and team2.name, wrapped in double braces. Team 1 follows the selected match's Side 1; remaining teams follow their configured order. Solo teams use their player display name.

Placeholders resolve on the server from the current overlay context immediately before validation and sending. An exact {{match.number}} becomes a number; Match {{match.number}} becomes a string. Missing context and unknown placeholders produce an error without broadcasting.

Send from the panels

Send custom packet appears in hosting and coordinator/referee panels only when at least one definition is available to the signed-in user. Choose a packet and optional preset. Literal unions become dropdowns; strings, numbers and booleans have typed controls. Nested objects have grouped fields. Complex values and unrestricted payloads use JSON. The JSON tab allows editing the complete payload, and additional custom fields can be supplied separately.

Validate JSON checks current permissions, resolves placeholders and previews the final payload without sending. Send packet repeats those checks and reports success only after the server emits to the overlay rooms. This acknowledgement confirms server broadcast; it does not confirm that every viewer processed the event.

Integrate through bk-client

Authenticate a socket with a token containing ws:read and ws:write. Fetching the overlay joins its update room:

const scope = { tournamentGuid, overlayGuid };
await client.socket.authenticate();
await client.socket.overlays.get(scope);

const unsubscribe = client.socket.overlays.onCustomPacket((event) => {
    if (event.overlayGuid !== overlayGuid) return;
    if (event.packetType === "pickCategory") {
        // Display the selected country and category using event.payload.
    }
});

const acknowledgement = await client.socket.overlays.sendCustomPacket({
    ...scope,
    packetType: "pickCategory",
    payload: { category: "accuracy", teamGuid: "country-guid", extraKey: "preserved" },
});

if (acknowledgement.type === "fail") throw new Error(acknowledgement.error);

Read-only viewers need a token with ws:read whose user has access to the overlay; they do not need custom-packet send permission to receive events. Sending always requires a registered definition, ws:write, and current role/assignment permission. Definitions can be managed through client.overlays.listCustomPackets, createCustomPacket, updateCustomPacket, and deleteCustomPacket; REST mutations require rest:write as well as management permission.

The customOverlayPacket MessagePack event contains { packetType, payload, tournamentGuid, overlayGuid, sentAt, sentByGuid }. Integrations may instead listen to the registered event name, such as pickCategory, whose MessagePack body is the payload itself. Subscribe to one representation to avoid processing the same packet twice. Socket.IO room union targeting avoids duplicate delivery when a viewer joined both overlay room names.

Raw Socket.IO callers may send sendCustomOverlayPacket or the registered event name. Both accept the same MessagePack envelope { tournamentGuid, overlayGuid, packetType, payload, presetId? } with a Socket.IO acknowledgement callback. The registered-name form infers packetType from the event name. Responses are { type: "success", packet } or { type: "fail", error }; validateCustomOverlayPacket returns the same preview with validated: true and broadcasts nothing.

Packets are transient events and are not replayed after a refresh. Use custom saved states when the overlay needs to restore the latest choices or other shared JSON.

Custom saved states

In Hosting → Advanced Overlay Settings → Custom saved states, register a state key, display name, TypeScript schema, initial JSON and optional presets. The same schema controls and role permissions apply as for packets. Organisers, admins, staff, stream hosts and globally permitted users always have access; optionally enable coordinators and referees, including the current match assignment restrictions. Eligible operators see Update custom state beside the packet sender. The editor starts with the saved value, offers typed fields and literal dropdowns, supports extra JSON keys, and acknowledges a save only after the database transaction commits and the update is broadcast.

For category picks that have not happened yet, use nullable or optional fields:

{
    team1PickedCategory: 'cat1' | 'cat2' | null;
    team2PickedCategory: 'cat1' | 'cat2' | null;
}

Initial JSON:

{
    "team1PickedCategory": null,
    "team2PickedCategory": null
}

Each named state belongs to one overlay. Its value survives refreshing, reconnecting, server restarts and selecting a different match. Reset it explicitly when preparing the next match; removing its definition permanently removes that saved JSON. Schema edits preserve the current JSON unless an explicit replacement is supplied, and reject incompatible existing values without discarding data.

Fetch from a custom overlay

The dynamic overlay does not need to consume custom state. Custom integrations can explicitly fetch all saved values without having permission to update them. The response contains an array of { guid, overlayGuid, stateKey, name, value, revision, updatedAt, updatedByGuid } snapshots; it does not expose role configuration or presets.

HTTP uses rest:read and normal overlay read access:

const response = await client.overlays.getCustomStates({ tournamentIdentifier: 'EUC26', overlayGuid });
if (!response.ok) throw new Error(await response.text());
const { states } = await response.json();
const categories = states.find((state) => state.stateKey === 'categorySelection')?.value;

The endpoint is GET /tournaments/:tournamentIdentifier/overlays/:overlayGuid/custom-states/values. For a one-time socket snapshot, use client.socket.overlays.getCustomStates({ tournamentGuid: 'EUC26', overlayGuid }). It accepts a ws:read token, checks normal overlay read access independently from state write permission, joins the overlay room before reading and returns { type: 'success', overlayGuid, states }.

For initial load, live updates and restoration on reconnect:

client.setAuthToken(token);
client.socket.connect();
const authentication = await client.socket.authenticate();
if (authentication.type === 'fail') throw new Error(authentication.error);

const unsubscribe = client.socket.overlays.observeCustomStates(
    { tournamentGuid: 'EUC26', overlayGuid },
    (states) => {
        const categories = states.find((state) => state.stateKey === 'categorySelection')?.value;
        renderCategories(categories);
    },
    (error) => console.error(error),
);

// Call unsubscribe() when the overlay integration is disposed.

The observer authenticates with the client's current token on reconnect, reloads the snapshot and handles updates that arrive during a snapshot request. Older revisions cannot overwrite newer ones. Removing and recreating the same key produces a new GUID, so late events for its old identity cannot resurrect removed state. The observer uses events rather than polling.

Update state

Merge a field into the latest saved object atomically. Nested objects merge recursively, arrays and primitives replace their field, and JSON null is a real value rather than a deletion marker. Undeclared keys remain valid additional data. The entire merged value must satisfy the schema:

const result = await client.socket.overlays.updateCustomState({
    tournamentGuid: 'EUC26',
    overlayGuid,
    stateGuid: categoriesState.guid,
    mode: 'merge',
    value: { team1PickedCategory: 'cat1', furtherCustomData: { displayed: true } },
});
if (result.type === 'fail') throw new Error(result.error);

For independent object field changes, merge mode can omit expectedRevision; simultaneous updates are serialized by a database row lock and use the latest saved object. Supply expectedRevision to detect stale edits. Replacing an entire value, or merging a top-level array, primitive or null, always requires the current revision:

const result = await client.socket.overlays.updateCustomState({
    tournamentGuid: 'EUC26',
    overlayGuid,
    stateGuid: categoriesState.guid,
    mode: 'replace',
    expectedRevision: categoriesState.revision,
    value: { team1PickedCategory: null, team2PickedCategory: null },
});

The editor uses replacement with revision protection so it can remove optional and extra keys as well as add them. If another operator updates the state, it retains your draft and asks you to load the latest revision. Conflicts return type: 'fail' with status: 409 over the socket, or HTTP 409. A preset can be supplied with presetId; its placeholders resolve against the current match before validation, and it can be combined with a JSON value.

validateCustomState previews the result without saving or broadcasting. HTTP integrations can use client.overlays.updateCustomState({ tournamentIdentifier, overlayGuid, stateGuid, mode, value, expectedRevision? }), which calls PATCH .../custom-states/:stateGuid/value. All writes require ws:write or rest:write, plus that state's current write permission. Definition management uses listCustomStates, createCustomState, editCustomState and deleteCustomState; definition edits and removal also require the latest revision.

Raw Socket.IO commands are getCustomOverlayStates, updateCustomOverlayState and validateCustomOverlayState, using MessagePack envelopes and acknowledgements. Updates emit customOverlayStateUpdate with the complete saved snapshot; registration, definition edits and removal emit customOverlayStatesChanged with { overlayGuid }. Custom state changes use these dedicated events rather than triggering a full match or dynamic overlay snapshot refresh.

On this page