Hardware
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...
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 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 [email protected], 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
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.