Overlays
Why you’d want this
Section titled “Why you’d want this”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.
Declaring an overlay
Section titled “Declaring an overlay”[overlay]dir = "overlay/dist" # optional; see resolver below
[overlay.data]reads = ["play_count"] # pack-state keys your widgets subscribe toEvery entry in reads must match a declared [state.<key>].
What the bundle exports
Section titled “What the bundle exports”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:
[overlay].dirfrompack.toml(must resolve inside the Pack directory).<pack_dir>/overlay/dist/: built output.<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.
No build step? Skip it.
Section titled “No build step? Skip it.”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.
Reading pack state from a widget
Section titled “Reading pack state from a widget”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.
Developing without a live bridge
Section titled “Developing without a live bridge”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:
cd overlaynpm installnpm run devThe 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.
Build recipes and full spec
Section titled “Build recipes and full spec”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.
Security notes
Section titled “Security notes”- 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-srcis loopback-only ('self'plusws://localhost:*/http://localhost:*, no cloud origin), andframe-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.
See also
Section titled “See also”- Pack state & the data plane: declaring the keys widgets read.
- Manifest reference:
[overlay]in the TOML tree. - MPP protocol: the wire format behind the data plane widgets subscribe to.