Skip to content

Pack state & the data plane

Your overlay needs live game data (the score, who’s still alive, the death count) but a browser source can’t read your game. Pack state is the pipe between them: your Adapter publishes named values, the bridge validates and stores them, and your overlay subscribes. Declare a key whenever you want something happening in-game to show up on stream.

Each named slot of state is declared in pack.toml, written by exactly one authorised source, validated against an inline JSON Schema, and served to the overlay over the bridge’s data plane, the bearer-gated WS /rpc/subscribe WebSocket. The bridge, not the cloud, is the source of truth for pack-specific state, and the data plane is read-only to the browser: the overlay is a pure renderer and never writes.

[state.play_count]
writer = "adapter"
[state.play_count.params_schema]
type = "integer"
minimum = 0
  • writer (required): one of adapter, dispatcher, platform. The bridge enforces it: a write arriving from any other source is rejected.
  • params_schema (optional): inline JSON Schema; every write is validated against it the same way Invocation params are. The external-file schema = "path" form is rejected at load.
  • persist (optional, default false): see Persistent keys.

To make the overlay receive a key, also list it under [overlay.data] reads:

[overlay.data]
reads = ["play_count"]
WriterWho writesTypical use
adapterThe game-side Adapter, via the MPP state_write frameLive game telemetry (HP, score, run progress)
dispatcherThe bridge’s own dispatch corePending-command queue, composite roll results
platformThe bridge, re-published from its cloud subscriptionsSession info, incoming cue events

An Adapter writes a key by sending a state_write frame (see the MPP protocol):

{"mpp": 2, "type": "state_write", "key": "play_count", "value": 3}

There is no acknowledgement: an accepted write is silent, and the bridge fans the new value out to overlay subscribers. A write to an undeclared key, from the wrong writer, or failing the schema is a protocol error.

Pack-state JSON keys are camelCase end-to-end: one convention across the Adapter, the bridge, the JSON Schemas, and the overlay, so no case-translation seam exists. (Values carried verbatim from an external source, e.g. a raw platform payload nested inside a value, keep their upstream casing; only the pack-state envelope is normalised.)

By default a key lives only in bridge memory: a fresh Adapter process starts from scratch. Flag a key persist = true to make the bridge store its latest value durably (across both Adapter and bridge restarts) and replay it to the writer on reconnect (in the hello_ack frame’s persisted[] array).

[state.effect_count]
writer = "adapter"
persist = true
[state.effect_count.params_schema]
type = "integer"
minimum = 0
description = "Number of effects this Pack has applied, lifetime."

Use persist for accumulated tallies that have no cumulative in-game source (a lifetime effect count, milestone first-seen flags). Keys recomputed every tick from live game data should stay non-persistent.

Persistence is a per-key choice, not an all-or-nothing setting: the Doom Pack persists effect_count (a lifetime tally worth keeping across Adapter restarts) but deliberately leaves its deaths counter non-persistent, since that’s a per-session stat that should reset with the session.

Durability is best-effort across manifest changes (a stored value that fails the current schema is dropped, not migrated) and eventually durable (an async write, not commit-before-ack). Toggling persist changes the manifest hash like any other contract change.

A persist state_write may carry an opaque scope string identifying which run the value belongs to (e.g. a save-file identifier). The bridge never interprets it: it stores the tag, echoes it back on replay, and the Adapter decides resume-vs-reset. This gives you per-run durability without the platform learning anything game-specific.

{"mpp": 2, "type": "state_write", "key": "resets", "value": 3, "scope": "run:12345"}

activeQueues is a reserved, opt-in pack-state key: an array of queue names currently accepting redemptions. If your game has contexts where some Events make no sense (e.g. a cutscene or menu screen), have the Adapter write the queues that are currently valid:

{"mpp": 2, "type": "state_write", "key": "activeQueues", "value": ["effects", "chat"]}

You opt in by writing the key. The bridge recognises activeQueues even when your pack.toml declares no [state.activeQueues] slot, so no declaration is required. Its schema is platform-fixed (an array of strings, written by the Adapter): the reserved definition always wins, so declaring a slot cannot redefine the contract.

The platform then pauses (on the streaming platform’s side) every Reward whose Event’s queue is not in activeQueues, so viewers cannot redeem an effect that cannot currently apply. The pause is orthogonal to the broadcaster’s enable/disable switch and flips automatically with game context.

A Pack that never writes activeQueues opts out: every Reward stays live at all times. The platform reasons only about queues; your game’s notion of “context” stays private to your Adapter.

The platform’s overlay host loads your Pack’s overlay bundle, opens the data plane with a scoped read-only token, subscribes to every key in [overlay.data] reads, and delivers a snapshot on connect plus a push on every change. Your overlay code only declares which keys it reads; token handling and the WebSocket are the host’s job. See Overlays.