I gave The Factory one requirement: build your own operator console, HATEOAS patterns only. Then I went to sleep.
Last week I wrote about why The Factory exists — the dark factory pattern, Jujutsu as the logistics network, 20 concurrent agents building software on an assembly line. If you haven't read that one, start there.
This post is about what happened when I deployed The Factory on my home server, Tailscale'd into it from my laptop, dropped a single requirement into the work graph — "build out the web UI following HATEOAS patterns" — and let it run.
I woke up to an operator console with 11 page templates, 8 fragment templates, and a JSON API with _links on every resource. No React. No TypeScript. No node_modules. The factory built its own control room.

That outcome — and specifically how the factory interpreted that requirement — is what this post is about.
The Temptation
When you're building a system that orchestrates AI agents, runs production lines, manages work items and blueprints, the obvious move for the UI is a React app. Or Svelte. Or Vue. Something that fetches data from your API, stores it in client-side state, and updates the view when things change.
You'd have a store. You'd have WebSocket subscriptions for live updates. You'd have loading states, error states, optimistic updates. You'd have a frontend team (or at least, a frontend hat to wear) keeping the client model in sync with the server model.
I've built that. More times than I care to count. And every single time, the bugs live in the gap between what the client thinks the state is and what the server knows it to be.
"Surely there's a better way"
There Is No State
The Factory's core principle is that disk is the only truth. The factory floor is stateless between ticks. Agents crash and restart; they read disk and continue. The jj repository and the work graph JSON files are the only durable things in the system. Everything else is ephemeral.
The operator console — the web UI — follows the exact same rule. There is no client state. There is no JavaScript tracking what's paused, what's running, what items need approval. The server knows. The server always knows. The client just asks.
This isn't a novel idea. It's how the web was supposed to work before we decided to move the application logic into the browser. It's called HATEOAS — Hypermedia As The Engine Of Application State — and it sounds academic until you actually build something with it and realize how many entire categories of bugs simply don't exist.
Askama + HTMX: The Stack
The web layer is built in Axum (Rust's async web framework), with Askama for server-side templating and HTMX for interactions. No TypeScript. No build step. No node_modules.
Askama compiles templates at build time. You define a Rust struct, derive Template, and point it at an HTML file. The compiler verifies that every variable referenced in the template actually exists on the struct. If you rename a field and forget to update the template, the build fails. It's the same Rust compiler guaranteeing correctness across the HTML layer that guarantees it everywhere else.
HTMX handles the interactive bits. Instead of writing JavaScript to POST a request and update the DOM, you put the instructions directly on the HTML element:
<button
hx-post="/lines/{{ line_id }}/pause"
hx-target="this"
hx-swap="outerHTML"
>
Pause
</button>
When the user clicks Pause, HTMX intercepts, POSTs to the server, and replaces the button with whatever the server returns. What does the server return? A different button:
<button
hx-post="/lines/{{ line_id }}/resume"
hx-target="this"
hx-swap="outerHTML"
>
Resume
</button>
That's it. No client state tracking whether the line is paused. No JavaScript variable isPaused = true. The button you see IS the current state. The action on the button IS the next available action. The server decided both.
HATEOAS: Discovery Over Convention
The JSON API follows the same principle. Every resource includes a _links block that tells the client what actions are available on that resource, right now, given its current state.
A work item that's sitting at a human approval gate looks like this:
{
"id": "item-abc123",
"state": "WaitingForApproval",
"gate": true,
"_links": {
"self": { "href": "/api/items/item-abc123" },
"approve": { "href": "/api/items/item-abc123/approve", "method": "POST" },
"production_line": { "href": "/api/lines/line-xyz" }
}
}
The same item, after approval:
{
"id": "item-abc123",
"state": "InProgress",
"gate": false,
"_links": {
"self": { "href": "/api/items/item-abc123" },
"production_line": { "href": "/api/lines/line-xyz" }
}
}
The approve link is gone. Not because the client removed it, not because some JavaScript checked if (item.gate). Because the server looked at the item's current state and decided what actions make sense. The client didn't need to know the rules. It just needed to follow the links.
This is the core HATEOAS insight: the client shouldn't encode business logic about what's possible. The server should tell it. When the rules change, you update the server. The client adapts automatically.
The Button That Swaps Itself
My favorite implementation detail in the whole codebase is the pause/resume button swap. It's a small thing that perfectly illustrates the philosophy.
The production line detail page renders a Pause button. User clicks it. HTMX fires a POST to /lines/{id}/pause. The Axum handler loads the line from disk, calls line.pause(), saves it back, and returns a Pause Button Fragment — which renders a Resume button. HTMX swaps outerHTML. The Pause button is gone. The Resume button is there.
No state. No synchronization. The DOM is the display; the server is the source of truth; the transition happens in one round trip with zero client logic.
The same pattern handles gate item approvals. An item waiting for a human to sign off shows an Approve button. You click it. The server marks the item approved, and the fragment it returns is a simple badge:
<span class="badge badge-active">Approved ✓</span>
The button is replaced by a static label. No more action available. The server decided. The client reflects it.
What "No State" Actually Eliminates
When I say there's no client state, I mean there's a whole class of bugs that simply cannot exist:
- Stale state: the client thinks the line is running, the server knows it crashed. Can't happen — the client asks the server every time.
- Sync bugs: the client updated its local model but the server rejected the write. Can't happen — the client has no local model.
- Race conditions: two tabs both think they can approve an item. The server handles it with a single write; one gets the badge, one gets an error fragment. No corrupted state.
- Hydration mismatches: client-rendered HTML doesn't match server-rendered HTML. Can't happen — there's only server-rendered HTML.
The dashboard polls for status every five seconds with a single HTMX attribute:
<div
hx-get="/fragments/status-summary"
hx-trigger="every 5s"
hx-swap="innerHTML"
></div>
No WebSocket connection to manage. No subscription to unsubscribe from. No memory leak when the component unmounts. Every five seconds, the server renders a fresh fragment, HTMX swaps it in. That's the whole thing.
The Factory Reflects Itself
What I find genuinely elegant about this — and I don't use that word loosely — is that the UI architecture is a direct expression of the factory's core philosophy.
The factory floor is stateless between ticks. The operator console is stateless between requests.
The factory recovers from crashes by reading disk. The operator console recovers from anything by reloading the page.
The factory's jj substrate is the only durable truth. The operator console's server is the only durable truth.
The factory doesn't care if an agent crashes mid-operation; the work graph on disk tells it where to resume. The operator console doesn't care if your browser tab dies; the next request gets fresh data.
You can describe the entire system — backend and frontend — with a single sentence: disk is truth, everything else is ephemeral.
That consistency isn't accidental. When the philosophy is right, it propagates. The architecture wants to be coherent.
What You Don't Need
No React. No Vue. No Svelte. No Redux, Zustand, Jotai, or whatever the state management library of the week is. No TypeScript compiler. No npm install. No node_modules. No build pipeline for the frontend. No source maps. No bundle analyzer. No hydration. No SSR/CSR tradeoff to agonize over.
The templates compile with the Rust binary. The HTML is valid HTML. HTMX is a single script tag loaded from a CDN. Mermaid.js renders the blueprint DAG visualizations. That's the entire frontend stack.

When you deploy The Factory, you deploy one binary. It serves the HTML, it handles the API, it runs the production floor. The operator console isn't a separate service. It's just the web layer of the same system.
"But What About..."
Yes, there are things this approach doesn't do well. Rich real-time collaboration, complex client-side data visualization, anything that needs sub-100ms local reactivity — those are legitimately better served by a proper frontend framework.
The Factory's operator console doesn't need any of that. It's a control room, not a real-time game. Operators check status, approve gates, pause lines, inspect dropped items. Five-second polling is fast enough. Full page renders are fast enough. Server-authoritative state is exactly right.
The right tool for the job. The job here is an operator console for an autonomous factory. The tool is hypermedia.
The Factory Keeps Growing
The system currently has 11 page templates, 8 fragment templates, and a JSON API with _links on every resource.
I gave The Factory that requirement on a Tuesday night. I Tailscale'd into the server Wednesday morning, opened the web UI, and it was there. The factory had read the requirement, decomposed it, and implemented it. Every design decision — Askama over a JS framework, HTMX for interactions, _links on every resource, button fragments instead of client state — the factory made those calls on its own, because the requirement said HATEOAS and HATEOAS implies all of them.
Which is, honestly, the entire point. The factory understood the philosophy well enough to implement it correctly. No frontend team required. No Ona session on a plane. Just a requirement, a running instance, and disk as the source of truth.
See you next time reader, Shardul
Published: March 9, 2026