System Overview & Architecture
Developer wiki for the benchspace / doppiobox platform. Start here. This page gives you the whole mental model in one read; every later page drills into one box on the diagrams below.
1. What this is, in plain terms
doppiobox (the product; codename / Frappe app name benchspace) is a cloud
development-environment service for Frappe developers. It is "Frappe Cloud, but for
development instead of production hosting" — the gap the founding note calls out
directly: Frappe Cloud hosts production sites, but there is no place to just sign up
and get a ready-to-code Frappe bench (backbone/notes/plan.md:1).
A user signs up, clicks a button, and ~20 seconds later gets a Dev Box: an isolated machine in the browser with
- a full Frappe bench + a running site, reachable at a real HTTPS URL, and
- VS Code in the browser (code-server) wired to that same bench, with a terminal showing live bench logs and the ability to push to GitHub.
Edit a file in the VS Code tab, refresh the site tab, the change is live
(backbone/notes/plan.md "Overview"). Each Dev Box is backed by its own
Firecracker microVM running on a Hetzner dedicated server — hard isolation
between tenants, not just a shared-kernel container.
Heads-up on one stale doc:
backbone/agent/architecture.mdstill describes an earlier Docker + Traefik design. The system has since moved to Firecracker microVMs + Caddy (see the rootCLAUDE.mdserver/URL tables, and the live Go code underbackbone/agent/internal/firecracker/). Where this page and that file disagree, this page andCLAUDE.mdare current. (Adockerruntime backend does still exist at the code level — theServerDocType'sruntime_backendfield and the Ansibletraefik/docker_imagesroles — but the production servers run the Firecracker + Caddy path;architecture.mddocuments only the older Docker design.)
2. The five subsystems
The repo is one monorepo with five cooperating parts. Each has one job.
| # | Subsystem | Lives in | Language | Responsibility (one line) |
|---|---|---|---|---|
| 1 | Frappe control-plane | benchspace/ |
Python (Frappe) | The brain: stores intent (DocTypes), billing, the task queue. Runs at boxes.buildwithhussain.com. |
| 2 | Go devbox-agent | backbone/agent/ |
Go | The hands: runs on each Hetzner server, provisions Firecracker VMs, programs Caddy routes. |
| 3 | React dashboard | dashboard/ |
React + Vite + TS | The product UI users actually click. |
| 4 | Python CLI | cli/ |
Python | Terminal client for power users / scripting. |
| 5 | Ansible / deploy backbone | backbone/ansible/, scripts/ |
Ansible + Bash | Server provisioning + agent deployment. |
2.1 Frappe control-plane (the brain) — benchspace/
The production-facing URL is https://boxes.buildwithhussain.com (CLAUDE.md,
"Production Frappe"). It is a standard Frappe app. It owns:
- The data model —
Dev Box,Server,Agent Job,Devbox Template,Doppio Customer, plans/subscriptions, wallet, GitHub tokens (benchspace/bench_space/doctype/). See page 07 — Frappe data model. - Billing & subscriptions — wallet, plans, trial/expiry handling
(
benchspace/api/billing.py,benchspace/api/wallet.py). See page 08 — Billing. - Auth & GitHub — login, role assignment, GitHub App flow
(
benchspace/api/auth.py,benchspace/api/github.py). See page 09 — Auth & GitHub. - The task queue — it never SSHes into a server. It writes an Agent Job row describing what should happen and lets the agent pull it.
It does not touch Firecracker, Caddy, or the servers directly.
2.2 Go devbox-agent (the hands) — backbone/agent/
A single static Go binary that runs on every Hetzner server at
/opt/devbox/agent/devbox-agent (CLAUDE.md, "Agent"). It:
- polls the Frappe control-plane every ~10s for pending Agent Jobs,
- executes them by driving Firecracker (create/start/stop/delete/reset a VM,
build the golden snapshot, build an image — see the action switch at
backbone/agent/internal/worker/worker.go:182), - programs Caddy to route the VM's URLs, and
- reports status back so Frappe's view of reality stays correct.
It keeps its own local SQLite DB as a durable work queue and reconciles crashed
VMs on startup (
backbone/agent/internal/worker/worker.go:58,:106). See page 04 — Go agent internals, page 02 — Firecracker, page 03 — VM lifecycle & golden snapshot, page 05 — Networking & Caddy.
2.3 React dashboard (the product UI) — dashboard/
Vite + React + TypeScript SPA (dashboard/src/, dashboard/vite.config.ts). Served
by Frappe under the /dashboard/<path> route rule
(benchspace/hooks.py:258). This is what end users see — create a box, watch it come
up, open VS Code, manage billing. See page 10 — Dashboard.
2.4 Python CLI — cli/
A terminal client (cli/benchspace_cli/, modules main, api, config,
context). Talks to the same Frappe API the dashboard uses, for users who'd rather
script their boxes than click. The package (cli/benchspace_cli/) is shipped as
compiled bytecode; its modules are main, api, config, and context. See
page 11 — CLI.
2.5 Ansible / deploy backbone — backbone/ansible/, scripts/
Turns a bare Hetzner box (rescue mode) into a running node: hardening, Firecracker,
the kernel + base rootfs, Caddy, and the agent. Frappe runs these playbooks
in-process via the embedded Ansible runner in benchspace/runner.py — the
Server DocType's "Prepare Server" button enqueues a background job that drives a
playbook and streams progress into Ansible Play / Ansible Task rows
(benchspace/runner.py:226, :319). Day-2 agent updates use
scripts/deploy-agent.sh. See page 12 — Deployment & Ansible and
backbone/ansible/SERVER_SETUP.md.
3. Control-plane vs data-plane: the poll-based split
This is the single most important idea in the system.
- Control-plane = Frappe. It stores intent: a
Dev Boxdocument plus anAgent Jobrow that says "create boxabc123on serverfc-01." - Data-plane = the Go agent on each server. Every ~10s it polls Frappe, picks up
pending jobs, and makes reality match the intent — creating the VM, wiring
Caddy, then reporting status back (
CLAUDE.md, "Production Frappe": "The agent polls Frappe every 10s for pending tasks.").
Frappe writes what should be true. The agent makes it true and writes back what is true. They never share a process or a database.
Why poll, not push?
The control-plane never opens a connection into a server. The agent reaches out.
That inversion buys a lot (backbone/agent/architecture.md:8 "Why Pull, Not Push"):
- Servers sit behind NAT with no inbound access. Frappe SSHing in to run VM commands would require open inbound ports on every node — a security and ops liability. With polling, only outbound HTTPS from the node is needed.
- Resilience. If the network blips or the agent restarts, nothing is lost — the
job is still
pendingin Frappe and still queued in the agent's local SQLite. The agent resets any half-donerunningtask back topendingon startup (crash recovery) and resumes (backbone/agent/architecture.md:85). - Simplicity & security. No long-lived inbound control channel, no orchestrator
holding live SSH sessions. One bearer token authenticates the agent's outbound
calls; the agent's own HTTP API is firewalled
(
backbone/notes/frappe-app-plan.md:63"Agent Auth").
The trade-off is latency: changes apply within one poll interval (~10s), not instantly. For "spin up a dev box," that is invisible.
4. The servers and URL patterns
Two physical Hetzner dedicated servers today (CLAUDE.md, "Servers"):
| Role | Box | IP | Purpose | .env prefix |
|---|---|---|---|---|
| Production | AX42 | 95.217.119.91 |
Live users | HETZNER_DEDICATED_SERVER_* |
| Test | AX41 | 95.217.148.61 |
Dev / testing | HETZNER_TEST_SERVER_* |
Default to the test server for all dev work. Only touch production when explicitly deploying (
CLAUDE.md).
Each server has a domain (e.g. fc-01.doppiobox.cloud) backed by wildcard DNS.
Every Dev Box gets a short slug; URLs are derived from slug + the server domain
(CLAUDE.md, "URL patterns"; built in the agent at
backbone/agent/internal/firecracker/mmds.go):
| Pattern | Goes to | Example |
|---|---|---|
{slug}.site.{domain} |
Frappe site (port 8000) + socket.io (9000) | abc123.site.fc-01.doppiobox.cloud |
{slug}.code.{domain} |
code-server / VS Code (port 8080) | abc123.code.fc-01.doppiobox.cloud |
{port}-{slug}.code.{domain} |
arbitrary dev port inside the box | 8081-abc123.code.fc-01.doppiobox.cloud |
Wildcard DNS (*.site.{domain}, *.code.{domain}) resolves everything to the
server's IP; Caddy inspects the hostname and proxies to the right VM, minting TLS
certs on demand (CLAUDE.md, "Caddy"). See page 05 — Networking & Caddy.
5. Architecture at a glance
(a) Component view — who talks to whom
(b) "Create a box" — high-level teaser
The full, blow-by-blow version (Agent Job states, MMDS, golden-snapshot restore, Caddy route insert) lives on page 06 — Task orchestration. Here's the shape:
6. Glossary
One line each; follow the link for the deep dive.
| Term | Meaning | More on |
|---|---|---|
| Dev Box | The product unit: one user's isolated Frappe bench + VS Code, backed by one microVM. Frappe DocType Dev Box (benchspace/bench_space/doctype/dev_box/). |
page 07 |
| Agent Job | A control-plane row describing one unit of work (create/start/stop/delete/reset…) for the agent to pull. The intent-vs-reality handoff. DocType Agent Job, written at dev_box.py:218. |
page 06 |
| Firecracker microVM | AWS's lightweight VMM. Each Dev Box runs in its own microVM for hard isolation (not a shared-kernel container). | page 02 |
| Golden snapshot | A pre-booted reference VM memory+disk snapshot. New boxes clone from it in ~20s instead of cold-booting; built by the agent (backbone/agent/internal/firecracker/golden.go). |
page 03 |
| Overlay | Per-VM copy-on-write rootfs layered over the shared base image (/opt/devbox/overlays/), so VMs share the base read-only and only store their diffs. |
page 02 / 03 |
| MMDS | Firecracker's Microvm Metadata Service (IMDS-shaped, 169.254.169.254). The agent PUTs slug, URLs, and env into it; the guest reads it on boot to configure itself (backbone/agent/internal/firecracker/mmds.go). |
page 02 |
| Slug | Short auto-generated id (8-char) naming a Dev Box; the subdomain key in every URL ({slug}.site.{domain}). |
page 07 |
| Devbox Template | A reusable box recipe (which apps/image to seed). DocTypes Devbox Template + Devbox Template App. |
page 07 |
| Pool box | A pre-warmed, unowned Dev Box kept ready so claims are instant (~3s restore vs ~20s clone). is_pool flag + replenish_pool scheduler (dev_box.py:315, hooks.py:154). |
page 03 / 06 |
| Plan / Subscription | Billing entities: a Dev Box Plan priced offering and a Dev Box Subscription tying a customer to it, with trial/expiry handling (benchspace/api/billing.py). |
page 08 |
| Doppio Customer | The billing/account identity for a user (DocType Doppio Customer); wallet + subscriptions hang off it. |
page 08 |
| Server | A Hetzner node DocType holding IP, domain, agent port + token, prepared via the Ansible runner (runner.py). |
page 12 |
| Caddy | Reverse proxy on each server; on-demand TLS; routes added/removed live via its admin API (:2019). |
page 05 |
| Ansible Play / Task | Per-run records the embedded Ansible runner writes so server provisioning is observable in Frappe (runner.py:48). |
page 12 |
7. Where to go next
| Page | Topic |
|---|---|
| 02 | Firecracker (microVMs, rootfs, MMDS) |
| 03 | VM lifecycle & golden snapshot (clone, pool, restore) |
| 04 | Go agent internals (worker loop, SQLite queue, reconcile) |
| 05 | Networking & Caddy (wildcard DNS, on-demand TLS, port routing) |
| 06 | Task orchestration (Agent Job lifecycle end-to-end) |
| 07 | Frappe data model (Dev Box, Server, Template, …) |
| 08 | Billing (plans, subscriptions, wallet, trials) |
| 09 | Auth & GitHub (login, roles, GitHub App) |
| 10 | Dashboard (React SPA) |
| 11 | CLI (Python terminal client) |
| 12 | Deployment & Ansible (server provisioning, agent deploy) |