Plugin widgets
A plugin widget shows the value returned by a small JavaScript file you write, on one of a deck's display-only side keys or device-native display zones. The plugin polls, fetches or computes a short string, and DeckBridge renders it as centered text.
Plugins run in a dedicated worker thread, isolated from the network ACK loop, so a slow, looping, or throwing plugin can never stall image delivery or the web UI.
Where plugin files go
Drop your *.js files in the plugins/ directory next to DeckBridge's cache/settings
store:
| OS | Plugins directory |
|---|---|
| macOS | ~/Library/Caches/deckbridge/plugins/ |
| Linux | $XDG_CACHE_HOME/deckbridge/plugins/ (or ~/.cache/deckbridge/plugins/ if XDG_CACHE_HOME is unset) |
Each file is one plugin. The file name (e.g. worldclock.js) is what you assign to a
display zone.
Alternatively, pick Custom path… in the plugin dropdown and enter an absolute path to a plugin file anywhere on disk (e.g. a plugin kept in your dotfiles repo). Bare file names resolve against the plugins directory; absolute paths are used as-is.
The plugin contract
A plugin is an ES module with a default export object exposing an async fetch
function:
// <plugins dir>/myplugin.js
export default {
interval: 30_000, // optional; poll period in ms (see below)
async fetch(ctx) {
const res = await ctx.fetch('http://example.local/api/value');
const { value } = await res.json();
return `Label\n${value}`; // string → up to 4 centered lines; null → clear the key
},
};
The module is imported once when the key is configured, then fetch(ctx) is called
on every poll tick. It may be async (return a Promise) or synchronous.
ctx reference
| Property | Type | Description |
|---|---|---|
ctx.param | string | The per-key argument you set for this key. Empty string if none. Use it to parameterize one plugin file across keys (a location, an entity id, an offset…). |
ctx.fetch(url, init?) | (string, {method?, headers?, body?}) => Promise | Proxied HTTP request. Resolves to { ok, status, text(), json() } — text() returns the body string, json() parses it as JSON. http:// only (see Limitations). |
ctx.log(message) | (string) => void | Write a line to DeckBridge's log, tagged plugin:<filename>. |
Return value
string— rendered as up to 4 centered lines (split on\n); short single lines use a larger font. The string is capped at 256 characters before rendering.null(orundefined) — clears the key (shows nothing).- Anything else, or a thrown error, marks the key
ERR(see below).
Poll interval
The delay between polls, in milliseconds, is chosen as:
max(1000, perKeyOverride ?? plugin.interval ?? 5000)
- Default when unset: 5000 ms (5 s).
- Minimum enforced: 1000 ms (1 s) — smaller values are clamped up.
- A per-key interval override (if set for the key) takes precedence over the plugin's
own
interval.
Lifecycle & key states
The worker starts lazily when the first plugin key is configured and stops when no plugin keys remain. What the key shows reflects the plugin's state:
| On the key | Meaning |
|---|---|
… | No value yet — the plugin has been configured but its first fetch hasn't returned. |
| (your text) | The most recent string the plugin returned. |
| (blank) | The plugin returned null. |
ERR | The plugin failed to load, threw, returned a non-string, or was disabled (below). The failure is also warn-logged with the plugin name. |
Failure handling:
- A load failure (bad file, no default export, no
fetch) is terminal for that plugin — it stops and the key staysERRuntil the config changes. - A per-poll throw (e.g. a network blip) is transient — the key shows
ERR, but the plugin keeps polling and recovers on the next successful return. - If the worker hangs or crashes, DeckBridge terminates and respawns it. After
repeated respawns in a row (a crash-looping plugin), plugins are disabled (
ERR) until you change the key's configuration — this stops a broken plugin from burning CPU.
Limitations
- Plain
http://only. The DeckBridge runtime has no TLS, soctx.fetchrejectshttps://URLs. Talk to services overhttpon the LAN, or put a local http proxy in front of an https endpoint. - Do not use the global
fetchorWebSocket. They are removed from the plugin worker on startup — calling them throws (in the underlying runtime they would abort the whole process). Always usectx.fetchfor HTTP. ctx.fetchlimits. Each request gets 10 seconds in total, from sending it to the last byte of the response. Request and response bodies are each capped at 1 MiB. At most 4 requests run at the same time; another request fails at once and is not queued. A request over a limit rejects with anErrorthat names the limit, for examplefetch timeout,response exceeds 1048576 bytesortoo many concurrent requests (max 4). Requests still running when the plugin is removed, or when DeckBridge quits, are aborted.- Value length cap. Returned strings are truncated to 256 characters. Each display zone fits a few short lines.
- Absolute paths only for imports. DeckBridge imports your file by its on-disk path;
don't rely on
file://URLs or relative imports of sibling files.
For a value another tool already has, prefer the Push API over a polling plugin.
Security
A plugin is arbitrary code with the same trust level as the command widget — full
filesystem, process-spawn, and native-library access via the runtime. The worker is a
crash/CPU isolation boundary, not a capability sandbox. The web UI has no auth and
binds 127.0.0.1 unless you pass --bind, so anyone who can reach it could point a key
at a plugin file on the host. Plugin widgets are opt-in per key and meant for a trusted personal
LAN. Only run plugin files you wrote or trust.
Examples
Ready-to-use example plugins live in
examples/plugins/
in the repository:
hello.js— the smallest possible plugin: showsctx.param(or"hi") plus a tick counter, re-rendered on the default 5 s interval. Copy this file to start a new plugin.worldclock.js— showsHH:MMat a fixed UTC offset passed viactx.param(e.g.-5,+9,5.5). Pure computation, no network.countdown.js— counts down to a target timestamp, or up from a past one (prefixed+).ctx.paramis the target with an optional|label:2026-12-31T23:59,2026-08-01|vacation, or a bare15:30for today at that time. Runs at a 1 s interval and repaints only when the text changes.home-assistant.js— fetches a Home Assistant entity's state over the REST API (GET {baseUrl}/api/states/{entity}with a bearer token) and shows its state and unit.ctx.paramformat:baseUrl|token|entity(plain http — no TLS).