# Hardware — the idea-to-reality pipeline (build knowledge)

How Leumas takes a sentence ("a blinky board with an ESP32 and a temperature sensor") to a zip a fab house will build. Read this before touching Circuit Studio, the fabrication lane, or anything...


How Leumas takes a sentence ("a blinky board with an ESP32 and a temperature sensor") to a zip a fab
house will build. Read this before touching Circuit Studio, the fabrication lane, or anything under
`shared/engines/adapters/hardware`.

## The shape

```
IDEA ──► DESIGN ──────────► EDIT ─────────► CHECK ──────► PACKAGE ──────► PRICE ──► ORDER
        circuit-studio      live preview    validate      fab package     fab quote   (human,
        adapter (pure)      iframe          / DRC         (in-browser)    adapter     on vendor site)

  BROWSER                                   |   SERVER
  ─────────────────────────────────────────────────────────────────────────────────────────
  products/standalone/circuit-preview       |   shared/engines/adapters/hardware/*
    RunFrame (React 19 · three 0.165)       |     circuit-studio  26 tools (20 pure)
    evaluates the .circuit.tsx              |     fabrication      4 tools (all pure)
    owns the circuit-json                   |   shared/engines/workspace  (sandboxes, fork, snapshots)
    builds gerbers/BOM/PnP/SVG + thumbnail  |   products/leumas-api/src/routes/circuitStudio.js
        ▲ postMessage protocol ▼            |
  shared/packages/ui/src/circuit/CircuitCanvas.jsx
  products/leumas-studio/src/admin/hardware/circuit/  (gallery · editor · capabilities)
```

**The evaluated circuit-json lives in the browser, so the manufacturing lane does too.** That is the
single design decision everything else follows from: no `tsci` binary, no server process, so the whole
pipeline works for a hosted tenant exactly as it does on an appliance.

## Where things live

| Thing | Path |
|---|---|
| Design/edit/validate tools | `shared/engines/adapters/hardware/circuit-studio/` (`index.js` + `services/*`) |
| Fab quoting | `shared/engines/adapters/hardware/fabrication/` (`index.js`, `drivers/{jlcpcb,pcbway}.js`) |
| Preview + fab package | `products/standalone/circuit-preview/src/{main.jsx,fabPackage.js}` |
| Panels (data-agnostic) | `shared/packages/ui/src/circuit/` — Canvas · PhaseBar · PreviewToolbar · Source · Layout · Autoroute · DRC · Simulate · **ExportWizard** · **FabricatePanel** · ToolsDock |
| Studio surface | `products/leumas-studio/src/admin/hardware/circuit/` — `CircuitGallery` · `CircuitEditor` · `useProjectSandbox` · `useCircuitCapabilities` · `phase.js` |
| Projects on disk | `shared/engines/workspace/src/index.js` — `resolveSandbox` · `copySandbox` · `/api/projects` (+ fork, snapshots, quota) |
| API | `products/leumas-api/src/routes/circuitStudio.js` (`/api/circuit`), `routes/arduino.js` (`/api/arduino`) |
| Agent tools | `shared/engines/coding-agent/src/circuitToolset.js` (12) |

## The host ⇄ frame protocol

The preview is an iframe because `@tscircuit/runframe` vendors react-dom@19 and targets three@0.165,
while the monorepo is React 18 / three 0.180. Isolation is the point — `@leumas/ui` keeps **zero**
tscircuit dependencies.

```
→ circuit:render     { fsMap, mainComponentPath, activeTab }
→ circuit:export     { requestId, artifacts[], boardName }
→ circuit:thumbnail  { requestId }
← circuit:ready · circuit:json · circuit:edit-event · circuit:tab · circuit:error · circuit:run
← circuit:export-result { requestId, ok, base64, filename, manifest }
← circuit:thumbnail-result { requestId, dataUri }
```

`circuit:json` carries a **summary**, not the full graph (trace geometry is megabytes and it fires on
every eval). The summary must stay circuit-json-*shaped*, because the host filters it by `type`:
components for the Layout panel, `pcb_board` for dimensions, `pcb_trace` for "is it routed", nets for
stats. An earlier version kept only components and silently reported every board as 0 nets, never
routed and dimensionless. If you need a new fact on the host, add its element type to the summary.

## Gating — the free tier is the product

| Capability | Who has it | Gates |
|---|---|---|
| `hardware.studio` | **everyone** (`BASE_CAPABILITIES`) | `/api/circuit`, the gallery, the editor, export |
| `hardware.maxProjects` / `maxSketches` / `snapshots` | 3 free, 100/100/50 on the plan | `/api/projects` create + fork |
| `hardware.ai` | plan | Circuit Agent, Leviathan design tools |
| `hardware.fab` | plan | fab package + quotes |
| `hardware.cli` | plan | the 6 `tsci`-backed tools |

`/api/circuit` mounts on **every** role. It used to sit inside `if (caps.localHardware)` and 404 for
every hosted tenant; `pnpm check:hardware` fails the build if that returns. `/api/arduino` genuinely
is machine-local (it flashes a board over USB) and stays behind `caps.localHardware`.

Quotas are only enforced when the capability key is actually present in the resolved map —
`limitFrom` returns 0 both for "denied" and "never configured", and treating the second as a limit
would lock everyone out at zero. That is why the coding-agent builder (`builder.maxProjects`, which
nobody configures) stays unmetered.

## Extending it

**A new fab vendor** — one file in `fabrication/drivers/`, exporting
`{ id, label, site, currency, capabilities, quote(spec, {fetchJson, apiKey}), cartUrl(spec) }`, plus a
line in `drivers/index.js`. Two rules: report `mode: 'estimate' | 'live'` on every quote, and never
add an order/checkout/payment call — `check-hardware.mjs` fails the build on either.

**A new export artifact** — add it to `FAB_ARTIFACTS` and give it a block in `buildFabPackage`,
wrapped in `attempt()` so a board that defeats one converter still exports everything else. Verified
converter names (they are NOT what you would guess): `convertSoupToGerberCommands` +
`stringifyGerberCommandLayers`, `stringifyExcellonDrill(convertSoupToExcellonDrillCommands({...}))`,
`convertBomRowsToCsv(await convertCircuitJsonToBomRows({circuitJson}))`,
`convertCircuitJsonToPickAndPlaceCsv(cj)`, `convertCircuitJsonToPcbSvg(cj)`.

**Do not add `circuit-json-to-gltf`.** It pulls `@resvg/resvg-js`, a native `.node` addon Rollup
cannot bundle, and it breaks the whole preview build. The 3D view already works — that is
`@tscircuit/3d-viewer` inside RunFrame, a different library. Likewise `circuit-json-to-spice` pulls a
`spicets` build that fails to import and takes `@tscircuit/eval` down with it.

**A new Leviathan tool** — add it to `useCircuitCapabilities.js` and wire it to the mutator the
button already uses (`runDesign`, `addPart`, `sb.write`, `post(...)`). Never open a second
persistence path; an AI edit must be indistinguishable from a hand edit.

## Verify

```sh
pnpm check:hardware   # static: reach guarantee, pure/CLI tier split, no purchase path
pnpm smoke:hardware   # live API: free tier, quotas, fork, snapshots, fab quoting
```

Browser-verified end to end on 2026-08-03: create → design → live render → thumbnail + stats
writeback → fork → fab package (9 gerber layers + drill + BOM + PnP + SVGs, 45KB) → quotes from both
vendors.


---
Source: shared/services/knowledge/build-knowledge/hardware-pipeline.md
Canonical: https://docs.leumas.tech/p/how-to/hardware-pipeline
