Appearance
postMessage Protocol
Warning: This is an advanced, internal protocol reference. Plugin authors should use the SDK methods (Dissent.fetch(), Dissent.storage.get(), etc.) instead of calling postMessage() directly.
Overview
The Dissent Plugin SDK talks to the host app with window.postMessage(). This page documents that wire protocol for debugging, and for the few actions that have no SDK wrapper.
All messages are plain objects with a type field. The host only answers requests whose event.source is the plugin's own frame.
Host → Plugin messages
dissent:init
Sent once when the frame loads. The SDK turns it into the PluginContext that Dissent.init() resolves with.
javascript
{
type: 'dissent:init',
theme: { '--background': '...', '--foreground': '...' }, // CSS custom properties
context: {
serverId: '...',
serverName: '...',
channelId: '...', // absent where there is no channel
channelName: '...',
pluginConfig: { /* admin config */ },
installId: '...', // this install's id
coreUrl: 'https://...', // the node the host talks to
hostHostname: 'app.dissent.chat',
contextType: 'channel', // channel | sidebar | profile | embed | personal
pluginSize: { cols: 1, rows: 2 }, // profile context only
placement: 'page', // personal context only: page | overlay
surface: '...', // overlay only
game: '...', // overlay only
permissions: ['storage:user'], // personal only: granted ∩ declared
capabilities: ['storage:get', '...'], // personal only: actions it can dispatch
overlayApi: 1, // personal only: overlay contract version
},
user: { // null unless profile:read or identity was granted
id: '...', // only with identity
displayName: '...',
avatarUrl: null,
permissions: ['profile:read'],
},
}In profile context, user is the profile owner, with an empty permissions array. A personal plugin's user is always null; its permissions arrive in context.permissions.
dissent:response
The answer to a dissent:request, echoing its id.
javascript
{ type: 'dissent:response', id: 'dissent-1-1757750000000', ok: true, data: { /* result */ } }Or on error:
javascript
{ type: 'dissent:response', id: 'dissent-1-1757750000000', ok: false, error: 'fetch:external not granted' }dissent:event
Delivered to a frame; plugins listen via Dissent.on(event, handler).
javascript
{ type: 'dissent:event', event: 'score:update', data: { home: 2, away: 1 } }What the host sends as dissent:event:
- Realtime events — any event name published with
realtime:publishto this install in the current channel. Only to frames grantedrealtime. - Local relay events — any event name dispatched with
localRelay:publishby another plugin frame in the same window.
Personal plugins also receive two data-free cues (pull again when they arrive):
game.saves.changed—data: { game }; a save file was rewritten. In-app pages only.overlay.context.changed—data: null; the overlay's game or panel layer changed. Askoverlay.context.
The host sends no message or member events (message:new, member:join, …) and, today, no command:invoked.
dissent:theme-update
Sent when the app's theme changes. The SDK re-emits it to Dissent.on('theme:change', …) handlers.
javascript
{ type: 'dissent:theme-update', theme: { '--background': '...', '--foreground': '...' } }Plugin → Host messages
dissent:escape
{ type: 'dissent:escape' } — the SDK sends this when Esc is pressed inside the frame, because the key never reaches the page around it. In the game overlay the host closes the overlay.
dissent:request
A request for data or an action. Must include a unique id; the response echoes it.
javascript
{
type: 'dissent:request',
id: 'dissent-1-1757750000000',
action: 'fetch:external',
params: { url: 'https://api.example.com/data', method: 'GET' },
}The host checks, in order: a personal plugin may only use its lane, and a profile-context frame only its own; the action must exist (unknown action: <name>); and the action's permission must be both granted by the user and still declared by the installed plugin.
Actions
These are every action the host's capability catalog accepts. "—" in the permission column means the provider checks its own conditions (for example, storage:* checks storage:server or storage:user depending on scope).
| Action | Permission | SDK method |
|---|---|---|
profile:read | profile:read | getUser() |
identity:get | identity | — |
presence:read | — (profile context only) | — |
fetch:external | fetch:external | fetch() |
bot:post | bot:post | postMessage() |
embed:render | — | used by onCommand() |
events:subscribe | — | on() (acknowledged, no effect) |
command:register | — | onCommand() (acknowledged, no effect) |
storage:get / storage:set / storage:delete / storage:list | storage:server or storage:user by scope | storage.* |
storage:get-companion / storage:set-companion | — | — |
storage:localGet / storage:localSet / storage:localDelete | storage:local | storageLocal.* |
log:append / log:read | storage:log | — |
realtime:publish | realtime | — |
realtime:publish-companion | realtime | — |
localRelay:publish | — | — |
members:list | members:read | — |
media.embed | media:embed | media.embed() |
voice.setGain | voice | voice.setGain() |
clips.contributors, clips.list, clips.myFolders, clips.share, clips.unshare, clips.editClip, clips.deleteClip, clips.rate, clips.featured | clips:gallery | gallery.* |
game.telemetry.status / .bind / .unbind | game:telemetry | gameTelemetry.* |
game.saves.currentRun / .runs / .run / .profileStats | game:saves | gameSaves.* |
overlay.context | overlay:context | overlay.context() |
net.fetch | net:direct | net.fetch() — desktop only |
wallet.connect / wallet.balance / wallet.signTypedData | wallet | wallet.* |
files:upload / files:getUrl / files:loadArrayBuffer / files:list / files:delete / files:releaseContext | — | — |
module:invoke | — | — |
There is no getMessages or channel:read action: message access was removed on 2026-08-15.
Example request flow:
Plugin sends:
javascript
{
type: 'dissent:request',
id: 'dissent-1-1757750000000',
action: 'fetch:external',
params: { url: 'https://api.example.com/data', method: 'GET' },
}Host responds with:
javascript
{
type: 'dissent:response',
id: 'dissent-1-1757750000000',
ok: true,
data: { status: 200, body: '{"result":"ok"}', content_type: 'application/json' },
}Request timeout
The SDK rejects a request that gets no response within 10 seconds (Dissent SDK: request "<action>" timed out), or 2 minutes for wallet.connect and wallet.signTypedData, which wait on the user, and 30 seconds for clips.editClip. It does not retry.
Debugging
Open your browser's DevTools console while a plugin is running:
javascript
// Spy on all messages
window.addEventListener('message', (event) => {
if (event.data?.type?.startsWith('dissent:')) {
console.log('Message:', event.data);
}
});When the server's developer mode is on, channel and sidebar plugins show a DevTools panel under the frame that logs these exchanges and proxied fetches.