Implementing Inter-Process Communication (IPC) in Electron
IPC in Electron is the only safe way to give renderer process access to system resources. Renderer runs in isolated Chromium context; everything related to file system, native APIs, and Node.js — only through IPC and preload script.
Architecture of Secure IPC
Renderer (Chromium)
↓ window.electronAPI.someMethod()
Preload Script (contextBridge)
↓ ipcRenderer.invoke('channel', payload)
Main Process (Node.js)
↓ ipcMain.handle('channel', handler)
Preload is the security boundary. It exposes to renderer exactly what API is needed, nothing extra.
Preload: Typed Bridge
// main/preload.ts
import { contextBridge, ipcRenderer } from 'electron';
type FileInfo = { path: string; content: string; size: number };
const api = {
fs: {
readFile: (path: string): Promise<FileInfo> =>
ipcRenderer.invoke('fs:readFile', path),
writeFile: (path: string, content: string): Promise<void> =>
ipcRenderer.invoke('fs:writeFile', path, content),
},
app: {
getVersion: (): Promise<string> =>
ipcRenderer.invoke('app:getVersion'),
},
window: {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
}
};
contextBridge.exposeInMainWorld('electronAPI', api);
export type ElectronAPI = typeof api;
Main: IPC Handlers
// main/ipc-handlers.js
const { ipcMain, app, dialog } = require('electron');
const fs = require('fs/promises');
const path = require('path');
function registerHandlers() {
// handle — for invoke (with response)
ipcMain.handle('fs:readFile', async (event, filePath) => {
const resolvedPath = path.resolve(filePath);
const allowedDirs = [
app.getPath('documents'),
app.getPath('downloads'),
app.getPath('userData')
];
const isAllowed = allowedDirs.some(dir => resolvedPath.startsWith(dir));
if (!isAllowed) {
throw new Error(`Access denied: ${resolvedPath}`);
}
const content = await fs.readFile(resolvedPath, 'utf-8');
const stat = await fs.stat(resolvedPath);
return { path: resolvedPath, content, size: stat.size };
});
ipcMain.handle('fs:writeFile', async (event, filePath, content) => {
await fs.writeFile(filePath, content, 'utf-8');
});
ipcMain.handle('app:getVersion', () => app.getVersion());
// on — for send (without response)
ipcMain.on('window:minimize', (event) => {
BrowserWindow.fromWebContents(event.sender)?.minimize();
});
}
module.exports = { registerHandlers };
Streaming Data via IPC
For large data volumes:
// main/ipc-handlers.js
ipcMain.handle('fs:readLargeFile', async (event, filePath) => {
const { port1, port2 } = new MessageChannelMain();
event.sender.postMessage('port', null, [port1]);
const stream = require('fs').createReadStream(filePath);
stream.on('data', (chunk) => {
port2.postMessage({ type: 'chunk', data: chunk });
});
stream.on('end', () => {
port2.postMessage({ type: 'end' });
port2.close();
});
});
Broadcast from Main to All Windows
// main/broadcast.js
const { BrowserWindow } = require('electron');
function broadcast(channel, data) {
BrowserWindow.getAllWindows().forEach(win => {
if (!win.isDestroyed()) {
win.webContents.send(channel, data);
}
});
}
module.exports = { broadcast };
Common errors: forgetting error handling in ipcMain.handle, passing non-serializable objects through IPC, not validating paths in handlers for path traversal attacks.







