Appearance
Overlay Contract — overlay.api: 1
What a plugin panel can rely on, and what it must not assume.
What "contract" means here
Once overlay.api: 1 is frozen, it is additive only. An action, field, event or rule that is in it is never removed or changed in shape — only deprecated behind a shim that keeps working. New capabilities arrive as additions, and a name that is ever retired is never reused to mean something else.
So you can build against everything on this page and expect it to keep working.
Declare the contract version in your manifest's overlay block. Absent means 1:
json
{ "overlay": { "api": 1, "games": ["slay-the-spire-2"], "surfaces": [ /* … */ ] } }A block declaring a version newer than the user's desktop app supports is not loaded. The launcher says a panel needs a newer Dissent rather than loading it to fail.
Who can have panels
A plugin the user installed personally, or one a server admin installed that the member switched on for themselves in My plugins → Over your game. Until a member switches a server plugin on, its panels appear nowhere — not drawn, and not listed in the launcher.
What a panel is told
dissent:init arrives when the frame finishes loading, and again after any reload:
jsonc
{
"type": "dissent:init",
"theme": { /* the app's theme tokens */ },
"user": null,
"context": {
"serverId": "", "serverName": "", "pluginConfig": {},
"contextType": "personal", // "channel" for a server install
"placement": "overlay",
"surface": "<your surfaces[].id>",
"game": "<catalog id>",
"installId": "<install id>",
"coreUrl": "<node origin>",
"capabilities": ["storage:get", "…"],
"permissions": ["storage:user", "…"],
"overlayApi": 1
}
}🔴 user is always null in a panel, whatever kind of install it is. The overlay holds no session — which is what makes "Dissent will never ask for your password in the overlay" true by construction rather than by policy.
To label something by person, use the sender_id on a realtime event. The node stamps it from the authenticated caller, so it cannot be forged; a name your plugin puts inside data is only your plugin's claim, and any member could publish a crafted event carrying someone else's.
Wait for init before you draw. If your page mounts a default view first, a Deck panel briefly shows your Wiki and downloads whatever the Wiki needs. Decide from ctx.surface.
What a panel may do — the overlay lane
The same list whether the plugin was installed personally or by an admin. A panel over a fullscreen game is where a user has the least context and the least chrome, so what runs there is decided by where it is drawn, not by how it got there.
| Permission | In a panel |
|---|---|
storage:user, storage:local | ✅ |
game:saves | ✅ |
overlay:context | ✅ |
net:direct | ✅ (desktop; the request leaves the user's own computer) |
friends:link | ✅ |
realtime | ✅ for server installs, in the channel the member chose |
fetch:external, bot:post, members:read, media:embed, files:*, clips:gallery, voice, wallet | ❌ |
A refused call answers <action> is not available in the overlay. The same plugin may use everything it is granted on its in-app page; only the panel is restricted.
Read ctx.capabilities and offer the features it lists, rather than calling and handling refusals.
overlay.context
javascript
const { game, surface, width, height, panelOpen } = await Dissent.overlay.context();Exactly those five fields, and refused anywhere but a panel.
🔴 width and height are the panel's CURRENT content size in CSS pixels — not the game's size, and not necessarily the size you declared. A user can resize and zoom a panel, so re-read them when overlay.context.changed fires instead of assuming your declared size.
panelOpen is the overlay's panel layer state. A frame that exists and sees panelOpen: true is visible, because closed panels are unloaded.
Messages
| Direction | Message | What it means |
|---|---|---|
| host → you | dissent:event overlay.context.changed (data: null) | The game or the panel layer changed. A cue — re-ask overlay.context |
| host → you | dissent:event <your realtime event> | A realtime event for your install, in the member's chosen channel (server installs). The message carries sender_id, stamped by the node |
| host → you | dissent:theme-update | The app's theme changed |
| you → host | dissent:escape | Closes the overlay. Both SDKs send this on Esc |
game.saves.changed is not delivered to panels — panels are hidden while the user plays and re-read on open.
Lifecycle
| Moment | What happens |
|---|---|
| Overlay opens over a game you target | A host-drawn Plugins launcher lists your panels. Nothing loads until the user opens one |
| The user opens a panel | Your frame loads and receives dissent:init |
| The user goes back to the game | The frame stays mounted but hidden — your JavaScript keeps running. Stop timers and animation when panelOpen is false |
| The overlay opens again | Shown, and overlay.context.changed fires |
| The user closes the panel (×) | Your frame is unloaded, and stays closed for that game |
| The game changes or exits | The list is rebuilt; panels for other games go |
⚠️ Renaming a surfaces[].id orphans every user's saved position and open state for it. The id is the layout key.
What the host draws, and you cannot
Host chrome carries your panel's title, your plugin's name, its origin host, a PLUGIN label and a close button. Above every plugin surface the host also draws its own accent frame and a Dissent badge, composited so no plugin can cover or imitate them.
There is no credential surface in the overlay at all — no login, no password field, no account recovery, no payment, and there never will be. Any such prompt over a game is fake.
Reserved names
surfaces[].layer accepts "panel" only. The names "hud" (an always-on surface drawn while the user plays) and chrome (a field on a surface) are reserved and refused today. Do not use either name for anything of your own.
Limits worth knowing
- A call unanswered in 10 seconds fails back with
the host did not answer. - Results are JSON only — a binary result is refused, never silently dropped.
- Panels: 1–8 per plugin, sizes 240–1600 × 160–1200, titles 1–60 characters.
- Games: 1–16 catalog ids per plugin.
- The manifest's overlay block refuses unknown fields outright.
See also: Overlay Panels for building one, and Permissions for what each permission means.