Fusible
One project for your circuit boards, your firmware, and the case around them. Fusible checks the whole design against rules. When a rule fails, the fuse blows: the command exits with an error, and nothing gets exported or generated.
Fusible reads the Circuit JSON that tscircuit produces,
checks the electrical and physical rules of every board, and only then writes
fabrication files, firmware pin bindings, and enclosures. It ships as a command
line tool, a browser studio (fusible studio), and a desktop app.

What you get
| Piece | What it does |
|---|---|
| Linter | Checks nets, shorts, clearance, decoupling, pin muxing, and cabled connectors. |
| Studio | Live PCB, schematic, 3D, parts, firmware pins, bodies, and checks in the browser. |
| Fabrication | Gerbers, drill files, KiCad projects, STEP, GLB, SVG, PDF. All behind the fuse. |
| Firmware | Typed Rust pin bindings (pins.rs) for STM32, RP2040, and ESP32-S3. |
| Bodies | Authored CAD files or a generated two-part enclosure, plus a kit to model your own. |
| Parts | A bill of materials with prices, picks, and print costs. |
| Team | A SYNC panel that shares the project through git, with no terminal. |
| KiCad | The studio watches your .kicad_pcb and writes moved parts back into your TSX. |
Install
Linting Circuit JSON, Gerbers, and SVG need nothing else. To compile .tsx
boards or export STEP, install tscircuit and check the toolchain:
The desktop app (Windows, macOS, Linux) is attached to each GitHub release. It
bundles bun, lists your projects, and runs fusible studio --desktop for each.
The installers are not code-signed yet.
Quick start
init writes a fusible.yml:
name: sensor-node
boards:
main:
entry: hardware/index.tsx
mcu: stm32g431
firmware:
app:
board: main
path: firmware
framework: embassy
pins_rs: firmware/src/pins.rs
build_cmd: cargo build
fusible init --currency USD also writes a bom.yml.
The project file
A project has three kinds of named things. Names are unique across all three, so any command takes one as its target.
name: rover
boards:
main:
sensor:
firmware:
main-fw:
sensor-fw:
bodies:
case:
knob:
lid:
links:
cable:
from: main.J2
to: sensor.J1
wires:
-
-
fusible checkchecks every board and reports all violations together.fusible check sensorchecks one.fusible build sensor-fwchecks thesensorboard, writes its pin bindings, then builds that firmware.fusible export main --preset fabwrites intodist/main/fab/. With several boards,exportandstudioneed a name.- A firmware or generated body names its
board:when the project has several. An authored body may name one too: it is then modeled in that board's coordinate frame.around:still works as an alias. - The old single-board form (
hardware:,mcu: { chip }, onefirmware:,mechanical:) still loads.
Links between boards
A link says that a connector of one board is cabled to a connector of another.
Each wire joins a pin a of from to a pin b of to. A pin is a number
(1) or a name (TX).
- No
net: both ends must carry the same net name. net: GND: both ends must carryGND.nets: [TX, RX]: the first end must carryTX, the secondRX. Use it for crossed wires.
The harness_mismatch rule blows the fuse when a connector or pin does not
exist, a pin is wired to nothing, or the nets do not match. It runs after the
boards pass, in fusible check and fusible build, when every board is checked.
Commands
check: the electrical linter
Compiling TSX stops after 120 seconds. Set FUSIBLE_BUILD_TIMEOUT_SECS to change that.
the fuse blew: 1 electrical violation(s)
[unconnected_net] net "I2C_SDA" has only 1 connection(s) (minimum 2): it is floating
help: every net must connect at least two pads/ports: complete the routing in tscircuit
The rules: unconnected_net, ghost_net, shorted_nets, malformed_element,
dangling_reference, trace_clearance, decoupling_capacitor, pin_mux,
E_SCHEMATIC_SCALE_EXCESSIVE, harness_mismatch, and the three assembly rules
(body_interference, hole_misaligned, connector_blocked). A rule that cannot prove a
net is connected reports it. Silence is never the default.
studio: the studio
dev, view, and webview still work as aliases.
- Project tree. Lists boards, firmware, and bodies, each with its fuse state. The URL keeps the selection (
?item=board:sensor). - Views. PCB layout, schematic, 3D board, parts, nets, and checks follow the selected board. Layers toggle on and off.
RUNS. Startscheck,build, andbodyfrom the browser with a live log. It runs the samefusiblebinary, so the result matches the terminal. One run at a time.PINS. Select a firmware to see its pin map: each GPIO by peripheral, signal, connected part, and Rust name. Pins with apin_muxconflict show the reason.BUILDruns the firmware build.BODIES. Shows the generated base and lid with lid and explode toggles, or the selected authored body with the outline of itsboard:drawn beside it. GLB loads as is. STEP and STL convert withocct-wasm, cached by path, time, and size (64 MiB limit). Authored GLB must be in millimetres, Z-up.PARTS & COSTS. Edits the samebom.ymlasfusible bom.SYNC. Appears when the project is inside a git work tree.SAVEstores your changes with a message, gets the others' work, puts yours on top, and sends it.GET UPDATESonly gets theirs. When two people changed the same file, a card names the file and the author, andKEEP MINEorKEEP THEIRSdecides.node_modules,dist,.fusible, and*.lckare never saved. Studio runs the realgitwith no shell and no prompt, so a missing sign-in fails with a message instead of hanging.- Narrow windows. The tree collapses with
[and becomes a drawer below 900px.
Studio serves only local requests. Run requests accept names from fusible.yml, a JSON body, and a local Origin.
route: the autorouter
A multi-layer A* router on R-trees. Each routed trace becomes an obstacle for the next, so routed nets cannot short. If a net cannot route, the fuse blows and the command lists the unrouted nets.
KiCad: diff, sync, and the studio watch
While the studio runs, it watches the board's KiCad file. KiCad owns placement. The TSX owns parts and nets.
- Name the file per board with
kicad:infusible.yml. Without it, the studio uses the single.kicad_pcbbeside the entry file or in akicad/folder. - On save in KiCad, the studio rewrites plain-number
pcbX,pcbY, andpcbRotationprops in your TSX, then rebuilds. An expression such aspcbX={base + 2}, a part without those props, or a flip to the other side is left alone and listed as a difference. - Before it writes a TSX file, the studio copies it to
.fusible/backup/and keeps the last 10 per file. - Added, removed, or renamed parts and changed nets are never written. A
KICAD: n DIFFERENCESchip lists them.KICAD: ERRORmeans a file could not be read.
build
Checks the boards and links, writes pins.rs, and builds the firmware only if
the fuse is intact.
export: fuse-gated deliverables
The rules run first. If one fails, no file is written. The PDF needs a local Chromium; without one, Fusible warns and writes the SVG only. Gerber, drill, and SVG output are pure Rust.
body: physical bodies
A body is authored (source:, a file from any CAD tool, with an optional
build: command for code-CAD) or generated (generate: enclosure, a base
and lid around the board outline). Fusible never overwrites an authored file. A
generated body is fuse-gated and regenerated on every run.
A board without a body gets a default enclosure. mechanical, case, and
housing are aliases of body. The enclosure comes from pcb_board: each
pcb_hole gets a screw boss, and components that reach the board edge get a
wall notch. Flags override fusible.yml, which overrides defaults. It needs
bun, and the first run installs occt-wasm into ~/.cache/fusible/.
Start from the generated body, or design your own:
ejectis fuse-gated. It writes the model to a staging file, moves it into place only on success, never replaces an existing file, and editsfusible.ymlin one step, keeping your comments. If the entry has a shape it cannot edit safely, it leaves the manifest alone and exits 2 with the lines to change by hand.kitwriteskit.jsonfor people who model from scratch: the boardoutlineandthickness, everyhole, every component footprint (withzandheightwhen the part has a 3D model), akeep_outbox per modeled part (z0toz1), and the wallopeningswhere a component reaches an edge. Thenotchblock holds the generator's constants. Units are mm in the PCB frame.board.stepis added for TSX entries when a runner is installed.ejectandkitare reserved. A body with either name is rejected.
assembly: boards and bodies in one model
assembly:
out_dir: dist # default: dist
format: glb # glb, step, or all; default: glb
parts:
main:
sensor:
enclosure: # a body follows the board it is modeled on
Units are mm. rotate is in degrees around the fixed X, then Y, then Z axes,
and applies before at. A body with a board (its board:, or the only board
for a generated body) sits on that board, and its own at and rotate are
added on top. That board must be a part too. A body without a board needs an
at. A board left out of parts is not drawn.
The command is fuse-gated: every board it draws, and every board a generated
body is fitted to, passes check first (--skip-lint skips it). A board needs
its .tsx entry and a tscircuit runner, which exports its STEP model. A
generated body is rebuilt for the assembly; an authored body is read from its
source: (STEP or STL) after its build: runs. The models of each part land in
dist/assembly/. Like body, it needs bun and occt-wasm.
Three rules compare the boards with the bodies and blow the fuse (exit 1, no
assembly.* written):
body_interference: a body overlaps the board slab or a component's volume, or two bodies overlap.hole_misaligned: a body has material near a board hole but no boss around it (fewer than 2 of 4 ring quadrants), or has any material inside the hole's bore (a pilot hole up to 0.5 mm narrower in radius is fine). A body fitted to a board (generated, or authored withboard:) must also have a boss for every non-plated hole of that board: a hole with no body material near it blows the fuse. Plated holes (vias, pads) need no boss.connector_blocked: a body stands in front of the opening a connector at the board edge needs.
Parts that only touch do not fire. Two bodies that overlap by more than 0.01 mm³ blow the fuse, so give a lip a clearance. Each run writes dist/assembly/check.json
(fuse, and per violation the rule, parts, overlap volume in mm³, and position)
and dist/assembly/preview.glb, which holds the bodies, the board slabs, and one
collision:<n> node per overlap, even when the fuse blows. fusible check runs
the rules too, when it checks every board. Without assembly: they are skipped.
A declared assembly: without bun, or a body that is not a closed solid, is a
tool error (exit 2), never a pass.
bom: parts and costs
Lists the placed components grouped by kind, value, and part number, and adds
each body as a 3D print. bom.yml holds prices, picks, and manual parts:
currency: EUR
print:
price_per_kg: 25
parts:
resistor-10kohm:
m3-screw:
prints:
case:
A key that matches a detected part overrides it. Any other key is a manual
part. Print cost is grams / 1000 * price_per_kg. A part without a price gets
an estimate, marked ~. bom never blows the fuse.
Packages and toolchain
FUSIBLE_BUN names the bun binary for every command. Without a tsci, Fusible
runs bun x --bun tsci.
Exit codes
| Code | Meaning |
|---|---|
0 |
Fuse intact. The command succeeded. |
1 |
Fuse blown. A rule failed and generation is blocked. |
2 |
Tool error. Unreadable input, missing runner, or bad arguments. |
CI can tell a bad design (1) from a broken setup (2).
CI
The repository is also a GitHub Action. It installs a release binary, sets up
bun, runs fusible check, and fails the job when the fuse blows:
- uses: actions/checkout@v7
- id: fuse
uses: KonoLabs/Fusible@v0.19.0
with:
target: "" # optional board name or file; empty checks every board
version: latest # or a version such as 0.19.0
Other inputs: working-directory (default .) and install-deps (default
true, runs bun install where a package.json sits). The fuse output is
intact, blown, or error, from exit codes 0, 1, and 2.
Layout of the source
src/
├── cli.rs, dispatch.rs, main.rs # commands and exit codes
├── commands/ # check, build, export, body, bom, links, init, doctor
├── config/ # fusible.yml: boards, firmware, bodies, links
├── parser/, netlist/, linter/ # pure Rust: Circuit JSON, net graph, rules
├── silicon/, codegen/ # pin muxing databases and pins.rs
├── gerber/, svg/, router/, kicad/ # fabrication, rendering, routing, KiCad sync
├── dev/ # studio server (tiny_http, SSE, git, KiCad watch)
├── tscircuit/ # runner discovery and process control
└── assets/ # embedded studio
studio/ # studio sources, built into src/assets/studio.bundle.js
desktop/ # Tauri shell
The parser, netlist, and rules have no C dependencies. tscircuit, bun, and
occt-wasm are used only for TSX, STEP, and bodies.
Development
Rebuild the studio bundle with cd studio && bun run build. See
ROADMAP.md for what is next and CHANGELOG.md for
what changed.