Skip to content

This site tracks the develop branch of the monorepo. Behaviour and protocol may change before the next release — see the source on GitHub.

doohnut-widget-sdk

TypeScript SDK for building Doohnut DOOH widgets.

Developer hub: developers.doohnut.comWidgets / SDK (English) and Portuguese hub.

Doohnut widgets are HTML/JS bundles rendered inside iframes by the Doohnut Player. This SDK wraps the postMessage protocol between your widget and the player, giving you typed events and a clean API instead of raw message handling.

Installation

bash
npm install doohnut-widget-sdk
# or
pnpm add doohnut-widget-sdk

Quick start

ts
import { DoohnutWidget } from 'doohnut-widget-sdk'

const widget = new DoohnutWidget({
  readyPayload: { name: 'my-widget', version: '1.0.0' },
})

widget.on('init', ({ config }) => {
  // config is the merged result from all config tiers
  document.getElementById('title')!.textContent = String(config.title ?? 'Hello')
})

widget.on('configUpdate', ({ config }) => {
  // hot-update your widget when config changes at runtime
  document.getElementById('title')!.textContent = String(config.title ?? 'Hello')
})

widget.on('frameEnter', () => {
  // frame became visible — start animations, resume video, etc.
})

widget.on('frameExit', () => {
  // frame left viewport — pause, clean up resources
})

Local development (Vite)

For Vite projects, the monorepo provides vite-plugin-doohnut-widget: a dev overlay at / with a mock host (INIT, FRAME_, CONFIG_UPDATE, STATE_, TIME_TICK), iframe URL under /__widget-app__/, package.json doohnut validation (including optional doohnut.config.fields shape checks), optional doohnut.devOverlay.presets and viewportPresets, a Viewport panel to resize the mock iframe, a visible mock STATE_* store, and optional DOOHNUT_PACK=1 tarball output (cross-env on Windows). See packages/vite-plugin-doohnut-widget/README.md and docs/widgets/development.md.

Layout and dimensions

Set doohnut.width and doohnut.height in package.json as the reference size (previews, thumbnails, and — unless you opt into responsive mode — the player’s fixed design canvas).

Set doohnut.aspectRatioPolicy:

  • mandatory (default when omitted) — the player lays out the iframe at your reference size and scales with contain inside the section. Use a fixed design plus inner scale-to-fit if needed.
  • optionalresponsive: the iframe fills the section; use fluid CSS (width/height 100%, flex/grid) so layout follows the real section size.

See docs/widgets/protocol.md and docs/widgets.md.

Constructor options

ts
const widget = new DoohnutWidget(options)
OptionTypeDefaultDescription
autoReadybooleantrueAutomatically send a READY message when the DOM is loaded. Set to false for manual control.
readyPayload{ name?: string; version?: string }Optional payload included in the automatic READY message. Used for logging in the player.

Events

Subscribe with .on(event, handler). The returned function unsubscribes.

ts
const unsub = widget.on('init', ({ config }) => {
  // handle config
})
unsub() // remove listener
EventFires whenHandler signature
initPlayer sends initial config (on load)({ config }) => void
configUpdateMerged config changes at runtime({ config }) => void
frameEnterFrame section becomes visible() => void
frameExitFrame section leaves viewport() => void
timeTickPlayer clock tick (reserved)() => void

init payload

ts
{
  config: Record<string, unknown> // merged config from all tiers
}

A frameEnter event will follow init as a separate event if the widget was already visible at handshake time. Always handle frameEnter independently — do not check for a special flag in init.

Methods

sendReady(payload?)

Tells the player your widget is initialized. The player re-sends INIT after receiving this.

Called automatically by the constructor when autoReady: true (the default). Only call manually when using autoReady: false.

ts
widget.sendReady({ name: 'my-widget', version: '1.0.0' })

sendError(message, code?)

Report an error to the player (logged server-side).

ts
widget.sendError('Failed to load data', 'FETCH_ERROR')

destroy()

Remove all event listeners and cancel pending state requests. Call when your widget unmounts.

ts
widget.destroy()

State API (experimental)

Note: The state API is not yet implemented on the host side. These methods will reject until host support is released. Expose them in your widget code now for forward compatibility.

ts
// Read state
const value = await widget.getState('widget', 'counter')

// Write state
await widget.setState('widget', 'counter', 42)

// Delete a key
await widget.clearState('widget', 'counter')

// Delete an entire scope
await widget.clearState('widget')

State scopes

ScopeDescription
widgetPer-widget, per-display. Resets when the widget reloads.
playlistShared across all widgets in the same playlist.
instanceDeprecated. Reserved for protocol compatibility; not used by the current player.
campaignShared across all playlists in the same campaign.

Config tiers

The config object you receive in init and configUpdate is already merged. The player resolves config from four tiers (lowest → highest priority):

TierSet by
Widget defaults (package.json)Widget author
Campaign configCampaign editor
Playlist configPlaylist editor
Frame/facet configFrame editor

Higher-tier values override lower-tier values for the same key. Your widget always receives the final merged result — you don't need to handle merging.

TypeScript

All types are bundled — no separate @types/ package needed.

ts
import type {
  IWidgetConfigUpdatePayload,
  IWidgetInitPayload,
  TWidgetStateScope,
  WidgetEventMap,
} from 'doohnut-widget-sdk'

Without the SDK

If you prefer raw postMessage handling, the protocol constants are also exported:

ts
import {
  createWidgetToHostMessage,
  isWidgetProtocolMessage,
  WIDGET_MSG_CONFIG_UPDATE,

  WIDGET_MSG_FRAME_ENTER,

  WIDGET_MSG_FRAME_EXIT,

  WIDGET_MSG_INIT,
  WIDGET_MSG_READY,
  WIDGET_PROTOCOL_SOURCE_HOST,
} from 'doohnut-widget-sdk'

window.addEventListener('message', (e) => {
  if (!isWidgetProtocolMessage(e.data)) { return }
  if (e.data.source !== WIDGET_PROTOCOL_SOURCE_HOST) { return }

  switch (e.data.type) {
    case WIDGET_MSG_INIT: {
      const { config } = e.data.payload as { config: Record<string, unknown> }
      applyConfig(config)
      break
    }
    case WIDGET_MSG_FRAME_ENTER:
      // widget is visible — start animations, resume video, etc.
      break
    case WIDGET_MSG_FRAME_EXIT:
      // widget left viewport — pause, release resources
      break
    case WIDGET_MSG_CONFIG_UPDATE: {
      const { config } = e.data.payload as { config: Record<string, unknown> }
      applyConfig(config)
      break
    }
  }
})

// Pull model: send READY so the host responds with INIT then FRAME_ENTER (if already visible).
window.parent.postMessage(createWidgetToHostMessage(WIDGET_MSG_READY, { name: 'my-widget' }), '*')

For the full wire-level protocol specification see docs/widgets/protocol.md.

License

MIT

Released under the same terms as the Doohnut platform.