Skip to main content

Bridge API Reference

Every Custom Widget and Dynamic Theme runs inside an isolated iframe. Inside that sandbox, monodit injects a global ctx object before your JavaScript executes.

This document is the canonical reference for that bridge.

For authoring workflow and manifest structure, see:


Overview

ctx is available immediately as window.ctx.

console.log(ctx.instanceId);
console.log(ctx.config);
console.log(ctx.theme.mode);

Broadly, the bridge is split into five groups:

  • Identity and configuration: instanceId, config
  • Private state: storage
  • Local cache: cache
  • Shared state: store
  • Asset storage: assets
  • Host integration: proxyFetch, openUrl, resize/config/token callbacks
  • AI integration: llm.registerTool
  • Gated host features: getCurrentLocation

Plan gating: ctx.store and ctx.assets are available for signed-in Free and Pro users. Guests use local guest storage only and cannot access shared buckets.


ctx.instanceId

The unique ID of the current widget or dynamic theme instance.

console.log(ctx.instanceId); // "550e8400-e29b-41d4-a716-446655440000"

Use it for:

  • Debugging
  • Generating isolated default storage keys
  • Distinguishing multiple instances of the same widget

ctx.config

The current configuration object for this instance.

const label = ctx.config.label ?? "Default";

Notes:

  • Populated from defaultConfig in the manifest plus user edits.
  • Updated in place when the host saves new settings.
  • Best consumed with ctx.onConfigChange(...).
ctx.onConfigChange((nextConfig) => {
console.log("Config changed:", nextConfig);
});

ctx.theme

Describes the currently active dashboard theme mode.

if (ctx.theme.mode === "dark") {
document.body.classList.add("dark");
}

Properties:

  • ctx.theme.mode: 'light' | 'dark'
  • ctx.theme.colors: reserved, currently an empty object

In most widgets/themes, CSS variables are the primary way to react to themes. For JavaScript-driven visuals, use ctx.onTokensChange(...).


ctx.storage

Private, per-instance key/value storage.

This is persisted inside the widget configuration itself, not inside shared buckets.

const count = Number(ctx.storage.get("count") ?? 0);
ctx.storage.set("count", count + 1);

API:

  • ctx.storage.get(key)unknown | undefined
  • ctx.storage.set(key, value)void

Use ctx.storage for:

  • Small UI state
  • Draft values
  • Per-instance toggles
  • State that should not be shared with other widgets

Avoid using it for:

  • Large JSON payloads
  • Cross-widget collaboration
  • File lists or asset metadata

ctx.cache

Browser-local, per-instance cache mediated by the host bridge.

This data lives in local IndexedDB only. It is not exported, not synced, and does not mutate the dashboard payload.

const snapshot = await ctx.cache.get("weather");
await ctx.cache.set("weather", data);
await ctx.cache.delete("weather");

API:

  • ctx.cache.get(key)Promise<unknown>
  • ctx.cache.set(key, value)Promise<void>
  • ctx.cache.delete(key)Promise<void>

Use ctx.cache for:

  • API response snapshots
  • Image or favicon caches
  • Local-first warm data used only to improve startup speed

Avoid using it for:

  • User settings that should follow the workspace
  • Shared state between widgets
  • Data that must survive export/import

ctx.store

Shared JSON document storage backed by Cloudflare D1 with a host-managed local IndexedDB cache.

await ctx.store.set({ text: "Hello" });
const doc = await ctx.store.get();

API:

  • ctx.store.get(namespace?)Promise<unknown>
  • ctx.store.set(value, namespace?)Promise<void>
  • ctx.store.subscribe(namespace, callback)() => void

Example:

async function boot() {
const initial = await ctx.store.get();
render(initial);

const unsubscribe = ctx.store.subscribe(null, (value) => {
render(value);
});

return unsubscribe;
}

Namespace resolution:

  • If namespace is passed explicitly, that value is used.
  • Otherwise the bridge falls back to ctx.config.storageNamespace.
  • If that is not set, it falls back to ctx.instanceId.

That means a widget is isolated by default, but can be re-connected to a shared bucket by the host UI.

Behavior Notes

  • Reads are stale-while-revalidate.
  • The bridge returns the cached IndexedDB value immediately when available.
  • After the initial read, the host checks server metadata and only re-downloads the full document when the bucket changed.
  • Writes are optimistic: the local cache updates first, then the backend sync runs asynchronously.
  • subscribe() reacts to updates from the same dashboard, other tabs in the same browser, and revalidation events when a newer server value is detected.
  • When a window regains focus, the host re-checks active document buckets so another browser or device can catch up without requiring a full remount.

Consistency Model

  • The server bucket is the source of truth.
  • The IndexedDB entry is a local mirror used for fast first paint and offline-friendly reads.
  • A document bucket is identified by its resolved namespace.
  • Widgets should assume that subscribe() can fire for:
    • local optimistic writes
    • write confirmations after the server responds
    • cache revalidation after the host detects a newer server value

Because of that, widgets should treat subscription payloads as authoritative document snapshots, not incremental patches.

For shared widgets, keep one logical JSON document per bucket and make your UI resilient to repeated full-document updates.

function applyValue(value) {
const text = value && typeof value.text === "string" ? value.text : "";
textarea.value = text;
}

async function load() {
const value = await ctx.store.get();
applyValue(value);
}

ctx.store.subscribe(null, applyValue);

Preventing Input Overwrites

If your widget has an editable control such as a textarea, do not blindly overwrite the DOM value every time subscribe() fires. A subscription update may arrive while the user is actively typing.

Recommended approach:

  • keep a local draft marker
  • track a monotonic field such as updatedAt
  • ignore older updates
  • defer applying remote snapshots while the focused input has unsaved local edits

Example:

const input = document.getElementById("note");
let draftVersion = 0;
let lastSavedVersion = 0;
let lastAppliedUpdatedAt = 0;
let pendingRemote = null;

function hasDirtyDraft() {
return draftVersion !== lastSavedVersion;
}

function getUpdatedAt(value) {
return value && typeof value.updatedAt === "number" ? value.updatedAt : 0;
}

function applySnapshot(value) {
const nextUpdatedAt = getUpdatedAt(value);
if (nextUpdatedAt && nextUpdatedAt < lastAppliedUpdatedAt) return;

const text = value && typeof value.text === "string" ? value.text : "";
if (input.value !== text) input.value = text;
if (nextUpdatedAt) lastAppliedUpdatedAt = nextUpdatedAt;
}

function flushPendingRemote() {
if (!pendingRemote || hasDirtyDraft()) return;
const snapshot = pendingRemote;
pendingRemote = null;
applySnapshot(snapshot);
}

input.addEventListener("input", () => {
draftVersion += 1;
const textSnapshot = input.value;
const payload = { text: textSnapshot, updatedAt: Date.now() };

window.clearTimeout(input._saveTimer);
input._saveTimer = window.setTimeout(async () => {
await ctx.store.set(payload);
lastSavedVersion = draftVersion;
lastAppliedUpdatedAt = Math.max(lastAppliedUpdatedAt, payload.updatedAt);
flushPendingRemote();
}, 180);
});

input.addEventListener("blur", flushPendingRemote);

ctx.store.subscribe(null, (value) => {
if (document.activeElement === input && hasDirtyDraft()) {
pendingRemote = value;
return;
}
applySnapshot(value);
});

ctx.assets

Bucket-aware asset storage backed by Cloudflare R2.

ctx.assets is designed for widgets that need binary files such as images, attachments, or media.

API:

  • ctx.assets.upload(file)Promise<string>
  • ctx.assets.list()Promise<AssetRecord[]>
  • ctx.assets.delete(objectKey)Promise<void>
  • ctx.assets.subscribe(callback)() => void

AssetRecord shape:

type AssetRecord = {
objectKey: string;
url: string;
filename: string;
contentType: string;
sizeBytes: number;
uploadedAt: string;
};

ctx.assets.upload(file)

Uploads a file into the current asset bucket and returns its public URL.

const url = await ctx.assets.upload(file);
console.log("Uploaded:", url);

ctx.assets.list()

Lists assets in the current bucket.

const assets = await ctx.assets.list();
const latest = assets[0];

ctx.assets.delete(objectKey)

Deletes a single object from the current bucket.

await ctx.assets.delete(asset.objectKey);

ctx.assets.subscribe(callback)

Subscribes to asset-bucket updates for the currently resolved bucket.

const unsubscribe = ctx.assets.subscribe(async () => {
const assets = await ctx.assets.list();
render(assets);
});

Bucket Resolution

For widgets that declare an asset storage contract, the host resolves the current bucket using a config key such as assetBucketId. If no bucket is configured, the bridge falls back to ctx.instanceId.

In practice:

  • new widgets start isolated
  • users can connect multiple widgets to the same asset bucket
  • list() and subscribe() automatically follow the resolved bucket

ctx.proxyFetch(url, options?)

Server-side proxied fetch for external URLs.

