to select ↑↓ to navigate
Doppiobox

Doppiobox

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

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.md still describes an earlier Docker + Traefik design. The system has since moved to Firecracker microVMs + Caddy (see the root CLAUDE.md server/URL tables, and the live Go code under backbone/agent/internal/firecracker/). Where this page and that file disagree, this page and CLAUDE.md are current. (A docker runtime backend does still exist at the code level — the Server DocType's runtime_backend field and the Ansible traefik/docker_images roles — but the production servers run the Firecracker + Caddy path; architecture.md documents 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 modelDev 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 Box document plus an Agent Job row that says "create box abc123 on server fc-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"):

  1. 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.
  2. Resilience. If the network blips or the agent restarts, nothing is lost — the job is still pending in Frappe and still queued in the agent's local SQLite. The agent resets any half-done running task back to pending on startup (crash recovery) and resumes (backbone/agent/architecture.md:85).
  3. 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

diagram

(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:

diagram


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)
Last updated 1 month ago
Was this helpful?
Thanks!