<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: 木头人</title>
    <description>The latest articles on DEV Community by 木头人 (@_553af38aefbe6c8b9dbc9).</description>
    <link>https://dev.to/_553af38aefbe6c8b9dbc9</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1593910%2F502b3870-81ad-4aad-888d-1d2dbd0890ed.png</url>
      <title>DEV Community: 木头人</title>
      <link>https://dev.to/_553af38aefbe6c8b9dbc9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/_553af38aefbe6c8b9dbc9"/>
    <language>en</language>
    <item>
      <title>Ditch Electron: Spawning a Rust-Powered Python Sidecar from Bun</title>
      <dc:creator>木头人</dc:creator>
      <pubDate>Fri, 12 Jun 2026 15:51:24 +0000</pubDate>
      <link>https://dev.to/_553af38aefbe6c8b9dbc9/ditch-electron-spawning-a-rust-powered-python-sidecar-from-bun-1bj3</link>
      <guid>https://dev.to/_553af38aefbe6c8b9dbc9/ditch-electron-spawning-a-rust-powered-python-sidecar-from-bun-1bj3</guid>
      <description>&lt;h3&gt;
  
  
  Part 1 of the ERTH Architecture Series: Launching local backends on Port 0, dynamic port negotiation, and establishing the dual-core desktop backbone.
&lt;/h3&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb3hvdndekfqu25098oqe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb3hvdndekfqu25098oqe.png" alt=" " width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are building a modern desktop application, you are probably tired of the same old options. &lt;/p&gt;

&lt;p&gt;On one hand, you have &lt;strong&gt;Electron&lt;/strong&gt;. 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.&lt;/p&gt;

&lt;p&gt;On the other hand, you have &lt;strong&gt;Tauri&lt;/strong&gt;. 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.&lt;/p&gt;

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

&lt;p&gt;Welcome to the &lt;strong&gt;ERTH Stack&lt;/strong&gt; (&lt;strong&gt;E&lt;/strong&gt;lectroBun + &lt;strong&gt;R&lt;/strong&gt;obyn + &lt;strong&gt;T&lt;/strong&gt;urso + &lt;strong&gt;H&lt;/strong&gt;TMX). 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.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Concept: Heterogeneous Dual-Core
&lt;/h2&gt;

&lt;p&gt;In the ERTH architecture, the desktop app is split into two physical processes:&lt;/p&gt;

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

&lt;p&gt;Here is how the lifecycle and process boundaries interact:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjkrjnvta50yueltrhdrh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjkrjnvta50yueltrhdrh.png" alt=" " width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Spawning the Robyn Sidecar from Bun
&lt;/h2&gt;

&lt;p&gt;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 &lt;code&gt;8080&lt;/code&gt;, and the user already has a service running on that port, your application will crash instantly.&lt;/p&gt;

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

&lt;p&gt;Here is the production-grade TypeScript code in Bun to spawn the Robyn child process and dynamically intercept the allocated port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src-app/frontend/src/bun/index.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;backendPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;portFound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Resolve the path to the packaged Python binary or local script&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pythonAppPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;backend&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;app.py&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;backendProcess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;uv&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;python&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pythonAppPath&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pipe&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// We need to read the stdout log stream&lt;/span&gt;
  &lt;span class="na"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inherit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ROBYN_PORT&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Force Robyn to bind to a dynamic port&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Create a reader to parse stdout line by line&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;backendProcess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// keep the last partial line in buffer&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`[Backend Log] &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

      &lt;span class="c1"&gt;// Look for Actix/Robyn startup signature: "listening on: 127.0.0.1:XXXXX"&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/listening on:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+127&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;1:&lt;/span&gt;&lt;span class="se"&gt;(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;backendPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;portFound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`🚀 Watchdog intercepted backend port: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;backendPort&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Notify the WebView window that the communication channel is ready&lt;/span&gt;
        &lt;span class="nf"&gt;initializeWebView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;backendPort&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;portFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Setting up Robyn on Port 0
&lt;/h2&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# src-app/backend/app.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;robyn&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Robyn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Robyn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/v1/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health_check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Get port from environment or fallback to 0
&lt;/span&gt;    &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ROBYN_PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Real-World Output
&lt;/h2&gt;

&lt;p&gt;When you run this architecture, the terminal logs show the magic of dynamic negotiation in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/images%2Fch6_01_port_collision_resolved.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/images%2Fch6_01_port_collision_resolved.png" alt="Watchdog capturing dynamic port terminal logs" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Robyn outputs: &lt;code&gt;Starting server at http://127.0.0.1:0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt; The OS intercepts and binds it to &lt;code&gt;127.0.0.1:57220&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Bun catches the &lt;code&gt;57220&lt;/code&gt; port from the log stream, mounts the WebView, and binds all future HTMX requests to &lt;code&gt;http://127.0.0.1:57220&lt;/code&gt;. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No port collision crashes. No manual user configuration. It just works.&lt;/p&gt;




