Part 5 of the ERTH Architecture Series: Eliminating complex Javascript bundlers by rendering HTML fragments directly from Python inside a native WebView.
In the fourth part of this series, we integrated Turso and SQLModel into our Python sidecar, establishing a local-first persistent data channel with offline capabilities and cloud sync.
Now, we have a secure backend running on a dynamic port, protected by a watchdog and backed by database replication.
The last remaining step is to build the user interface.
Traditionally, this means setting up Node.js, running npm install, configuring Vite or Webpack, choosing React or Vue, setting up state management (Redux/Zustand), and writing boilerplate JSON API endpoints just to pass data back and forth. This frontend tax slows down development and inflates app bundle sizes.
What if we could skip the build pipeline entirely? What if our Python backend could return HTML fragments directly to the WebView, updating the interface instantly without full page reloads?
In this final post, we will integrate HTMX into our ERTH Stack (ElectroBun + Robyn + Turso + HTMX) to achieve a Zero-Build, Server-Driven UI.
The Hypermedia Exchange
Rather than fetching JSON and parsing it on the client, HTMX allows us to make AJAX requests directly from HTML attributes. The server processes the request, renders an HTML fragment, and returns it. HTMX then swaps this fragment directly into the DOM.
Here is the hypermedia lifecycle:
Let's build this.
Step 1: Writing Server-Driven UI in Python
In our Robyn backend, we implement a routing structure that supports Dual-Mode Responses. If a request comes from HTMX (indicated by the hx-request header), we render HTML. If it is a standard request, we return JSON (perfect for CLI testing and APIs).
Here is the task toggle route in Python:
# backend/app.py
from robyn import Robyn, Request, Response
from db import toggle_todo_status
app = Robyn(__file__)
def render_todo_item(todo: dict) -> str:
"""Renders a single todo list item as an HTML fragment"""
completed_class = "completed" if todo["is_completed"] else ""
checked_attr = "checked" if todo["is_completed"] else ""
return f"""
<div class="todo-item {completed_class}" id="todo-{todo['id']}">
<input type="checkbox"
{checked_attr}
hx-put="/api/v1/todos/{todo['id']}/toggle"
hx-target="#todo-{todo['id']}"
hx-swap="outerHTML">
<span>{todo['title']}</span>
</div>
"""
@app.put("/api/v1/todos/:id/toggle")
async def toggle_todo(request: Request):
todo_id = request.path_params.get("id")
updated_todo = await toggle_todo_status(todo_id)
if not updated_todo:
return Response(status_code=404, description="Todo not found")
is_htmx = request.headers.get("hx-request") == "true"
if is_htmx:
# Return pure HTML fragment for HTMX to swap in the DOM
return Response(
status_code=200,
headers={"Content-Type": "text/html; charset=utf-8"},
description=render_todo_item(updated_todo)
)
else:
# Standard REST response
import json
return Response(
status_code=200,
headers={"Content-Type": "application/json"},
description=json.dumps(updated_todo)
)
Step 2: Designing the Zero-Build Frontend
On the frontend side inside our ElectroBun container, our index.html is extremely simple. We link directly to the local htmx.min.js file. There are no build tools, loaders, or packagers.
<!-- views/main/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ERTH Assistant</title>
<script src="js/htmx.min.js"></script>
<style>
.todo-item { display: flex; align-items: center; gap: 10px; padding: 8px; }
.todo-item.completed span { text-decoration: line-through; color: #888; }
</style>
</head>
<body>
<h1>Tasks</h1>
<!-- Loaded dynamically when the watchdog negotiates the dynamic port -->
<div id="todo-list" hx-get="/api/v1/todos" hx-trigger="backend-ready from:body">
<p>Connecting to backend engine...</p>
</div>
<script>
// Automatically configure HTMX requests to inject security tokens and redirect to dynamic port
document.addEventListener('htmx:configRequest', (evt) => {
if (window.__ENV__ && window.__ENV__.BACKEND_PORT) {
const port = window.__ENV__.BACKEND_PORT;
if (evt.detail.path.startsWith('/')) {
evt.detail.path = `http://127.0.0.1:${port}${evt.detail.path}`;
}
}
if (window.__ENV__ && window.__ENV__.TOKEN) {
evt.detail.headers['Authorization'] = `Bearer ${window.__ENV__.TOKEN}`;
}
});
</script>
</body>
</html>
When the Bun process dispatches the backend-ready event, HTMX instantly triggers a GET request to retrieve the tasks, rendering them in place. When a user clicks a checkbox, HTMX sends a PUT request carrying the Opaque Token, and swaps the returned task layout in milliseconds—without re-rendering the entire page.
Recap of the ERTH Desktop Architecture
Across these five posts, we have constructed a new desktop architecture that challenges the monopoly of Electron and Tauri:
- E - ElectroBun: Provides our lightweight frontend shell. It replaces Electron's bloated Chromium engine with native system WebViews (WebKit), cutting base memory usage to just 15MB.
- R - Robyn: Launches an async Python sidecar on a dynamic Port 0. It runs Actix-web under the hood, running heavy business logic and enabling seamless integration with local AI toolkits (Ollama, PyTorch).
- T - Turso (libSQL): Serves as our Local-First synchronization database. Combined with SQLModel, UUIDv7 indexing, and Tombstone deletion flags, it replicates data seamlessly between local SQLite and remote edge nodes.
- H - HTMX: Bypasses complex JavaScript build chains. It fetches HTML fragments directly from our Robyn backend, simplifying client state management.
The Complete Architecture Handbook
If you want to build this system yourself, containing all 16 chapters of step-by-step guides, system startup logs, and packaging guides (compiling to a single 128MB .app / .exe bundle), check out our handbook:
📖 ERTH Assistant: Local-First + AI Sidecar Desktop Architecture on Leanpub (Includes a free 5-chapter preview edition!)
The full source code is completely open-sourced on GitHub:
👉 bnpysse/erth_assistant on GitHub
Thank you for following this series, and happy hacking!

Top comments (1)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.