Skip to content

Overlays

A redeem that only happens inside the game is invisible to viewers who can’t see your inputs, so paying for chaos feels flat. An overlay puts the action on stream: live counters, reveal reels, effect toasts, rendered right in OBS and fed real game data. Ship one with your Pack and viewers see exactly what their redeems did, which is what makes the spend feel worth it.

A Pack ships its overlay as stage-level widgets fed live game data over the data plane. The platform serves a single pack-agnostic overlay host at http://127.0.0.1:<bridge-port>/overlay; your Pack ships a JavaScript library the host loads, not a standalone web app.

[overlay]
dir = "overlay/dist" # optional; see resolver below
[overlay.data]
reads = ["play_count"] # pack-state keys your widgets subscribe to

Every entry in reads must match a declared [state.<key>].

The bridge serves your bundle at /pack-assets/<pack_id>/index.js. It must be a standard ES module with one optional named export:

export const overlayWidgets: Record<string, () => JSX.Element>

Components are Solid functional components. A missing export (or a missing index.js) renders an empty stage with a single info log: a broken bundle never crashes the overlay.

Where the bridge looks for files (3-tier resolver)

Section titled “Where the bridge looks for files (3-tier resolver)”

The first directory that exists wins:

  1. [overlay].dir from pack.toml (must resolve inside the Pack directory).
  2. <pack_dir>/overlay/dist/: built output.
  3. <pack_dir>/overlay/: raw files.

Commit your built overlay/dist/ to the Pack: the bridge does not build Packs at activation, so operators never need Node installed.

Tier 3 is a real choice, not just a fallback. If your overlay/index.js is already valid browser ES module code, the bridge serves it as-is from overlay/ with no build, no bundler, no dist/, and nothing to rebuild when you change it. That covers a hand-rolled widget or Solid authored without JSX. Reach for a framework and a build step (tier 2) only once the overlay grows enough to earn it. Most overlays are a few widgets and never do.

mobrule pack validate knows the difference. If it finds a build setup (overlay/package.json) but no built overlay/dist/index.js, it warns [OVERLAY-BUILD] so you never ship a framework overlay you forgot to build or commit. A vanilla tier-3 overlay has no package.json, so it never trips the warning.

The overlay host opens the data plane with a scoped read-only token (never the bridge’s full local token) and wraps your mounted widgets in a BridgeConnectionProvider. A widget subscribes to a key with useSubscription:

import { useBridge, useSubscription } from '<vendored>/overlay-data'
export function PlayCounter() {
const conn = useBridge()
const count = useSubscription('play_count', conn)
return <div class="counter">{count() ?? 0}</div>
}
export const overlayWidgets = { playCounter: PlayCounter }

You get a snapshot on mount and a push on every change. Token handling, the WebSocket, and reconnection are the host’s job; your Pack never touches a credential.

Because a Pack ships a lib, you cannot just open it in a browser. The starter template includes a dev harness: an index.html + dev entry that mounts your overlayWidgets against a mock data plane with canned state, giving you hot reload and a browser preview with no bridge and no token:

Terminal window
cd overlay
npm install
npm run dev

The harness lives under overlay/dev/ and is invisible to the lib-mode production build; npm run build && ls dist/ should yield index.js and nothing else. The minimal canonical example is pack-hello-world/overlay in the platform repo.

Three build setups all work (Vite + Solid, plain hand-rolled JS, esbuild); see this page’s tier-3 no-build-step note and the Doom overlay tutorial for a worked example, plus the MPP protocol for the data-plane wire format widgets consume.

  • Installing a Pack runs its code. The bridge origin’s CSP is locked down for overlay JS: script-src 'self' (only same-origin, pack-served bundles run; no inline or remote script), connect-src is loopback-only ('self' plus ws://localhost:* / http://localhost:*, no cloud origin), and frame-ancestors 'none'. A Pack’s overlay JS can never reach mobrule’s cloud or the STDB module from the browser, only the local bridge’s data plane.
  • That said, CSP is not a full sandbox: overlay JS still runs with the same DOM/browser APIs as any same-origin script. Only install Packs you trust.
  • The overlay is served without the bridge’s full local token; widgets only ever receive the scoped, read-only data-plane token. A leaked overlay credential can read pack state already rendered on stream, nothing more.