Building Extensions

Before building an extension, you need a plugin initialized with npm run overseer -- init. Extensions live inside the extensions/ directory of your plugin and are referenced from the plugin’s root manifest.json.

Overseer picks up plugin changes at startup. Restart Overseer after modifying any manifest or asset files to see your changes.

Iframe extensions

Iframe mode embeds a URL in a standard HTML iframe. No SDK access — use this when you don’t need inter-extension events.

{
  "id": "@yourname/5e-compendium",
  "version": "1.0.0",
  "category": "Character Sheets",
  "label": "5e Compendium",
  "description": "Quick reference for spells, monsters, and items.",
  "icon": { "$ref": "assets/icon.png" },
  "source": "iframe",
  "iframe": {
    "src": "https://www.dndbeyond.com",
    "style": { "width": "100%", "height": "100%" },
    "frameBorder": "0",
    "allow": "autoplay",
    "allowFullScreen": true
  }
}

The iframe object accepts standard HTML iframe attributes: src, style, allow, allowFullScreen, referrerPolicy, frameBorder, title, sandbox.

Bundled extensions

Bundled mode ships HTML, CSS, and JavaScript files with your plugin. Set source to a $ref pointing at your HTML entry point:

{
  "id": "@yourname/initiative-tracker",
  "version": "1.0.0",
  "category": "Combat",
  "label": "Initiative Tracker",
  "description": "Track turn order during encounters.",
  "icon": { "$ref": "assets/icon.png" },
  "source": { "$ref": "index.html" },
  "permissions": {
    "send": ["turn-changed"],
    "receive": ["combat-start"]
  }
}

The $ref path resolves relative to the manifest file. Your HTML loads in the tile with full SDK access — import from @overseer-studio/sdk to use it.

Directory structure

extensions/initiative-tracker/
  manifest.json
  index.html
  style.css
  app.js
  assets/
    icon.png

No build tools are required. Write plain HTML, CSS, and JavaScript and reference them from your entry point. If you use a build tool (Vite, webpack, etc.), run the build first so your output files are up to date before restarting Overseer.

Example: minimal bundled extension

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <style>
    body { font-family: sans-serif; padding: 16px; color: #eee; background: transparent; }
    .roll { font-size: 2rem; text-align: center; margin: 1rem 0; }
  </style>
</head>
<body>
  <div class="roll" id="result">--</div>
  <button onclick="roll()">Roll d20</button>

  <script type="module">
    import { onReady, sendEvent } from '@overseer-studio/sdk';

    onReady(() => {
      // SDK is ready
    });

    window.roll = function roll() {
      const result = Math.floor(Math.random() * 20) + 1;
      document.getElementById('result').textContent = result;
      sendEvent('dice-result', { result, sides: 20 });
    };
  </script>
</body>
</html>

Webview extensions

Webview mode loads an external URL with full SDK access. Use this when a hosted site needs to integrate with Overseer’s event system.

{
  "id": "@yourname/dice-roller",
  "version": "1.0.0",
  "category": "Dice",
  "label": "Dice Roller",
  "description": "A dice roller that broadcasts results to other extensions.",
  "icon": { "$ref": "assets/icon.png" },
  "source": "webview",
  "webview": {
    "src": "https://dice.example.com/room/{config.roomId}"
  },
  "config": {
    "roomId": {
      "type": "string",
      "required": true,
      "label": "Room ID",
      "placeholder": "Enter your room ID"
    }
  },
  "permissions": {
    "send": ["dice-result"],
    "receive": ["dice-roll"]
  }
}

Webview also supports injectJavaScript to inject a script into the loaded page. Overseer prompts the user before running injected scripts.

URL templates

Insert dynamic values into source URLs with {path.to.value} syntax:

TemplateResolves to
{config.fieldName}The user’s value for that config field.
{src}The resolved base URL (from baseUrl).

Templates can be combined: "{src}/room/{config.roomId}". If a variable has no value, the placeholder is left as-is. Use defaultValue on config fields to avoid blank URLs.

Adding configuration fields

Config fields create inputs in the tile editor. Users fill them in when they assign the extension to a tile. Values are available in URL templates and in SDK lifecycle events.

{
  "config": {
    "url": {
      "type": "url",
      "required": true,
      "label": "Character Sheet URL",
      "placeholder": "https://www.dndbeyond.com/characters/12345",
      "defaultValue": "https://www.dndbeyond.com"
    },
    "theme": {
      "type": "select",
      "label": "Theme",
      "defaultValue": "dark",
      "options": [
        { "label": "Dark", "value": "dark" },
        { "label": "Light", "value": "light" }
      ]
    }
  }
}

Available field types: string, url, number, checkbox, color, json, select, file. See the Extension Manifest reference for details on each type.

Using the event system

Bundled and webview extensions can send and receive events through the SDK. Declare which events your extension uses in the manifest first:

{
  "permissions": {
    "send": ["dice-roll"],
    "receive": ["dice-result"]
  }
}

Then import and use the SDK functions in your code:

import { subscribe, sendEvent } from '@overseer-studio/sdk';

subscribe('dice-result', (payload) => {
  console.log('Received roll:', payload.result);
});

sendEvent('dice-roll', { equation: '2d6+3' });

For the complete API, see the SDK Reference.