How Modly Handles IPC Communication Between Electron Main and Renderer for Workflow Execution
Modly uses a preload script architecture with contextBridge.exposeInMainWorld to expose a safe window.electron API that forwards renderer calls to the main process via ipcRenderer.invoke, while workflow execution spawns a Python subprocess that streams progress back through ipcMain.emit.
Modly is an open-source Electron application for procedural 3D content generation. Its IPC (Inter-Process Communication) layer enables the renderer process UI to control workflow execution in the main process, which delegates heavy computation to a Python backend. This article examines the complete IPC pathway based on the lightningpixel/modly source code.
The Preload Script: Secure API Exposure
Electron's context isolation prevents direct access to Node.js APIs from the renderer. Modly solves this through electron/preload/electron-api.ts, which creates a controlled bridge.
Exposing Workflow Methods
The preload script uses contextBridge.exposeInMainWorld to attach methods to window.electron:
// electron/preload/electron-api.ts
contextBridge.exposeInMainWorld('electron', {
workflow: {
run: (args) => ipcRenderer.invoke('workflow:run', args),
stop: () => ipcRenderer.send('workflow:stop'),
onProgress: (callback) => {
ipcRenderer.on('workflow:progress', (_, data) => callback(data))
return () => ipcRenderer.removeAllListeners('workflow:progress')
}
},
workflows: {
list: () => ipcRenderer.invoke('workflows:list'),
save: (workflow) => ipcRenderer.invoke('workflows:save', workflow),
delete: (id) => ipcRenderer.invoke('workflows:delete', id),
import: () => ipcRenderer.invoke('workflows:import'),
export: (id) => ipcRenderer.invoke('workflows:export', id)
}
})
Key patterns here:
ipcRenderer.invokefor request/response RPC (returns promises)ipcRenderer.sendfor fire-and-forget commandsipcRenderer.onwith cleanup functions for event subscriptions
Main Process Handlers: IPC Registration
The main process registers handlers in electron/main/ipc-handlers.ts using ipcMain.handle and ipcMain.on:
| Channel | Handler Type | Purpose |
|---|---|---|
workflows:list |
ipcMain.handle |
Returns array of saved workflow JSON files |
workflows:save |
ipcMain.handle |
Persists workflow definition to disk |
workflows:delete |
ipcMain.handle |
Removes workflow file by ID |
workflows:import |
ipcMain.handle |
Reads user-selected JSON, validates, stores |
workflows:export |
ipcMain.handle |
Writes workflow to user-chosen path |
These handlers perform filesystem operations through Node.js APIs—operations inaccessible to the isolated renderer.
Workflow Execution: The Complete IPC Flow
Running a workflow triggers Modly's most complex IPC sequence. The implementation spans three files and involves a Python subprocess.
Step 1: Renderer Initiates Execution
From the UI layer (typically workflowRunStore.ts):
// Start workflow execution
await window.electron.workflow.run({
workflowId: 'terrain-generator-v2',
overrides: { seed: 42, resolution: 2048 }
})
Step 2: Main Process Receives and Spawns
In electron/main/process-runner.ts, the handler:
- Loads the workflow JSON from
workflows/directory - Validates the node graph
- Spawns Python via
electron/main/python-bridge.ts
// electron/main/process-runner.ts (conceptual)
ipcMain.handle('workflow:run', async (event, { workflowId, overrides }) => {
const workflow = await loadWorkflow(workflowId)
const pythonProcess = spawnPythonBridge(workflow, overrides)
pythonProcess.on('message', (status) => {
// Forward Python progress to renderer
event.sender.send('workflow:progress', {
nodeId: status.currentNode,
percent: status.progressPercent,
logs: status.logLines
})
})
return { pid: pythonProcess.pid }
})
Step 3: Bidirectional Progress Streaming
While Python executes, status flows back through:
Python subprocess → python-bridge.ts → process-runner.ts
↓
event.sender.send('workflow:progress')
↓
Renderer ← ipcRenderer.on('workflow:progress') ← preload re-exposure
The renderer subscribes via the preload's cleanup-aware API:
// Listen to execution progress
const unsubscribe = window.electron.workflow.onProgress(({
nodeId,
percent,
logs
}) => {
updateNodeStatus(nodeId, percent)
appendToLogPanel(logs)
})
// Cleanup on unmount or workflow completion
unsubscribe()
Step 4: Stopping Execution
The stop command uses ipcRenderer.send for immediate delivery without awaiting response:
// Immediate termination request
window.electron.workflow.stop() // → ipcMain.on('workflow:stop', ...)
The main process kills the Python subprocess and emits a final workflow:progress with { status: 'cancelled' }.
IPC Communication Patterns in Modly
Modly employs three distinct IPC patterns depending on the use case:
Synchronous RPC with invoke/handle
- Use when the renderer needs a result or confirmation
- Examples:
workflows:list,workflows:save, initialworkflow:run(returns PID)
Event streaming with send/on
- Use for ongoing, multi-message updates
- Example:
workflow:progressevents during execution
Fire-and-forget with send/on
- Use for commands requiring no acknowledgment
- Example:
workflow:stop
Renderer-Side State Management
The IPC API is consumed by two key renderer modules:
workflowsStore.ts(shared store) — wraps CRUD operations (list,save,delete) with reactive stateworkflowRunStore.ts(execution store) — orchestratesrun,stop, and progress subscription lifecycle
Preflight validation in src/areas/workflows/preflight.ts checks node connectivity and parameter validity before the IPC call, preventing invalid workflows from reaching the main process.
Security Considerations
Modly's IPC architecture follows Electron security best practices:
- No
nodeIntegration— renderer cannot access filesystem or spawn processes directly - Context isolation enabled — preload runs in separate context from renderer
- IPC channel allowlisting — only predefined channels pass through preload
- User input never eval'd — workflow JSON is parsed with
JSON.parse, noteval
Summary
- Preload bridge (
electron/preload/electron-api.ts) exposes controlledwindow.electronAPI usingcontextBridge.exposeInMainWorld - Main handlers (
electron/main/ipc-handlers.ts) register async RPC for workflow CRUD operations - Process runner (
electron/main/process-runner.ts) spawns Python viaelectron/main/python-bridge.tsand streams progress viaevent.sender.send - Bidirectional flow uses
invokefor commands,on/emitfor progress updates, andsendfor stop signals - Renderer stores abstract IPC details from UI components while managing subscription lifecycles
Frequently Asked Questions
How does Modly prevent renderer process from directly accessing the filesystem?
Modly enables Electron's context isolation and disables nodeIntegration. The renderer can only access filesystem operations through the explicitly exposed window.electron API in electron/preload/electron-api.ts, which forwards requests to the main process. This prevents arbitrary code execution and follows Electron's security guidelines.
Why does Modly use both ipcRenderer.invoke and ipcRenderer.send?
invoke provides request/response semantics suitable for CRUD operations where the renderer needs confirmation or data (e.g., workflows:list returning an array). send is fire-and-forget, used when the renderer doesn't need acknowledgment—like workflow:stop where immediate delivery matters more than response. Progress streaming uses on with callbacks because it involves multiple messages over time.
Where does the actual workflow computation happen if IPC connects renderer to main?
The main process delegates to a Python subprocess spawned through electron/main/python-bridge.ts. The main process acts as a coordinator: it manages the Python lifecycle, parses stdout/stderr, and forwards structured progress messages back to the renderer via IPC. This Python bridge handles generation algorithms, mesh processing, and file I/O that would block the main process.
Can multiple workflows run simultaneously in Modly?
The current architecture in process-runner.ts tracks a single Python subprocess per workflow execution. Multiple concurrent executions would require extending the runner to manage a process map (workflowId → subprocess) rather than singleton state. The IPC channels support this—workflow:run returns a PID that could identify specific instances—but the store layers would need corresponding updates to handle parallel execution state.
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 →