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.storeandctx.assetsare 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
defaultConfigin 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 | undefinedctx.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
namespaceis 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.
Recommended Pattern
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()andsubscribe()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:
statusheaderstext()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.toolsblock. nameshould exactly match the manifest tool name.handler(args)may be synchronous or async.- Throw an
Errorfor invalid input, ambiguous matches, or unavailable state. - Return a compact result object. A
messagestring is recommended for user-facing summaries.
Design guidance:
- Prefer semantic actions such as
add_todo,complete_task, orappend_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:
latitudelongitudeaccuracy(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.storageNamespacectx.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.storagefor tiny private statectx.storefor shared JSON documentsctx.assetsfor 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.