Electron

Electron

IPC

Electron-IPC

Electron IPC

What It Is

Inter-Process Communication (IPC) in Electron enables secure communication between the main process (Node.js), preload scripts (bridge), and renderer processes (web pages). IPC is the foundation of Electron's security model, allowing renderers to access system capabilities without direct Node.js access.

In WhatNext, IPC provides the bridge between the React UI (renderer) and system-level operations (main process), with an additional layer for P2P networking (utility process).

Why We Use It

  • Security: Renderer sandbox enforced (no direct Node.js access)
  • Type safety: Strongly-typed API surface via TypeScript
  • Modularity: Clear separation between UI and system concerns
  • Electron architecture: Industry-standard pattern for desktop apps

Critical security principle: nodeIntegration: false and contextIsolation: true enforced. All system access must go through preload script.

How It Works

Three-Process Architecture

┌──────────────────┐
│ Renderer Process   (React UI - sandboxed Chromium)
    (React)       │
└────────┬─────────┘
         │ IPC via window.electron
         ↓
┌────────────────────┐
│  Preload Script      (Security boundary - contextBridge)
  (preload.ts)      │
└────────┬───────────┘
         │ ipcRenderer.invoke()
         ↓
┌────────────────────┐
│   Main Process       (Node.js - full system access)
   (main.ts)        │
└────────┬───────────┘
         │ MessagePort
         ↓
┌────────────────────┐
│ Utility Process      (Node.js - libp2p P2P networking)
  (p2p-service.ts)  │
└────────────────────┘

IPC Flow

  1. Renderer calls window.electron.app.getVersion()
  2. Preload translates to ipcRenderer.invoke('app:get-version')
  3. Main handles via ipcMain.handle('app:get-version', …)
  4. Main returns result
  5. Preload forwards to renderer
  6. Renderer receives typed result

Key Patterns

Pattern 1: Preload API Surface

Expose minimal, type-safe API to renderer:

// preload.ts
import { contextBridge, ipcRenderer } from 'electron';

const electronAPI = {
    app: {
        getVersion: () => ipcRenderer.invoke('app:get-version'),
        getPlatform: () => ipcRenderer.invoke('app:get-platform')
    },
    window: {
        minimize: () => ipcRenderer.invoke('window:minimize'),
        maximize: () => ipcRenderer.invoke('window:maximize'),
        close: () => ipcRenderer.invoke('window:close')
    },
    dialog: {
        openFile: (options) => ipcRenderer.invoke('dialog:open-file', options),
        openDirectory: (options) => ipcRenderer.invoke('dialog:open-directory')
    }
};

// Expose to renderer
contextBridge.exposeInMainWorld('electron', electronAPI);

// Type declarations for renderer
export type ElectronAPI = typeof electronAPI;

Pattern 2: Main Process Handlers

Handle IPC requests in main process:

// main.ts
import { app, ipcMain, BrowserWindow, dialog } from 'electron';

function setupIPC(mainWindow: BrowserWindow) {
    // Application info
    ipcMain.handle('app:get-version', () => app.getVersion());
    ipcMain.handle('app:get-platform', () => process.platform);

    // Window controls
    ipcMain.handle('window:minimize', () => mainWindow.minimize());
    ipcMain.handle('window:maximize', () => {
        if (mainWindow.isMaximized()) {
            mainWindow.unmaximize();
        } else {
            mainWindow.maximize();
        }
    });

    // File dialogs
    ipcMain.handle('dialog:open-file', async (_, options) => {
        const result = await dialog.showOpenDialog(mainWindow, options);
        return result.filePaths;
    });

    // External links (with security validation)
    ipcMain.handle('shell:open-external', async (_, url: string) => {
        // Validate URL is http(s)
        if (!url.startsWith('http://') && !url.startsWith('https://')) {
            throw new Error('Invalid URL protocol');
        }
        await shell.openExternal(url);
    });
}

Pattern 3: Renderer Usage

Use typed API in React components:

// React component
import { useState, useEffect } from 'react';

function AppInfo() {
    const [version, setVersion] = useState('');
    const [platform, setPlatform] = useState('');

    useEffect(() => {
        // Call via window.electron (exposed by preload)
        window.electron.app.getVersion().then(setVersion);
        window.electron.app.getPlatform().then(setPlatform);
    }, []);

    return (
        <div>
            <p>Version: {version}</p>
            <p>Platform: {platform}</p>
        </div>
    );
}

