# How IPC Communication Works Between Renderer and Main Process in Modly

> Learn how Modly's IPC communication works. Discover how renderer calls map to ipcRenderer invoke/send channels handled by ipcMain, secured by a preload script.

- Repository: [lightningpixel/modly](https://github.com/lightningpixel/modly)
- Tags: internals
- Published: 2026-08-20

---

**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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/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.

```typescript
// 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:

```typescript
// 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.

```typescript
// 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:

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/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:

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/electron/preload/electron-api.ts) | Defines the `window.electron` API surface |
| [`electron/preload/index.ts`](https://github.com/lightningpixel/modly/blob/main/electron/preload/index.ts) | Entry point loading the API via `contextBridge` |
| [`electron/main/ipc-handlers.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/ipc-handlers.ts) | Registers all `ipcMain` handlers |
| [`electron/main/extension-path-guard.ts`](https://github.com/lightningpixel/modly/blob/main/electron/main/extension-path-guard.ts) | Path validation for extension security |
| [`electron/main/python-bridge.ts`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/ipc-handlers.ts) and exposure in [`electron-api.ts`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/electron/main/ipc-handlers.ts) using `ipcMain.on` or `ipcMain.handle`, then expose a wrapper method in [`electron/preload/electron-api.ts`](https://github.com/lightningpixel/modly/blob/main/electron/preload/electron-api.ts) that calls the corresponding `ipcRenderer` method with your channel name. Follow the existing naming convention (`domain:action`) for consistency.