IPC & Context Bridge
The same main-to-renderer plumbing you know from Electron - handle, invoke, and a context bridge in a real isolated world.
Bunmaska’s IPC mirrors Electron’s. The main process exposes handlers; a preload script bridges a safe surface to the page; the page calls it. No remote module, no nodeIntegration foot-gun - context isolation is on, in a dedicated isolated world.
Main process: handle requests
import { ipcMain } from "bunmaska";
ipcMain.handle("dialog:open", async () => {
// ...do privileged work...
return "/Users/you/Documents/report.pdf";
});
ipcMain.handle("add", (_event, a: number, b: number) => a + b);
Preload: expose a safe surface
The preload runs in an isolated world and is injected verbatim, so keep it plain JavaScript. Two globals are available to it: contextBridge and __bunmaska.
// preload.js
contextBridge.exposeInMainWorld("api", {
add: (a, b) => __bunmaska.invoke("add", a, b),
openDialog: () => __bunmaska.invoke("dialog:open"),
});
Renderer: call it
const sum = await window.api.add(20, 22); // 42
const path = await window.api.openDialog();
The page can reach window.api, but it cannot reach the bridge internals, Bun, or the main process directly - exactly like Electron’s contextIsolation: true.
Why a context bridge at all?
Because your renderer loads web content, and web content should not have a direct line to the operating system. The bridge is the airlock: you decide precisely which functions cross it, and everything else stays sealed off in the main process.
The bridge is async-only by design - values are structured-cloned across the world boundary, just like Electron’s
contextBridge. If you’re porting code that used synchronousipcRenderer.sendSync, you’ll move it toinvoke.