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

> Discover how the Python bridge connects Electron and FastAPI in Modly. Learn to route requests, forward logs, and handle crashes with this complete implementation guide.

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

---

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

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

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

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/python-bridge.ts) attaches listeners to the child process streams:

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

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/electron/preload/electron-api.ts)) exposes these channels:

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

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

```typescript
// 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`](https://github.com/lightningpixel/modly/blob/main/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`](https://github.com/lightningpixel/modly/blob/main/python-bridge.ts) to include `--reload`, though this may conflict with the readiness polling if restarts occur mid-check.