Electron Main ↔ Renderer IPC
The renderer is untrusted (see electron-core), so it reaches the main process only through a
preload contextBridge that exposes a small, typed API — never raw Node or ipcRenderer.
The pattern
Preload (preload.js) — the only place page and Node-ish power meet:
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
readConfig: () => ipcRenderer.invoke('config:read'), // request/response
saveConfig: (cfg) => ipcRenderer.invoke('config:save', cfg),
onProgress: (cb) => ipcRenderer.on('job:progress', (_e, p) => cb(p)), // stream
});
Main — validate every argument; you are the trust boundary:
ipcMain.handle('config:save', (_e, cfg) => {
if (!isValidConfig(cfg)) throw new Error('invalid config'); // never trust the renderer
return writeConfig(cfg);
});
Renderer — calls the typed surface only: await window.api.saveConfig(cfg).
Rules
invoke/handlefor request→response (returns a Promise);on/sendfor fire-and-forget streams (main→renderer or renderer→main).- Expose named methods, not channels — don't hand the page
ipcRenderer.send/invokedirectly (it could hit any channel). - Validate & authorize every payload in main; the channel name is not a capability check.
- Namespace channels (
config:read,job:progress); keep a typed.d.tsfor the bridge. - Do not use
@electron/remote(formerlyremote) — it punches through the boundary; it's removed from core for this reason. - Senders: prefer
ipcMain.handleover manualevent.reply; for renderer→renderer, route through main.
Guardrails
Minimal typed bridge; validate in main; no raw ipcRenderer/Node on window; no @electron/remote.
Pairs with electron-security (the surrounding hardening) and electron-core (the process model).