FDO plugin code runs in two completely separate runtimes. Mixing assumptions between them is the most common source of plugin bugs. This page explains exactly what runs where, what globals are available, and how third-party imports work in each context.
The two runtimes
| Backend runtime | Iframe UI runtime |
|---|
| What runs here | Your plugin class, init(), handlers, storage, logging | render() output, renderOnLoad() output |
| Environment | Node.js / plugin process | Sandboxed browser iframe, React-hosted |
| npm imports | Supported — bundled into your plugin artifact | Not supported at runtime |
| DOM access | Not available | Full browser DOM |
window.* helpers | Not available | Available (host-injected) |
| Injected UI libraries | Not available | Available (host-injected) |
Backend runtime
The backend runtime is your plugin process — the Node.js environment where your plugin class runs. This is where all non-UI logic belongs.
What runs in the backend runtime:
- The
FDO_SDK class and your plugin subclass
init() — setup, handler registration, storage initialization
PluginRegistry.registerHandler(...) — all registered UI message handlers
PluginRegistry.useStore(...) — in-memory and JSON-backed storage
- All logging methods (
this.log, this.info, this.error, etc.)
- Privileged action helpers (
requestPrivilegedAction, requestScopedProcessExec, etc.)
- Any npm module you import and bundle into your plugin artifact
What is NOT available in the backend runtime:
- Browser DOM (
document, window, etc.)
- Host-injected UI libraries (Notyf, ACE, goober, etc.)
window.createBackendReq and other iframe-only helpers
// This is backend runtime code — correct
init(): void {
this.info("Setting up plugin");
const store = PluginRegistry.useStore("default");
store.set("initialized", true);
PluginRegistry.registerHandler("getData", async () => {
return store.get("initialized");
});
}
Iframe UI runtime
The iframe UI runtime is a sandboxed browser iframe managed by the FDO host. It is a browser environment, not Node.js. Your plugin’s render() output and renderOnLoad() output run here after the host processes and mounts them.
What runs in the iframe UI runtime:
- The HTML/UI string returned by
render()
- The on-load function string returned by
renderOnLoad()
- Event handlers attached via
onclick, addEventListener, and similar browser APIs
window.createBackendReq(...) calls to communicate back to the backend
Host-injected globals available in the iframe:
| Global | Purpose |
|---|
window.createBackendReq(handler, payload) | Send a message to a registered backend handler and receive the response |
window.waitForElement(selector, callback) | Run a callback once a DOM element matching the selector exists |
window.executeInjectedScript(src) | Dynamically inject a script into the iframe |
window.addGlobalEventListener(event, handler) | Attach a global event listener (managed by the host) |
window.removeGlobalEventListener(event, handler) | Remove a previously added global event listener |
window.applyClassToSelector(selector, className) | Apply a CSS class to all elements matching a selector |
Host-injected UI libraries:
| Library | Global | Notes |
|---|
| Goober (CSS-in-JS) | css, styled | Apply scoped styles via DOM class names |
| Notyf | Notyf | Toast notification library |
| Highlight.js | hljs | Syntax highlighting |
| ACE Editor | ace | Embedded code editor |
| Split Grid | Split | Resizable split panes |
| FontAwesome | Icon classes | Use <i class="fas fa-..."> in your HTML |
Goober and CSS-in-JS work in the iframe UI runtime because they operate on the browser DOM at runtime, not through module bundling. Use them to apply CSS class names in your rendered HTML — for example, assign a class generated by goober to a container element.
render(): string {
return `
<div>
<p>Hello from the iframe.</p>
<button onclick="
window.createBackendReq('getData', {})
.then(result => {
new Notyf().success('Got: ' + JSON.stringify(result));
});
">
Fetch from backend
</button>
</div>
`;
}
renderOnLoad(): string {
return `() => {
window.waitForElement("#my-editor", (el) => {
const editor = ace.edit(el);
editor.setTheme("ace/theme/monokai");
});
}`;
}
Why npm imports don’t work in the iframe UI runtime
Your plugin is a Node.js module. When you import something from npm, the bundler (webpack or similar) resolves and bundles that module into your plugin artifact at build time. That artifact runs in the backend runtime.
The iframe UI runtime is a browser environment that receives only the string output of render() and renderOnLoad(). There is no module loader in the iframe — it cannot resolve npm packages at runtime.
This means:
- You cannot call
import('some-npm-package') from inside your render output or on-load function.
- You cannot use
require('some-npm-package') at runtime in iframe code.
- Libraries that you import in your plugin class are available to your backend code but not to the iframe runtime.
What to do instead:
- Use host-injected globals documented by FDO/SDK (the table above).
- If you need a UI library not currently injected by the host, request it through FDO host injection policy — plugins cannot add arbitrary libraries to the iframe.
renderOnLoad(): string {
// hljs is injected by the host — use it directly
return `() => {
document.querySelectorAll("pre code").forEach((block) => {
hljs.highlightElement(block);
});
}`;
}
// DO NOT do this — this will not work in the iframe runtime
renderOnLoad(): string {
return `() => {
const hljs = require("highlight.js"); // ❌ fails at runtime
hljs.highlightAll();
}`;
}
Runtime safety rules
To keep backend and iframe concerns separate, follow these rules:
Safe in backend runtime only:
- Register handlers, read/write stores, write logs
- Call privileged action helpers (
requestPrivilegedAction, etc.)
- Import and use npm packages
Safe in iframe UI runtime only:
- Access
document and window
- Call
window.createBackendReq(...) to reach the backend
- Use injected globals (
Notyf, hljs, ace, Split, css, etc.)
- Attach
onclick handlers and DOM event listeners
Never assume iframe globals exist in:
- Plugin constructors
- Class field initializers
- Backend initialization paths
- Error fallback renderers (unless you are certain they run in the iframe path)
Runtime separation at a glance
┌─────────────────────────────────────┐ ┌────────────────────────────────────────┐
│ Backend Runtime │ │ Iframe UI Runtime │
│ (Node.js / plugin process) │ │ (sandboxed browser iframe) │
│ │ │ │
│ ● Your plugin class │ │ ● render() output (HTML/UI string) │
│ ● init() │ │ ● renderOnLoad() function string │
│ ● Registered handlers │ │ ● Browser DOM │
│ ● PluginRegistry.useStore(...) │ │ ● window.createBackendReq(...) │
│ ● Logging (this.info, etc.) │ │ ● window.waitForElement(...) │
│ ● npm bundled imports │ │ ● Notyf, hljs, ace, Split, goober │
│ ● Privileged action helpers │ │ ● FontAwesome icon classes │
└─────────────────────────────────────┘ └────────────────────────────────────────┘
│ IPC (UI_MESSAGE) ↑
└────────────────────┘
window.createBackendReq() in iframe
→ host routes → callHandler() in backend