Use this instead of regular fetch() when browser CORS would block the request.

const res = await ctx.proxyFetch("https://api.example.com/data");
if (res.status === 200) {
const data = await res.json();
console.log(data);
}

Options:

await ctx.proxyFetch("https://api.example.com/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ok: true }),
});

Return shape:

  • status
  • headers
  • text()
  • json()

Restrictions:

  • Requires login
  • Private IPs and internal hostnames are blocked

ctx.openUrl(url, target?)

Requests that the host open a URL.

ctx.openUrl("https://example.com", "_blank");
ctx.openUrl("/dashboard", "_self");

Use this instead of direct navigation APIs inside the sandbox.


ctx.llm.registerTool(name, handler)

Registers a widget-scoped tool handler that monodit's in-app AI can invoke.

ctx.llm.registerTool("add_todo", async (args) => {
const text = String(args.text || "").trim();
if (!text) throw new Error("text is required");

const current = await ctx.store.get();
const items = Array.isArray(current) ? current : [];
await ctx.store.set(items.concat([{ id: crypto.randomUUID(), text }]));

return { message: `Added "${text}".` };
});

API:

  • ctx.llm.registerTool(name, handler)void

Rules:

  • Register only tools that are declared in the manifest's llm.tools block.
  • name should exactly match the manifest tool name.
  • handler(args) may be synchronous or async.
  • Throw an Error for invalid input, ambiguous matches, or unavailable state.
  • Return a compact result object. A message string is recommended for user-facing summaries.

Design guidance:

  • Prefer semantic actions such as add_todo, complete_task, or append_note.
  • Do not expose raw storage mutation primitives such as set_store_document.
  • Validate inputs at runtime even when the manifest includes an inputSchema.

ctx.getCurrentLocation()

Requests the user's current coordinates from the host application.

const coords = await ctx.getCurrentLocation();
console.log(coords.latitude, coords.longitude);

Return shape:

  • latitude
  • longitude
  • accuracy (optional)
  • timestamp

Requirements:

  • The widget manifest must declare "capabilities": ["geolocation"]
  • The host prompts the user for approval per widget instance before returning coordinates

Failure cases:

  • missing geolocation capability declaration
  • browser geolocation unavailable
  • browser-level permission denial
  • user declines the host approval prompt

This bridge is preferred over direct navigator.geolocation usage because sandboxed iframe geolocation behavior is inconsistent across browsers.


ctx.onResize(callback)

Fires when the widget/theme iframe is resized.

ctx.onResize((width, height) => {
console.log(width, height);
});

This is especially useful for:

  • Canvas rendering
  • Dense/sparse layouts
  • Recomputing chart scales

ctx.onConfigChange(callback)

Fires when the host saves a new config object for this instance.

ctx.onConfigChange((nextConfig) => {
rerender(nextConfig);
});

Use it whenever your widget depends on:

  • ctx.config.storageNamespace
  • ctx.config.assetBucketId
  • other manifest-defined config fields

If your widget subscribes to shared storage, re-check whether the active bucket/namespace changed and re-bind if needed.


ctx.onTokensChange(callback)

Fires when resolved theme tokens change.

ctx.onTokensChange((tokens) => {
draw(tokens.primaryColor);
});

Use this for JavaScript-driven visuals. For regular DOM/CSS widgets, CSS variables are usually enough.


ctx.setGlobalTokens(tokens)

Overrides dashboard design tokens from inside a widget or dynamic theme.

ctx.setGlobalTokens({ primaryColor: "#e11d48" });

This is primarily intended for Dynamic Themes or highly specialized widgets.


CSS Variables Injected Into the Sandbox

The bridge keeps these variables on :root up to date:

  • --primary-color
  • --background-color
  • --widget-background
  • --widget-border-radius
  • --font-family
  • --text-color

Typical usage:

body {
background: transparent;
color: var(--text-color);
font-family: var(--font-family, sans-serif);
}

button {
background: var(--primary-color);
}

Error Handling

Storage APIs reject with regular Error objects.

Common failure cases:

  • user is not on Pro
  • user is not authenticated
  • quota exceeded
  • invalid bucket or deleted bucket

Recommended pattern:

try {
await ctx.store.set({ ok: true });
} catch (err) {
console.error(err);
status.textContent = err.message;
}

Practical Guidance

Use:

  • ctx.storage for tiny private state
  • ctx.store for shared JSON documents
  • ctx.assets for files and media

If your widget is meant to participate in host-managed bucket sharing, declare a storage contract in the manifest. See Storage & Buckets and Custom Widget Authoring Guide.