Skip to content

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:publish to this install in the current channel. Only to frames granted realtime.
  • Local relay events — any event name dispatched with localRelay:publish by 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. Ask overlay.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).

ActionPermissionSDK method
profile:readprofile:readgetUser()
identity:getidentity—
presence:read— (profile context only)—
fetch:externalfetch:externalfetch()
bot:postbot:postpostMessage()
embed:render—used by onCommand()
events:subscribe—on() (acknowledged, no effect)
command:register—onCommand() (acknowledged, no effect)
storage:get / storage:set / storage:delete / storage:liststorage:server or storage:user by scopestorage.*
storage:get-companion / storage:set-companion——
storage:localGet / storage:localSet / storage:localDeletestorage:localstorageLocal.*
log:append / log:readstorage:log—
realtime:publishrealtime—
realtime:publish-companionrealtime—
localRelay:publish——
members:listmembers:read—
media.embedmedia:embedmedia.embed()
voice.setGainvoicevoice.setGain()
clips.contributors, clips.list, clips.myFolders, clips.share, clips.unshare, clips.editClip, clips.deleteClip, clips.rate, clips.featuredclips:gallerygallery.*
game.telemetry.status / .bind / .unbindgame:telemetrygameTelemetry.*
game.saves.currentRun / .runs / .run / .profileStatsgame:savesgameSaves.*
overlay.contextoverlay:contextoverlay.context()
net.fetchnet:directnet.fetch() — desktop only
wallet.connect / wallet.balance / wallet.signTypedDatawalletwallet.*
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.