How the Python Bridge Connects Electron and FastAPI in Modly: Complete Implementation Guide

The Python bridge in Modly spawns a FastAPI child process, waits for a health check to pass, then routes HTTP requests between the Electron UI and the Python backend while forwarding logs and crash events through IPC.

The Modly application (lightningpixel/modly) is a desktop 3-D generation tool built on Electron with Python-powered inference. The Python bridge (electron/main/python-bridge.ts) enables seamless two-way communication between the JavaScript frontend and the FastAPI server that runs the heavy lifting. This article breaks down exactly how it works, with complete source references from the repository.

Spawning the FastAPI Server on Startup

When the Electron main process initializes, PythonBridge.start() launches the backend as a child process.

Python Interpreter Resolution

The bridge first determines which Python executable to use:

  • Production: Uses the interpreter bundled with the packaged app (platform-specific path resolution)
  • Development: Falls back to ./.venv/bin/python (or .venv\Scripts\python.exe on Windows)
uvicorn main:app --host 127.0.0.1 --port 8765

Environment Variable Injection

FastAPI needs access to model paths and credentials. The bridge injects these environment variables before spawning:

  • MODELS_DIR – Location of downloaded diffusion models
  • WORKSPACE_DIR – User's project workspace
  • EXTENSIONS_DIR – Third-party extension modules
  • HF_TOKEN – Hugging Face authentication token

This setup occurs in electron/main/python-bridge.ts within the start() method.

The Readiness Handshake: When Is FastAPI Actually Ready?

Spawning a process doesn't mean the server is accepting requests. The bridge implements a polling health check using axios.

// Conceptual flow from python-bridge.ts
private async waitForReady(): Promise<void> {
  while (!this.ready) {
    try {
      const response = await axios.get('http://127.0.0.1:8765/health');
      if (response.status === 200) {
        this.ready = true;
        return;
      }
    } catch {
      // FastAPI still starting, wait and retry
      await delay(100);
    }
  }
}

The bridge repeatedly hits /health until it receives HTTP 200. Only then does this.ready become true, and the Electron app considers the backend operational.

HTTP Communication Pattern: No Custom Protocols Needed

Once ready, the bridge exposes API_BASE_URL (http://127.0.0.1:8765) for all subsequent communication. The UI uses standard HTTP fetch/axios—no IPC overhead for API calls.

Frontend API Helper

// src/shared/hooks/useApi.ts
import { API_BASE_URL } from '../../electron/main/python-bridge';

export async function apiGet<T>(path: string): Promise<T> {
  const response = await fetch(`${API_BASE_URL}${path}`);
  if (!response.ok) throw new Error(`API error ${response.status}`);
  return response.json() as Promise<T>;
}

// Example: Check backend health from renderer
const health = await apiGet<{status: string}>('/health');

FastAPI Endpoints Exposed

The FastAPI server in Modly exposes standard REST endpoints including:

  • POST /generation – Trigger 3-D model generation
  • GET /model – List available models
  • POST /optimize – Run mesh optimization
  • GET /health – Liveness probe

All follow ordinary HTTP semantics—the bridge itself doesn't intercept or transform these requests, it merely makes the base URL available.

Bi-Directional IPC: Logs and Crash Events Flow Back to UI

Where HTTP handles request-response, Electron IPC handles push notifications from the Python process to the UI.

Capturing Process Output

python-bridge.ts attaches listeners to the child process streams:

// From python-bridge.ts implementation
this.process.stdout.on('data', (data) => {
  const line = data.toString().trim();
  logger.python.info(line);           // Write to internal logger
  
  // Forward non-info logs to renderer for UI display
  if (!line.includes('INFO')) {
    this.mainWindow?.webContents.send('python:log', line);
  }
});

this.process.stderr.on('data', (data) => {
  const line = data.toString().trim();
  logger.python.error(line);
  this.mainWindow?.webContents.send('python:log', line);
});

Crash Detection and Recovery

If the FastAPI process exits unexpectedly, the bridge notifies the renderer:

// From python-bridge.ts
this.process.on('exit', (code) => {
  this.ready = false;
  this.mainWindow?.webContents.send('python:crashed', { code });
});

The preload script (electron/preload/electron-api.ts) exposes these channels:

// electron/preload/electron-api.ts
export const onPythonLog = (callback: (msg: string) => void) => {
  ipcRenderer.on('python:log', (_event, msg) => callback(msg));
};

export const onPythonCrashed = (callback: (info: {code: number}) => void) => {
  ipcRenderer.on('python:crashed', (_event, info) => callback(info));
};

This lets the UI display real-time Python logs in a console panel and show error dialogs when the backend crashes.

Graceful Shutdown and Memory Management

3-D generation workloads consume substantial RAM. Modly provides explicit lifecycle control.

Stop: Clean Process Termination

PythonBridge.stop() kills the entire process tree to prevent orphaned subprocesses:

  • Linux/macOS: Uses process.kill(-pid, 'SIGTERM') on the process group
  • Windows: Falls back to taskkill /F /T /IM to terminate the tree

This ensures extensions or spawned model loaders don't leak.

Restart: Fresh State Without App Restart

// src/shared/hooks/useGeneration.ts
import { pythonBridge } from '../../electron/main/python-bridge';

export async function freeMemoryAndRestart() {
  await pythonBridge.restart();   // stop() then start()
}

The restart() method chains stop() → start(), allowing the UI to reclaim memory after heavy generation runs without closing the Electron application.

Integrating the Bridge in the Main Process

Here's the complete initialization pattern from electron/main/index.ts:

// electron/main/index.ts
import { PythonBridge } from './python-bridge';
const pythonBridge = new PythonBridge();

// Bridge needs window reference to send IPC messages
pythonBridge.setWindowGetter(() => mainWindow);

app.on('ready', async () => {
  await pythonBridge.start();          // Launches uvicorn, waits for health
  console.log('FastAPI ready on', pythonBridge.getPort());
});

The setWindowGetter() callback pattern avoids circular dependencies—the bridge stores a getter function rather than the window itself, since mainWindow may not exist when the bridge is instantiated.

Summary

  • Spawning: PythonBridge.start() runs uvicorn with injected environment variables, resolving Python from bundled or development paths
  • Readiness: Polls /health via axios until HTTP 200, setting this.ready
  • API Communication: Exports API_BASE_URL for standard HTTP fetch from the renderer—no IPC for normal requests
  • IPC Events: Forwards stdout/stderr as python:log, and process exit as python:crashed to the UI
  • Lifecycle: stop() kills the process tree cross-platform; restart() chains stop/start for memory recovery

Frequently Asked Questions

Why use HTTP instead of direct IPC for all communication?

HTTP provides a language-agnostic, debuggable interface that matches FastAPI's native protocol. IPC would require serialization overhead and custom message handling. The bridge only uses IPC for push notifications (logs, crashes) where HTTP's request-response model doesn't fit.

What happens if the health check never passes?

The bridge continues polling indefinitely in 100ms intervals. In production, the UI should monitor this.ready through IPC and display a loading state. If the Python process crashes during startup, the exit handler fires and sends python:crashed with a non-zero code.

Can I change the FastAPI port?

The port 8765 is hardcoded in python-bridge.ts. To modify it, change the --port argument in the uvicorn spawn command and update the API_BASE_URL constant. Both must remain synchronized.

Does the bridge support hot-reloading during development?

Not automatically. The FastAPI server runs with default uvicorn settings (no --reload). To enable reload, modify the spawn arguments in python-bridge.ts to include --reload, though this may conflict with the readiness polling if restarts occur mid-check.

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 →