DEV Community

木头人
木头人

Posted on

Ditch Electron: Spawning a Rust-Powered Python Sidecar from Bun

Part 1 of the ERTH Architecture Series: Launching local backends on Port 0, dynamic port negotiation, and establishing the dual-core desktop backbone.


If you are building a modern desktop application, you are probably tired of the same old options.

On one hand, you have Electron. It’s the industry standard, but it forces you to bundle a full Chromium browser and a Node.js runtime with every app. Even a simple "Hello World" takes up 200MB+ of disk space and eats hundreds of megabytes of RAM. For background utility utilities or AI assistants that need to be nimble, this is a massive tax.

On the other hand, you have Tauri. It solves the bundle size issue by binding to OS-native WebViews and using Rust for the backend. But unless you are already a Rust expert, you will find yourself fighting the compiler's borrow checker and async lifecycles, slowing down your development velocity.

But what if you want to use Python for its rich AI ecosystem (Ollama, SQLModel, PyTorch), but still keep the UI lightweight, fast-loading, and responsive?

Welcome to the ERTH Stack (ElectroBun + Robyn + Turso + HTMX). In this first post of our 5-part series, we will break down how to launch a high-performance Python sidecar backend directly from a Bun-based desktop shell, bypassing the bloated Electron environment entirely.


The Concept: Heterogeneous Dual-Core

In the ERTH architecture, the desktop app is split into two physical processes:

  1. The Main Process (Bun): Responsible for native OS window management, IPC (Inter-Process Communication), and hosting the HTML/CSS view. We use ElectroBun—a next-generation, ultra-lightweight wrapper that binds directly to the OS-native WebKit engine (no Chromium bloat!).
  2. The Sidecar Process (Python/Robyn): Responsible for heavy computations, database access, and local LLM orchestration. We use Robyn, an incredibly fast, Rust-based async Python web framework.

Here is how the lifecycle and process boundaries interact:


Step 1: Spawning the Robyn Sidecar from Bun

Under the hood of the Bun master process, we don't want to rely on static ports. If your app is hardcoded to run on port 8080, and the user already has a service running on that port, your application will crash instantly.

To avoid this, we start the Robyn backend on Port 0. In computer networking, binding to port 0 tells the operating system to automatically allocate a random, currently unused high-range port.

Here is the production-grade TypeScript code in Bun to spawn the Robyn child process and dynamically intercept the allocated port:

// src-app/frontend/src/bun/index.ts
import { join } from 'path';

let backendPort = 0;
let portFound = false;

// Resolve the path to the packaged Python binary or local script
const pythonAppPath = join(import.meta.dir, '..', '..', '..', 'backend', 'app.py');

const backendProcess = Bun.spawn(["uv", "run", "python", pythonAppPath], {
  stdout: "pipe", // We need to read the stdout log stream
  stderr: "inherit",
  env: {
    ...process.env,
    ROBYN_PORT: "0", // Force Robyn to bind to a dynamic port
  }
});

// Create a reader to parse stdout line by line
const reader = backendProcess.stdout.getReader();
const decoder = new TextDecoder();
let buffer = "";

(async () => {
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() || ""; // keep the last partial line in buffer

    for (const line of lines) {
      console.log(`[Backend Log] ${line}`);

      // Look for Actix/Robyn startup signature: "listening on: 127.0.0.1:XXXXX"
      const match = line.match(/listening on:\s+127\.0\.0\.1:(\d+)/);
      if (match) {
        backendPort = parseInt(match[1], 10);
        portFound = true;
        console.log(`🚀 Watchdog intercepted backend port: ${backendPort}`);

        // Notify the WebView window that the communication channel is ready
        initializeWebView(backendPort);
        break;
      }
    }
    if (portFound) break;
  }
})();
Enter fullscreen mode Exit fullscreen mode

Step 2: Setting up Robyn on Port 0

On the Python side, Robyn is incredibly simple to configure. By reading the ROBYN_PORT environment variable or defaulting to 0, we boot up a multi-threaded Rust Actix-web server underneath Python:

# src-app/backend/app.py
import os
from robyn import Robyn, Request, Response

app = Robyn(__file__)

@app.get("/api/v1/health")
async def health_check(request: Request):
    return Response(
        status_code=200,
        headers={"Content-Type": "application/json"},
        description='{"status": "healthy", "database": "connected"}'
    )

if __name__ == "__main__":
    # Get port from environment or fallback to 0
    port = int(os.environ.get("ROBYN_PORT", 0))
    app.start(host="127.0.0.1", port=port)
Enter fullscreen mode Exit fullscreen mode

Real-World Output

When you run this architecture, the terminal logs show the magic of dynamic negotiation in action:

Watchdog capturing dynamic port terminal logs

  1. Robyn outputs: Starting server at http://127.0.0.1:0
  2. The OS intercepts and binds it to 127.0.0.1:57220.
  3. Bun catches the 57220 port from the log stream, mounts the WebView, and binds all future HTMX requests to http://127.0.0.1:57220.

No port collision crashes. No manual user configuration. It just works.


What’s Next?

We now have a Bun frontend shell connected to a Python sidecar. But running multiple processes introduces new architectural failure points:

  • What happens if the Python process crashes or is killed by the system?
  • How do we prevent other local programs from scanning ports and hijacking our Python API?

In the next post, we will cover the Watchdog Heartbeat Pipeline and Opaque Token Security Interceptors to make this dual-core setup industrial-grade and secure.

If you want to skip ahead and read the full blueprint immediately, check out the companion book:

📖 ERTH Assistant: Local-First + AI Sidecar Desktop Architecture on Leanpub (Includes a free 5-chapter preview edition!)

The full source code is also open-sourced on GitHub:

👉 bnpysse/erth_assistant on GitHub

Stay tuned for Part 2!

Top comments (0)