How Modly Handles IPC Communication Between Electron Main and Renderer Processes

Modly implements a secure preload script architecture that exposes a controlled API via contextBridge.exposeInMainWorld, allowing the renderer process to invoke main-process operations through ipcRenderer.invoke while receiving real-time progress updates via event-driven ipcRenderer.on listeners.

Modly is an Electron-based application that orchestrates complex workflows by coordinating between its React frontend and a Python backend runtime. According to the lightningpixel/modly source code, the application establishes robust IPC communication between Electron main and renderer processes through a carefully isolated preload script pattern. This design ensures that sensitive Node.js APIs remain inaccessible to the renderer while still enabling powerful workflow management capabilities.

Secure API Exposure via electron/preload/electron-api.ts

The foundation of Modly's IPC security lies in electron/preload/electron-api.ts, which acts as a controlled bridge between the UI and the privileged main process. Rather than exposing raw Electron modules to the renderer, this file uses contextBridge.exposeInMainWorld to attach a curated API object to window.electron.

The preload script implements two distinct communication patterns:

  • Request-response RPC: Uses ipcRenderer.invoke for synchronous-style calls that return promises
  • Event-driven updates: Uses ipcRenderer.on and removeAllListeners for subscribing to streaming data from the main process

This approach ensures that the renderer can only invoke explicitly defined methods, preventing arbitrary access to the filesystem or shell execution capabilities.

Main Process IPC Handlers in electron/main/ipc-handlers.ts

The main process registers its API surface in electron/main/ipc-handlers.ts, which uses ipcMain.handle for asynchronous RPC and ipcMain.on for fire-and-forget messaging. This file manages the CRUD operations for workflow definitions through five specific channels:

  1. workflows:list – Returns an array of saved workflow JSON files from the storage directory
  2. workflows:save – Persists a workflow definition to disk after validation
  3. workflows:delete – Removes a workflow file by identifier
  4. workflows:import – Reads a user-selected JSON file and stores it in the application's workflow directory
  5. workflows:export – Writes a workflow definition to a user-chosen file path outside the application directory

Each handler is registered during application startup and remains active throughout the main process lifecycle, ensuring consistent state management across renderer reloads.

Workflow Execution and the Python Bridge

When the UI initiates workflow execution, the IPC flow extends beyond JavaScript into a spawned Python process. The renderer calls window.electron.workflow.run(), which triggers ipcRenderer.invoke('workflow:run', { workflowId, overrides }) in the preload script.

The main-process listener resides in electron/main/process-runner.ts and performs the following sequence:

  • Loads the requested workflow definition from the workflows directory
  • Spawns a child Python process through electron/main/python-bridge.ts to handle generation and mesh processing
  • Streams progress updates back to the renderer via ipcMain.emit('workflow:progress', …)

The Python bridge communicates status messages—including log lines and completion percentages—back to the main process, which then forwards them to the renderer through the preload-exposed window.electron.workflow.onProgress callback.

Bidirectional Communication Patterns

Modly's architecture supports full-duplex communication during workflow execution. While the renderer can initiate actions through the preload API, the main process actively pushes updates without waiting for renderer requests.

From Renderer to Main:

// Start execution
await window.electron.workflow.run({ workflowId: 'my-wf' })

// Stop execution
window.electron.workflow.stop()  // Maps to ipcRenderer.send('workflow:stop')

From Main to Renderer:

// Subscribe to progress in the renderer
window.electron.workflow.onProgress(({ nodeId, percent }) => {
  console.log(`Node ${nodeId} is ${percent}% complete`)
})

This bidirectional flow enables real-time feedback during long-running Python operations while maintaining process isolation.

Code Implementation Examples

Listing Available Workflows

From the renderer context, fetching stored workflows uses the exposed list method:

const workflows = await window.electron.workflows.list()

This invokes the workflows:list handler in electron/main/ipc-handlers.ts, which scans the workflows directory and returns parsed JSON objects.

Saving Workflow Changes

After editing in the UI defined in src/areas/workflows/workflowRunStore.ts, persist changes through:

await window.electron.workflows.save(updatedWorkflow)

The preload script forwards this to the workflows:save channel, which validates the schema before writing to disk.

Monitoring Execution Progress

To receive real-time updates during execution:

// Initiate workflow
window.electron.workflow.run({ workflowId: 'my-wf' })

// Handle progress events
window.electron.workflow.onProgress(({ nodeId, percent }) => {
  console.log(`Node ${nodeId} is ${percent}% complete`)
})

The onProgress method wraps ipcRenderer.on('workflow:progress', callback) and manages listener cleanup through removeAllListeners when subscriptions end.

Summary

  • Modly uses electron/preload/electron-api.ts to create a secure window.electron API via contextBridge.exposeInMainWorld, preventing direct renderer access to Node.js modules
  • CRUD operations for workflows are handled through ipcMain.handle registrations in electron/main/ipc-handlers.ts using channels like workflows:list and workflows:save
  • Workflow execution flows from the renderer through workflow:run to electron/main/process-runner.ts, which spawns Python processes via electron/main/python-bridge.ts
  • Real-time progress updates travel from Python through the main process to the renderer via workflow:progress events exposed as window.electron.workflow.onProgress
  • Process isolation is maintained throughout, with the renderer in src/areas/workflows/workflowRunStore.ts communicating exclusively through the preload-defined interface

Frequently Asked Questions

Why does Modly use a preload script instead of direct ipcRenderer access?

Modly follows Electron security best practices by restricting direct access to ipcRenderer from the renderer process. The preload script in electron/preload/electron-api.ts acts as a capabilities proxy, exposing only specific methods through contextBridge.exposeInMainWorld. This prevents arbitrary IPC calls and ensures that malicious code in the renderer cannot exploit Node.js APIs to access the filesystem or execute shell commands.

How does Modly stream progress updates from Python to the UI?

During workflow execution, the Python child process communicates status messages to electron/main/process-runner.ts, which then emits workflow:progress events through ipcMain. The preload script listens for these events via ipcRenderer.on and invokes registered callbacks exposed as window.electron.workflow.onProgress. This creates a streaming channel from the Python backend through the main process to the React frontend without blocking the renderer thread.

What is the difference between ipcMain.handle and ipcMain.on in Modly's architecture?

Modly uses ipcMain.handle in electron/main/ipc-handlers.ts for request-response patterns like workflows:list and workflows:save, where the renderer expects a return value or error. Conversely, ipcMain.on handles fire-and-forget messages like workflow:stop and the outgoing workflow:progress events that originate from the main process. The former supports async/await patterns in the renderer, while the latter enables event-driven updates without blocking.

How are workflow files managed across the IPC boundary?

Workflow persistence is abstracted through the preload API. When the renderer calls window.electron.workflows.save(), the preload script uses ipcRenderer.invoke to trigger the workflows:save handler in the main process. This handler validates the workflow structure in src/areas/workflows/preflight.ts before writing JSON to the filesystem. File operations never execute in the renderer; they are always proxied through the main process via these defined IPC channels.

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 →