&lt;h2&gt;
  
  
  What’s Next?
&lt;/h2&gt;

&lt;p&gt;We now have a Bun frontend shell connected to a Python sidecar. But running multiple processes introduces new architectural failure points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;em&gt;What happens if the Python process crashes or is killed by the system?&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;How do we prevent other local programs from scanning ports and hijacking our Python API?&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next post, we will cover the &lt;strong&gt;Watchdog Heartbeat Pipeline&lt;/strong&gt; and &lt;strong&gt;Opaque Token Security Interceptors&lt;/strong&gt; to make this dual-core setup industrial-grade and secure.&lt;/p&gt;

&lt;p&gt;If you want to skip ahead and read the full blueprint immediately, check out the companion book:&lt;/p&gt;

&lt;p&gt;📖 &lt;strong&gt;&lt;a href="https://leanpub.com/erth_assistant" rel="noopener noreferrer"&gt;ERTH Assistant: Local-First + AI Sidecar Desktop Architecture on Leanpub&lt;/a&gt;&lt;/strong&gt; (Includes a free 5-chapter preview edition!)&lt;/p&gt;

&lt;p&gt;The full source code is also open-sourced on GitHub:&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/bnpysse/erth_assistant" rel="noopener noreferrer"&gt;bnpysse/erth_assistant on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stay tuned for Part 2!&lt;/p&gt;

</description>
      <category>eletrobun</category>
      <category>robyn</category>
      <category>turso</category>
      <category>htmx</category>
    </item>
    <item>
      <title>Why I Abandoned Electron &amp; SPAs to Build a 128MB Local-First Desktop AI Agent</title>
      <dc:creator>木头人</dc:creator>
      <pubDate>Fri, 12 Jun 2026 15:51:04 +0000</pubDate>
      <link>https://dev.to/_553af38aefbe6c8b9dbc9/why-i-abandoned-electron-spas-to-build-a-128mb-local-first-desktop-ai-agent-ahe</link>
      <guid>https://dev.to/_553af38aefbe6c8b9dbc9/why-i-abandoned-electron-spas-to-build-a-128mb-local-first-desktop-ai-agent-ahe</guid>
      <description>&lt;h3&gt;
  
  
  How the ERTH Architecture (ElectroBun + Robyn + Turso + HTMX) breaks the obesity of modern desktop app development.
&lt;/h3&gt;




&lt;p&gt;If you’ve tried to build a cross-platform desktop application recently, you’ve likely faced the classic developer’s dilemma:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Electron&lt;/strong&gt; makes developer velocity fast, but at the cost of dragging a bloated Chromium kernel and Node.js runtime into every build. A simple "Hello World" easily eats up 200MB+ of disk space and hundreds of megabytes of RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tauri&lt;/strong&gt; solves the footprint issue by using OS-native WebViews and Rust, but forces you onto Rust’s steep learning curve, sacrificing the agility of rapid prototyping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Python Distribution Hell&lt;/strong&gt;: With the rise of local LLMs and Edge AI, Python is the de facto language for AI orchestration. Yet, packaging Python, its heavy dependencies, and databases into a double-click-to-run package for non-technical users remains a nightmare.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Faced with these shackles, I decided to take a step back and rewrite the physical laws of desktop development. &lt;/p&gt;

&lt;p&gt;Today, I’m introducing the &lt;strong&gt;ERTH Stack&lt;/strong&gt; (&lt;strong&gt;E&lt;/strong&gt;lectroBun + &lt;strong&gt;R&lt;/strong&gt;obyn + &lt;strong&gt;T&lt;/strong&gt;urso + &lt;strong&gt;H&lt;/strong&gt;TMX)—a heterogeneous, local-first, zero-JS desktop application architecture designed for independent full-stack creators. &lt;/p&gt;

&lt;p&gt;And yes, the entire bundle—including a browser shell, a high-performance Python sidecar, a local database, and local AI agent execution—packages into a single &lt;strong&gt;128MB&lt;/strong&gt; standalone binary.&lt;/p&gt;

&lt;p&gt;Our open-source implementation is live on GitHub:&lt;br&gt;&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/bnpysse/erth_assistant" rel="noopener noreferrer"&gt;GitHub Repository: bnpysse/erth_assistant&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  The Core Pillars of the ERTH Architecture
&lt;/h2&gt;

