How IPC Communication Handles Requests Between Renderer and Main Process in Modly
Modly uses Electron's contextBridge and ipcRenderer to expose a type-safe window.electron API in the preload script, with main-process handlers registered via ipcMain.handle for async RPC and ipcMain.on for one-way messages.
The Inter-Process Communication (IPC) architecture in Modly follows Electron's security best practices by isolating the renderer process from direct Node.js access. Instead, all cross-process requests flow through a tightly controlled preload layer that exposes only whitelisted functionality to the web-based UI.
The Preload Script: Creating the Trusted API Surface
Modly's preload script at electron/preload/electron-api.ts constructs a secure bridge between the Chromium-based renderer and the Node.js main process. This file uses Electron's contextBridge module to inject a global window.electron object that the renderer can safely access.
The preload API wraps two core Electron IPC methods:
ipcRenderer.send(channel, ...args)— Fire-and-forget messages for actions that don't need a responseipcRenderer.invoke(channel, ...args)— Promise-based request/response RPC for operations that return data
This design prevents the renderer from directly importing ipcRenderer or any Node modules, eliminating a common attack surface in Electron applications.
Main Process Handlers: ipcMain.handle vs ipcMain.on
Handler registration happens in electron/main/ipc-handlers.ts. Modly distinguishes between two communication patterns:
One-Way Messages (ipcMain.on)
Used for actions where the renderer doesn't need confirmation or return values. Window controls are the canonical example:
// 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();
});
ipcMain.on('window:close', () => getWindow()?.close());
The corresponding preload exposure uses send():
// electron/preload/electron-api.ts
window.electron = {
window: {
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
// ...
}
};
Request-Response RPC (ipcMain.handle)
Used when the renderer needs data back from the main process. File dialogs, model queries, and system information all use this pattern:
// electron/main/ipc-handlers.ts
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];
});
ipcMain.handle('window:isMaximized', () => {
return getWindow()?.isMaximized() ?? false;
});
The preload wraps these with invoke():
// 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>,
}
};
Complete Message Flow
Understanding how an IPC request travels through Modly's architecture:
Renderer (UI thread)
│
▼
Calls window.electron.fs.selectImage()
│
▼
Preload script (electron/preload/electron-api.ts)
│
▼
ipcRenderer.invoke('fs:selectImage')
│
▼
Electron internal bridge
│
▼
Main process (electron/main/ipc-handlers.ts)
│
▼
ipcMain.handle('fs:selectImage') handler executes
│
▼
Native dialog.showOpenDialog() → filesystem access
│
▼
Return value serializes across IPC boundary
│
▼
Promise resolves in renderer with file path (or null)
This flow executes entirely asynchronously, preventing the renderer from blocking while the main process performs privileged operations like filesystem access or spawning Python processes.
Renderer-Side Usage Patterns
The exposed window.electron API provides ergonomic methods that hide the underlying channel names:
// Window state management
await window.electron.window.minimize();
const isMaxed: boolean = await window.electron.window.isMaximized();
// File operations with full dialog support
const imagePath: string | null = await window.electron.fs.selectImage();
const meshPath: string | null = await window.electron.fs.selectMeshFile();
// Python bridge initialization
const { success, port } = await window.electron.python.start();
if (success) {
console.log(`Python server available on port ${port}`);
}
// Extension management
const extensions = await window.electron.extensions.list();
await window.electron.extensions.installFromGitHub('author/repo-name');
All methods return promises for async handlers and void for fire-and-forget operations, with TypeScript definitions ensuring compile-time safety.
Security Architecture
Modly's IPC design implements defense in depth:
- Channel whitelisting — Only channels explicitly registered in
ipc-handlers.tscan receive messages - Context isolation — The preload runs in an isolated context with no access to renderer globals
- No Node exposure —
contextBridgeexplicitly prevents leakingrequire()or Node APIs - Path validation — Handlers like those for extensions use
electron/main/extension-path-guard.tsto prevent directory traversal
According to the Modly source code, the preload entry at electron/preload/index.ts loads the API definition and immediately clears any accidental global exposures.
Key Channel Categories
The IPC surface organizes functionality into semantic groups:
| Category | Example Channels | Handler Location |
|---|---|---|
| Window controls | window:minimize, window:maximize, window:close, window:isMaximized |
electron/main/ipc-handlers.ts |
| File system | fs:selectImage, fs:selectMeshFile, fs:saveModel, fs:listDir |
electron/main/ipc-handlers.ts |
| Python bridge | python:start, python:status |
electron/main/python-bridge.ts |
| Model management | model:download, model:delete, model:export |
electron/main/ipc-handlers.ts |
| Extensions | extensions:list, extensions:installFromGitHub, extensions:uninstall |
electron/main/ipc-handlers.ts |
| System info | app:info, system:memory |
electron/main/ipc-handlers.ts |
Summary
- Preload script (
electron/preload/electron-api.ts) creates a type-safewindow.electronAPI usingcontextBridge ipcRenderer.invoke()handles request-response patterns;ipcRenderer.send()handles one-way messages- Main process handlers register via
ipcMain.handle()for async RPC andipcMain.on()for fire-and-forget - Security is enforced through channel whitelisting, context isolation, and path validation guards
- All renderer access to filesystem, dialogs, Python bridge, and window management flows through this controlled IPC layer
Frequently Asked Questions
What is the difference between ipcRenderer.send and ipcRenderer.invoke in Modly?
ipcRenderer.send is fire-and-forget: the renderer emits a message and doesn't wait for a response, used for window controls like minimize or maximize. ipcRenderer.invoke returns a Promise and enables request-response patterns, required when the renderer needs data back such as a selected file path or Python server port. Modly's preload API abstracts this distinction—methods like window.electron.window.minimize() use send internally while window.electron.fs.selectImage() uses invoke.
Why does Modly use a preload script instead of enabling nodeIntegration?
The preload script at electron/preload/electron-api.ts follows Electron's security recommendations by using contextBridge to expose only whitelisted functionality. Enabling nodeIntegration would give the renderer direct access to Node.js APIs and the filesystem, creating severe security risks if the renderer loads untrusted content. Modly's approach isolates privileged operations to the main process while keeping the UI layer sandboxed.
How does Modly prevent unauthorized IPC channel access?
Only channels registered in electron/main/ipc-handlers.ts can receive messages. The preload script does not expose ipcRenderer directly—instead, it wraps specific channel names in typed methods. Attempting to call ipcRenderer.invoke('unknown:channel') from the renderer would fail because the preload never exposes the raw ipcRenderer object, and the main process has no handler registered for that channel.
Where are Python bridge IPC handlers implemented?
The python:start and python:status channels are handled in electron/main/python-bridge.ts, not the main ipc-handlers.ts file. This modular approach keeps the Python-specific spawn logic and port management separate from generic window and filesystem handlers, while still registering through the standard ipcMain.handle pattern.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →