Skip to main content
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 runtimeIframe UI runtime
What runs hereYour plugin class, init(), handlers, storage, loggingrender() output, renderOnLoad() output
EnvironmentNode.js / plugin processSandboxed browser iframe, React-hosted
npm importsSupported — bundled into your plugin artifactNot supported at runtime
DOM accessNot availableFull browser DOM
window.* helpersNot availableAvailable (host-injected)
Injected UI librariesNot availableAvailable (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:
GlobalPurpose
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:
LibraryGlobalNotes
Goober (CSS-in-JS)css, styledApply scoped styles via DOM class names
NotyfNotyfToast notification library
Highlight.jshljsSyntax highlighting
ACE EditoraceEmbedded code editor
Split GridSplitResizable split panes
FontAwesomeIcon classesUse <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);
        });
    }`;
}

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