Pattern 4: Event-Based Communication

For continuous updates (not request/response):

// Preload
const electronAPI = {
    onWindowMaximized: (callback) => {
        ipcRenderer.on('window-maximized', callback);
        return () => ipcRenderer.removeListener('window-maximized', callback);
    }
};

// Main
mainWindow.on('maximize', () => {
    mainWindow.webContents.send('window-maximized');
});

// Renderer
useEffect(() => {
    const cleanup = window.electron.onWindowMaximized(() => {
        console.log('Window maximized');
    });
    return cleanup;  // Cleanup listener
}, []);

Pattern 5: Utility Process Communication

Main ↔ Utility via MessagePort:

// Main spawning utility
import { utilityProcess } from 'electron';

const p2pProcess = utilityProcess.fork(
    path.join(__dirname, 'p2p-service.js')
);

// Send to utility
p2pProcess.postMessage({ type: 'START_NODE' });

// Receive from utility
p2pProcess.on('message', (message) => {
    console.log('From utility:', message);
    // Forward to renderer via IPC if needed
    mainWindow.webContents.send('p2p:event', message);
});

// Utility process (p2p-service.ts)
import { parentPort } from 'node:worker_threads';

parentPort?.on('message', (message) => {
    if (message.type === 'START_NODE') {
        // Start libp2p node
    }
});

parentPort?.postMessage({
    type: 'node_started',
    peerId: '12D3KooW...'
});

Common Pitfalls

Pitfall 1: Exposing Node.js Directly

Problem: Enabling nodeIntegration in renderer.

// ❌ NEVER DO THIS
new BrowserWindow({
    webPreferences: {
        nodeIntegration: true,  // ← Security vulnerability!
        contextIsolation: false
    }
});

Why dangerous: Renderer has full Node.js access, including require('child_process'). Malicious code (XSS) can execute arbitrary system commands.

Solution: Keep nodeIntegration: false, contextIsolation: true. Use preload script.

Pitfall 2: Forgetting to Register Handlers

Problem: Calling IPC method without corresponding ipcMain.handle().

Error:

Error: No handler registered for 'app:get-version'

Solution: Register handler in main process before renderer loads:

// main.ts - before createWindow()
ipcMain.handle('app:get-version', () => app.getVersion());

Pitfall 3: Not Validating Input

Problem: Accepting untrusted renderer input without validation.

// ❌ Dangerous
ipcMain.handle('file:delete', async (_, filePath) => {
    await fs.unlink(filePath);  // ← Malicious path could delete system files!
});

// ✅ Validated
ipcMain.handle('file:delete', async (_, filePath) => {
    const userDataPath = app.getPath('userData');
    if (!filePath.startsWith(userDataPath)) {
        throw new Error('Path outside user data directory');
    }
    await fs.unlink(filePath);
});

Pitfall 4: Memory Leaks from Event Listeners

Problem: Adding event listeners without cleanup.

// ❌ Memory leak
useEffect(() => {
    window.electron.onP2PEvent((data) => {
        console.log(data);
    });
    // ← No cleanup!
}, []);

// ✅ Proper cleanup
useEffect(() => {
    const cleanup = window.electron.onP2PEvent((data) => {
        console.log(data);
    });
    return cleanup;  // ← Cleanup on unmount
}, []);

Pitfall 5: Synchronous IPC

Problem: Using ipcRenderer.sendSync() blocks renderer.

// ❌ Blocks UI
const version = ipcRenderer.sendSync('app:get-version');

// ✅ Async (non-blocking)
const version = await ipcRenderer.invoke('app:get-version');

References

Official Documentation

WhatNext Implementation

  • Preload script: app/src/main/preload.ts
  • Main handlers: app/src/main/main.ts
  • Utility process: app/src/utility/p2p-service.ts
  • Type definitions: app/src/renderer/electron.d.ts
  • Issue 3: Setup Electron IPC for Core Functionality

Status: ✅ Production-ready, running in WhatNext v0.0.0
Security: nodeIntegration: false, contextIsolation: true enforced
Last Updated: 2025-11-12

Security