Electron
kkRPC provides type-safe bidirectional RPC communication for Electron applications. It supports three communication patterns:
- Renderer ↔ Main (via IPC)
- Main ↔ Utility Process (via postMessage)
- Renderer → External Process (via Main relay)
Package Structure
Section titled “Package Structure”Electron has TWO separate sub-packages. This separation exists because different Electron processes run in different environments:
| Package | Import Path | Environment | Use Case |
|---|---|---|---|
| electron-ipc | kkrpc/electron-ipc | Browser-like (Renderer) | Renderer ↔ Main communication |
| electron | kkrpc/electron | Node.js (Main, Utility) | Main ↔ Utility Process communication |
Why Two Packages?
Section titled “Why Two Packages?”Environment Separation: Electron’s Renderer process runs in a Chromium sandbox with contextIsolation: true. It has NO access to Node.js APIs and requires ipcRenderer to be exposed via contextBridge. The kkrpc/electron-ipc package is designed specifically for this browser-like environment.
Main and Utility Process: Both run in full Node.js environments with access to utilityProcess, child_process, etc. The kkrpc/electron package includes Node.js-specific adapters.
Where to Import From
Section titled “Where to Import From”// Renderer Process (Chromium sandbox)// Utility Process (Node.js)import { ElectronUtilityProcessChildIO, ElectronUtilityProcessIO } from "kkrpc/electron"// Main Process (Node.js)import { ElectronIpcMainIO, ElectronIpcRendererIO, RPCChannel } from "kkrpc/electron-ipc"Architecture Overview
Section titled “Architecture Overview”┌─────────────────┐ IPC ┌─────────────────┐ postMessage ┌─────────────────┐│ Renderer │◄────────────►│ Main │◄───────────────►│ Utility Process││ (Chromium) │ kkrpc-ipc │ (Node.js) │ │ (Node.js) ││ │ │ │ │ ││ ElectronIpc │ │ ElectronIpc │ │ ElectronUtility ││ RendererIO │ │ MainIO │ │ ProcessChildIO ││ │ │ │ │ ││ kkrpc/ │ │ kkrpc/electron- │ │ kkrpc/electron ││ electron-ipc │ │ ipc │ │ │└─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ spawn(stdio) │ │ │ ┌────────▼────────┐ │ │ External Node │ │ │ Process │ │ │ (via relay) │ │ └─────────────────┘ │ │ Custom Channel (via relay) └──────────────────────────────────────────────┐ │ ┌───────────▼───────────┐ │ External Process │ │ (Node/Bun/Deno) │ │ via createRelay │ └───────────────────────┘Preload Script Setup
Section titled “Preload Script Setup”First, expose ipcRenderer via contextBridge in your preload script. This is REQUIRED for the renderer to communicate with main.
Option 1: Secure IPC Bridge (Recommended)
Section titled “Option 1: Secure IPC Bridge (Recommended)”Use the built-in createSecureIpcBridge factory for automatic channel whitelisting. This factory accepts the ipcRenderer from Electron and returns a secured version:
import { contextBridge, ipcRenderer } from "electron"import { createSecureIpcBridge } from "kkrpc/electron-ipc"
const securedIpcRenderer = createSecureIpcBridge({ ipcRenderer, channelPrefix: "kkrpc-"})
contextBridge.exposeInMainWorld("electron", { ipcRenderer: securedIpcRenderer})This approach:
- Only allows IPC communication on channels starting with
"kkrpc-" - Blocks any other IPC channels (logged as warnings)
- Follows Electron security best practices
- Works with any Electron version (no direct Electron dependency in kkrpc)
You can also whitelist specific channels:
import { contextBridge, ipcRenderer } from "electron"import { createSecureIpcBridge } from "kkrpc/electron-ipc"
const securedIpcRenderer = createSecureIpcBridge({ ipcRenderer, allowedChannels: ["kkrpc-ipc", "kkrpc-worker-relay"]})
contextBridge.exposeInMainWorld("electron", { ipcRenderer: securedIpcRenderer})Option 2: Manual Setup
Section titled “Option 2: Manual Setup”If you need custom behavior, set up the bridge manually:
import { contextBridge, ipcRenderer } from "electron"
contextBridge.exposeInMainWorld("electron", { ipcRenderer: { send: (channel: string, ...args: any[]) => ipcRenderer.send(channel, ...args), on: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.on(channel, listener), off: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.off(channel, listener) }})API Definition
Section titled “API Definition”Define your API types that will be shared across processes:
// Types shared across all processesexport interface MainAPI { showNotification(message: string): Promise<void> getAppVersion(): Promise<string> pingRenderer(message: string): Promise<string>}
export interface RendererAPI { showAlert(message: string): Promise<void> getRendererInfo(): Promise<{ userAgent: string language: string platform: string }>}
export interface WorkerAPI { add(a: number, b: number): Promise<number> multiply(a: number, b: number): Promise<number> getProcessInfo(): Promise<{ pid: number version: string platform: string }>}Pattern 1: Renderer ↔ Main IPC
Section titled “Pattern 1: Renderer ↔ Main IPC”This is the most common pattern - communicating between the UI (Renderer) and the backend (Main).
Main Process
Section titled “Main Process”import { app, BrowserWindow, ipcMain } from "electron"import { ElectronIpcMainIO, RPCChannel } from "kkrpc/electron-ipc"import type { MainAPI, RendererAPI } from "./api"
const mainAPI: MainAPI = { showNotification: async (message: string) => { console.log(`[Main] Notification: ${message}`) win?.webContents.send("notification", message) }, getAppVersion: async () => app.getVersion(), pingRenderer: async (message: string) => { // Call renderer methods const info = await rendererAPI.getRendererInfo() return `Renderer responded! Platform: ${info.platform}` }}
// Create windowconst win = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, "preload.js"), contextIsolation: true, nodeIntegration: false }})
// Setup IPCconst ipcIO = new ElectronIpcMainIO(ipcMain, win.webContents)const ipcRPC = new RPCChannel<MainAPI, RendererAPI>(ipcIO, { expose: mainAPI })const rendererAPI = ipcRPC.getAPI()Renderer Process
Section titled “Renderer Process”import { ElectronIpcRendererIO, RPCChannel } from "kkrpc/electron-ipc"import type { MainAPI, RendererAPI } from "./api"
const rendererAPI: RendererAPI = { showAlert: async (message: string) => { alert(message) console.log("[Renderer] Alert shown:", message) }, getRendererInfo: async () => ({ userAgent: navigator.userAgent, language: navigator.language, platform: navigator.platform })}
// Setup IPC (uses window.electron.ipcRenderer from preload)const ipcIO = new ElectronIpcRendererIO()const ipcRPC = new RPCChannel<RendererAPI, MainAPI>(ipcIO, { expose: rendererAPI })const mainAPI = ipcRPC.getAPI()
// Call main process methodsawait mainAPI.showNotification("Hello from renderer!")const version = await mainAPI.getAppVersion()Pattern 2: Main ↔ Utility Process
Section titled “Pattern 2: Main ↔ Utility Process”Utility Process is Electron’s way to run Node.js code in a separate process. This is different from the Renderer process - it has full Node.js access.
Main Process
Section titled “Main Process”import { utilityProcess } from "electron"import { ElectronUtilityProcessIO, RPCChannel } from "kkrpc/electron"import type { MainAPI, WorkerAPI } from "./api"
// Fork utility process (separate Node.js process)const workerPath = path.join(__dirname, "./worker.js")const workerProcess = utilityProcess.fork(workerPath)
// Setup communicationconst workerIO = new ElectronUtilityProcessIO(workerProcess)const workerRPC = new RPCChannel<MainAPI, WorkerAPI>(workerIO, { expose: mainAPI })const workerAPI = workerRPC.getAPI()
// Call worker methodsconst result = await workerAPI.add(2, 3) // 5const info = await workerAPI.getProcessInfo()console.log(`Worker PID: ${info.pid}`)Utility Process (Worker)
Section titled “Utility Process (Worker)”import { ElectronUtilityProcessChildIO, RPCChannel } from "kkrpc/electron"import type { MainAPI, WorkerAPI } from "./api"
const workerAPI: WorkerAPI = { add: async (a: number, b: number) => a + b, multiply: async (a: number, b: number) => a * b, getProcessInfo: async () => ({ pid: process.pid, version: process.version, platform: process.platform })}
const io = new ElectronUtilityProcessChildIO()const rpc = new RPCChannel<WorkerAPI, MainAPI>(io, { expose: workerAPI })const mainAPI = rpc.getAPI()
// Call back to main processawait mainAPI.showNotification("Hello from worker!")Pattern 3: Renderer → External Process (via Relay)
Section titled “Pattern 3: Renderer → External Process (via Relay)”Connect the Renderer directly to an external Node.js/Bun/Deno process through Main using a transparent relay.
Renderer (IPC) → Main (relay) → External Node Process (stdio)Main Process (sets up relay)
Section titled “Main Process (sets up relay)”import { spawn } from "child_process"import { createRelay, NodeIo } from "kkrpc"import { ElectronIpcMainIO } from "kkrpc/electron-ipc"
// Spawn external Node.js processconst workerPath = path.join(__dirname, "./external-worker.js")const workerProcess = spawn("node", [workerPath])
// Create transparent relay: IPC channel "external-relay" <-> stdioconst relay = createRelay( new ElectronIpcMainIO(ipcMain, win.webContents, "external-relay"), new NodeIo(workerProcess.stdout, workerProcess.stdin))
// Cleanupapp.on("window-all-closed", () => { relay.destroy() workerProcess.kill()})Renderer Process (uses relay)
Section titled “Renderer Process (uses relay)”import { ElectronIpcRendererIO, RPCChannel } from "kkrpc/electron-ipc"import type { ExternalAPI } from "./api"
// Connect via the relay channel (NOT the default "kkrpc-ipc")const io = new ElectronIpcRendererIO("external-relay")const rpc = new RPCChannel<{}, ExternalAPI>(io)const externalAPI = rpc.getAPI()
// Calls go directly to external process through Main's relayconst result = await externalAPI.heavyCalculation(1000)External Process
Section titled “External Process”import { NodeIo, RPCChannel } from "kkrpc"import type { ExternalAPI } from "./api"
const externalAPI: ExternalAPI = { heavyCalculation: async (n: number) => { // Heavy CPU work here return n * n }}
const io = new NodeIo(process.stdin, process.stdout)const rpc = new RPCChannel<ExternalAPI, {}>(io, { expose: externalAPI })Adapter Reference
Section titled “Adapter Reference”| Adapter | Import Path | Runs In | Communication | Protocol |
|---|---|---|---|---|
ElectronIpcMainIO | kkrpc/electron-ipc | Main | Main ↔ Renderer | ipcMain |
ElectronIpcRendererIO | kkrpc/electron-ipc | Renderer | Renderer ↔ Main | ipcRenderer |
ElectronUtilityProcessIO | kkrpc/electron | Main | Main ↔ Utility | postMessage |
ElectronUtilityProcessChildIO | kkrpc/electron | Utility | Utility ↔ Main | postMessage |
Complete Working Example
Section titled “Complete Working Example”A complete working example with all three patterns is available in examples/electron-demo:
cd examples/electron-demonpm installnpm run devThe demo showcases:
- Pattern 1: Renderer → Main IPC with bidirectional calls
- Pattern 2: Main → Utility Process delegation
- Pattern 3: Renderer → External Node Process via relay
- Multiple Channels: Using separate IPC channels for different purposes
Common Patterns
Section titled “Common Patterns”Multiple Channels
Section titled “Multiple Channels”You can create multiple IPC channels for different purposes:
// Default channel for Main APIconst defaultIO = new ElectronIpcMainIO(ipcMain, win.webContents)const mainRPC = new RPCChannel<MainAPI, RendererAPI>(defaultIO, { expose: mainAPI })
// Separate channel for external process relayconst externalIO = new ElectronIpcMainIO(ipcMain, win.webContents, "external-channel")const externalProcess = spawn("node", ["./worker.js"])createRelay(externalIO, new NodeIo(externalProcess.stdout, externalProcess.stdin))// Default channel for Main APIconst mainIO = new ElectronIpcRendererIO()const mainRPC = new RPCChannel<RendererAPI, MainAPI>(mainIO, { expose: rendererAPI })
// Separate channel for External APIconst externalIO = new ElectronIpcRendererIO("external-channel")const externalRPC = new RPCChannel<{}, ExternalAPI>(externalIO)Cleanup
Section titled “Cleanup”Always clean up resources when windows close:
app.on("window-all-closed", () => { // Destroy all RPC channels ipcRPC?.destroy() workerRPC?.destroy()
// Kill all child processes workerProcess?.kill()
if (process.platform !== "darwin") { app.quit() }})Troubleshooting
Section titled “Troubleshooting””window.electron is undefined”
Section titled “”window.electron is undefined””Make sure your preload script is correctly exposing ipcRenderer. The recommended approach:
// preload.ts - Use the secure bridge factoryimport { contextBridge, ipcRenderer } from "electron"import { createSecureIpcBridge } from "kkrpc/electron-ipc"
const securedIpcRenderer = createSecureIpcBridge({ ipcRenderer, channelPrefix: "kkrpc-"})
contextBridge.exposeInMainWorld("electron", { ipcRenderer: securedIpcRenderer})Or manually:
// preload.ts - Manual setupimport { contextBridge, ipcRenderer } from "electron"
contextBridge.exposeInMainWorld("electron", { ipcRenderer: { send: (channel: string, ...args: any[]) => ipcRenderer.send(channel, ...args), on: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.on(channel, listener), off: (channel: string, listener: (...args: any[]) => void) => ipcRenderer.off(channel, listener) }})“Cannot find module ‘kkrpc/electron’”
Section titled ““Cannot find module ‘kkrpc/electron’””Make sure you’re importing from the correct package:
// WRONG - Renderer can't use Node.js packagesimport { ElectronUtilityProcessIO } from "kkrpc/electron" // ❌
// CORRECT - Renderer uses electron-ipcimport { ElectronIpcRendererIO } from "kkrpc/electron-ipc" // ✓Channel Conflicts
Section titled “Channel Conflicts”Each ElectronIpcMainIO instance must have a unique channel name if you create multiple:
// These will conflict!const io1 = new ElectronIpcMainIO(ipcMain, win.webContents) // Uses "kkrpc-ipc"const io2 = new ElectronIpcMainIO(ipcMain, win.webContents) // Also "kkrpc-ipc" ❌
// Use unique channel namesconst io1 = new ElectronIpcMainIO(ipcMain, win.webContents, "channel-1")const io2 = new ElectronIpcMainIO(ipcMain, win.webContents, "channel-2") // ✓Features
Section titled “Features”- Type-safe: Full TypeScript support across all process boundaries
- Bidirectional: All processes can expose and call APIs
- Secure: Works with
contextIsolation: true(recommended) - Flexible: Three communication patterns (IPC, Utility Process, Relay)
- Nested APIs: Full support for nested method calls like
api.math.grade1.add() - Error Preservation: Complete error objects across process boundaries
- Multiple Channels: Support for separate IPC channels