&lt;p&gt;To achieve a minimalist footprint without sacrificing developer velocity, we structured the architecture into four core layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;              ERTH ARCHITECTURE
  [E]lectroBun (UI Shell) ───[HTMX]───► [H]TMX (Zero-JS Frontend)
        │                                    ▲
      (IPC)                               (HTTP)
        ▼                                    │
  [R]obyn (Python Sidecar) ───────────► [T]urso / libSQL (Local-First DB)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  1. [E]lectroBun: The Lightweight Shell
&lt;/h3&gt;

&lt;p&gt;Instead of Electron’s heavy Chromium, &lt;strong&gt;ElectroBun&lt;/strong&gt; binds directly to the OS-native WebKit engine (Cocoa WebKit on macOS, WebView2 on Windows). The main process runs on Bun, launching in milliseconds and keeping idle memory consumption incredibly low.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. [R]obyn: The Rust-Powered Python Sidecar
&lt;/h3&gt;

&lt;p&gt;Python handles the heavy lifting—AI logic, local LLM mounting, and database orchestration. We use &lt;strong&gt;Robyn&lt;/strong&gt;, a high-performance, async Python web framework built on a Rust runtime. It runs as a background sidecar process, dynamically spawned by the Bun main process on &lt;code&gt;Port 0&lt;/code&gt; to avoid port collision conflicts.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. [T]urso: Local-First with Cloud-Edge Sync
&lt;/h3&gt;

&lt;p&gt;We use &lt;strong&gt;libSQL (Turso)&lt;/strong&gt;. The database is a physical file residing locally (&lt;code&gt;local_edge.db&lt;/code&gt;), executing CRUD operations at a blinding 0.1ms latency. Meanwhile, a silent background thread replicates incremental changes to the Turso cloud database, providing seamless offline immunity.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. [H]TMX: Hypermedia-Driven UI
&lt;/h3&gt;

&lt;p&gt;We threw client-side SPAs (React/Vue) and Webpack/Vite pipelines out of the window. By introducing &lt;strong&gt;HTMX&lt;/strong&gt;, our frontend behaves as a lightweight hypermedia receiver. The Robyn Python backend renders HTML fragments directly, and HTMX swaps them into the DOM out-of-band (OOB Swap). &lt;strong&gt;Zero client-side JS business logic, zero state-sync headaches.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Surviving the Deep-Water Debugging Zone
&lt;/h2&gt;

&lt;p&gt;Building a heterogeneous cross-language desktop app is not without its traps. During development, we had to solve three major technical challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watchdog Self-Healing Pipeline&lt;/strong&gt;: Since the app runs a Bun main process and a Python sidecar, what happens if the Python backend crashes? We built a watchdog monitoring pipeline that probes the backend every 3 seconds. If the backend dies, the watchdog executes a silent &lt;code&gt;SIGTERM&lt;/code&gt; restart and re-negotiates a new dynamic port seamlessly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CORS &amp;amp; OPTIONS Preflight Trap&lt;/strong&gt;: Since HTMX sends cross-origin requests with custom headers (&lt;code&gt;hx-request&lt;/code&gt;, &lt;code&gt;hx-target&lt;/code&gt;), browsers trigger automatic &lt;code&gt;OPTIONS&lt;/code&gt; preflight checks. In Robyn’s Rust-based core, we had to ensure preflight requests bypass the auth middleware to prevent socket deadlocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opaque Token Interception&lt;/strong&gt;: To prevent other local programs from scanning ports and hijacking our Python backend, communication is locked down with UUIDv7 short-lived tokens (Opaque Tokens) injected during startup.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Read the Full Engineering Blueprint
&lt;/h2&gt;

&lt;p&gt;I have documented the entire process—from setting up the dual-core ignition, building the self-healing watchdog, writing Cocoa NSPanel window float hooks, to building cross-platform GitHub Actions pipelines—in my new book:&lt;/p&gt;

&lt;p&gt;📖 &lt;strong&gt;&lt;a href="https://leanpub.com/erth_assistant" rel="noopener noreferrer"&gt;"ERTH Assistant: Local-First + AI Sidecar Desktop Architecture"&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you want to build high-performance, secure desktop AI assistants, you can get the book on Leanpub. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I have also made a Free 5-Chapter Preview Edition available for download directly on the landing page!&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Let me know what you think of the ERTH stack in the comments. Let's reclaim local computing sovereignty together!&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>electrobun</category>
      <category>robyn</category>
      <category>turso</category>
      <category>htmx</category>
    </item>
  </channel>
</rss>
