Web to Desktop
Preload & IPC
Lesson 2 of 5
What you'll learn
- Expose a minimal API to the page with a preload script and
contextBridge - Round-trip requests with
ipcRenderer.invokeandipcMain.handle - Understand why
nodeIntegrationstays off andcontextIsolationstays on
If the renderer can't touch Node, how does your UI ever save a file? Through a preload script — a file that runs inside the renderer before the page loads, with access to a safe subset of Electron APIs (like ipcRenderer). The preload uses contextBridge to publish a small, hand-picked API onto the page's window:
// preload.js
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {
readConfig: () => ipcRenderer.invoke("config:read"),
saveNote: (text) => ipcRenderer.invoke("note:save", text),
});
The page sees only window.api.readConfig() and window.api.saveNote(text) — not ipcRenderer itself, and certainly not fs. On the other side, main registers a handler per channel and returns a value (or a promise); invoke resolves with it:
// main.js
const { ipcMain } = require("electron");
const fs = require("node:fs/promises");
ipcMain.handle("config:read", () => fs.readFile(configPath, "utf8"));
ipcMain.handle("note:save", (event, text) => fs.writeFile(notePath, text));
invoke/handle is the request/response pair you'll use for almost everything. (There's also fire-and-forget send/on, and webContents.send for main-to-renderer pushes.)
Why the walls stay up
contextIsolation: true (default since Electron 12) runs your preload in a separate JavaScript world from the page, so page code can't reach into preload internals or tamper with the bridge — contextBridge copies values across the boundary rather than sharing objects. nodeIntegration: false (default since Electron 5) keeps require out of the page entirely, and sandbox: true puts the renderer in Chromium's OS-level sandbox. Turning any of these off to "make things easier" converts every XSS bug in your UI into arbitrary code execution on the user's machine.
Expose functions, not modules
Never bridge whole objects like ipcRenderer or fs to the page — that hands over the exact power the sandbox exists to withhold. Expose one named function per operation, and validate arguments (and event.senderFrame) in main.
The challenge is a JS model of invoke/handle: requests correlated by id over two emitters, wrapped in a frozen bridge API — the same shape Electron gives you for real.
Run it. Each invoke gets a correlation id, main's handler answers on that id, and the page only ever sees the frozen api object.
What does contextBridge.exposeInMainWorld actually give the page?
Next: putting the "desktop" in desktop app — windows, menus, tray icons, dialogs, and deep links.
Saved on this device. Sign in to sync your progress everywhere.