How IPC Communication Works Between Renderer and Main Process in Modly

Modly uses Electron's Inter-Process Communication (IPC) architecture with a preload-script security layer, exposing a trusted window.electron API that maps renderer calls to ipcRenderer.invoke/send channels handled by ipcMain in the main process.

Modly is an Electron-based application that requires secure communication between its web-based UI (renderer process) and the Node.js backend (main process). Understanding how this IPC system works is essential for extending the application or debugging communication issues. This article examines the complete request/response flow as implemented in the lightningpixel/modly repository.

The Three-Layer IPC Architecture

Modly's IPC system consists of three distinct layers working together to maintain security while enabling rich functionality.

Preload Script: The Security Boundary

The electron/preload/electron-api.ts file creates a trusted API surface that isolates the renderer from direct Node.js access. This approach follows Electron's security best practices by using contextBridge to expose only explicitly permitted functionality.

The preload script exposes methods that internally call:

  • ipcRenderer.send(channel, ...args) — fire-and-forget messages for actions that don't need a response
  • ipcRenderer.invoke(channel, ...args) — request/response RPC-style calls that return promises

Main Process Handlers: Request Processing

All IPC handlers are registered in electron/main/ipc-handlers.ts using two registration methods:

Method Purpose Example Use
ipcMain.on(channel, handler) One-way message handling Window controls (window:minimize)
ipcMain.handle(channel, handler) Async request/response File dialogs, data fetching

Message Flow Visualization


Renderer (UI) ──► window.electron.<method>() ──► ipcRenderer.invoke/on/send('channel')
                  │
Preload (contextBridge)
                  ▼
Main Process (ipcMain) ──► handler in ipc-handlers.ts
                  │
                  ▼
        Performs work (dialog, filesystem, Python bridge)
                  │
                  ▼
        Returns value (Promise) → resolves in renderer

One-Way vs. Request-Response Patterns

Modly uses both communication patterns strategically based on whether the renderer needs feedback.

Fire-and-Forget with ipcRenderer.send

Window controls demonstrate the simplest pattern — the renderer notifies the main process without waiting for a result.

// electron/preload/electron-api.ts
window.electron = {
  window: {
    minimize: () => ipcRenderer.send('window:minimize'),
    maximize: () => ipcRenderer.send('window:maximize'),
    close: () => ipcRenderer.send('window:close'),
  }
}

Corresponding main-process handler:

// electron/main/ipc-handlers.ts
ipcMain.on('window:minimize', () => getWindow()?.minimize());
ipcMain.on('window:maximize', () => {
  const win = getWindow();
  if (win) win.isMaximized() ? win.restore() : win.maximize();
});

Request-Response with ipcRenderer.invoke

When the renderer needs data or confirmation, Modly uses the promise-based invoke/handle pattern.

// electron/preload/electron-api.ts
window.electron = {
  window: {
    isMaximized: () => ipcRenderer.invoke('window:isMaximized') as Promise<boolean>,
  },
  fs: {
    selectImage: () => ipcRenderer.invoke('fs:selectImage') as Promise<string | null>,
  }
}

Corresponding handler with async result:

// electron/main/ipc-handlers.ts
ipcMain.handle('window:isMaximized', () => getWindow()?.isMaximized() ?? false);

ipcMain.handle('fs:selectImage', async () => {
  const win = getWindow();
  if (!win) return null;
  
  const result = await dialog.showOpenDialog(win, {
    title: 'Select an image',
    filters: [{ name: 'Images', extensions: ['jpg', 'jpeg', 'png', 'webp'] }],
    properties: ['openFile'],
  });
  
  return result.canceled ? null : result.filePaths[0];
});

Complete IPC Channel Categories

The ipc-handlers.ts file registers handlers across six functional domains:

  1. Window controls — window:minimize, window:maximize, window:close, window:isMaximized

  2. File system dialogs — fs:selectImage, fs:selectMeshFile, fs:saveModel, fs:listDir, fs:listFiles

  3. Python bridge — python:start, python:status (implementation in electron/main/python-bridge.ts)

  4. Model management — model:download, model:delete, model:export, model:activeDownloads

  5. Extension management — extensions:list, extensions:installFromGitHub, extensions:uninstall

  6. App & system info — app:info, system:memory

Practical Usage in the Renderer

The exposed API simplifies IPC calls into familiar method invocations:

// Minimize window without waiting
await window.electron.window.minimize();

// Get file path with full async handling
const imagePath = await window.electron.fs.selectImage();
if (imagePath) {
  console.log('Selected:', imagePath);
}

// Start Python server and receive structured response
const { success, port } = await window.electron.python.start();
if (success) {
  console.log(`Python server on port ${port}`);
}

Security Model and Channel Isolation

Modly's IPC design enforces explicit channel whitelisting — only channels registered in ipc-handlers.ts are accessible. The preload script acts as a controlled gateway, preventing the renderer from directly accessing ipcRenderer or Node.js APIs. This architecture mitigates risks from compromised renderer content by ensuring all main-process access flows through audited, type-safe wrapper functions.

The electron/main/extension-path-guard.ts file provides additional security validation for extension-related operations, ensuring path traversal attacks cannot exploit the file system handlers.

Key Source Files

File Responsibility
electron/preload/electron-api.ts Defines the window.electron API surface
electron/preload/index.ts Entry point loading the API via contextBridge
electron/main/ipc-handlers.ts Registers all ipcMain handlers
electron/main/extension-path-guard.ts Path validation for extension security
electron/main/python-bridge.ts Python server lifecycle handlers

Summary

  • Modly IPC uses Electron's contextBridge + preload pattern to create a secure, auditable API boundary between renderer and main process
  • Two communication patterns: send/on for fire-and-forget, invoke/handle for request-response
  • All handlers centralized in electron/main/ipc-handlers.ts with consistent channel naming (domain:action)
  • Renderer access strictly mediated through window.electron object — no direct Node.js access
  • Type-safe wrappers in preload script ensure consistent promise types and error handling

Frequently Asked Questions

What prevents malicious code in the renderer from accessing the file system directly?

The renderer runs in a sandboxed context with no direct Node.js access. Only the preload script (loaded before renderer code executes) can access ipcRenderer, and it exposes specific methods rather than the raw IPC object. This means compromised renderer code can only invoke the predefined handlers in ipc-handlers.ts, not arbitrary file system operations.

Why does Modly use both send and invoke instead of just one pattern?

Performance and semantics differ. send is lighter for one-way notifications where no confirmation is needed (window minimize). invoke adds promise overhead but enables data return and error propagation (file dialogs, Python status). Using both appropriately keeps the UI responsive while supporting complex workflows.

How are IPC channels kept in sync between preload and main process?

Both sides reference string channel names that must match exactly. The centralized registration in ipc-handlers.ts and exposure in electron-api.ts creates a de facto contract. TypeScript interfaces (implied by the as Promise<T> type assertions in preload) help catch mismatches during development, though runtime validation would require additional tooling.

Can I add custom IPC channels to Modly?

Yes — add the handler in electron/main/ipc-handlers.ts using ipcMain.on or ipcMain.handle, then expose a wrapper method in electron/preload/electron-api.ts that calls the corresponding ipcRenderer method with your channel name. Follow the existing naming convention (domain:action) for consistency.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →