A visual pipeline builder. Drag typed nodes onto a canvas, wire the ports together, and an LLM workflow takes shape — the way you'd patch signal through a studio rack, which is where the name comes from.
The interesting problem isn't the drag-and-drop. A real tool like this has dozens of node types, and if every one is a hand-written component it collapses under its own weight. So the core of Patchbay is the layer underneath: a way to describe a node as data and have it become a working node on its own.
React + ReactFlow + Zustand on the front end, Python + FastAPI on the back.
Two processes, two terminals.
cd backend
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn main:app --reload # http://localhost:8000cd frontend
npm install
npm start # http://localhost:3000 (or next free port)The frontend talks to http://localhost:8000 by default; set REACT_APP_API_BASE
to point it elsewhere.
Every node type is one declarative object:
{
type: 'filter',
title: 'Filter',
icon: Icons.filter,
accent: '#fbbf24',
fields: [
{ key: 'condition', label: 'Condition', type: 'select', options: [...] },
{ key: 'value', label: 'Value', type: 'text' },
],
targets: [{ id: 'input' }],
sources: [{ id: 'pass', label: 'pass' }, { id: 'fail', label: 'fail' }],
}Three pieces make that work:
nodes/BaseNode.js— the card shell. Header, icon, accent, body, and the placement of every connection handle. Nodes never position a handle themselves; they declare what they connect and the shell lays them out.nodes/fields.js— turns a field descriptor into a control (text, number, select, checkbox, auto-growing textarea). Add a control type once, every node can use it.nodes/createNode.js— the factory. Takes a description, returns a React component wired to the store.
nodes/registry.js holds every definition and is the single source of truth for
three consumers: the ReactFlow node-types map, the toolbar, and the styling.
Adding a node type is one object — around eight lines — and it appears in all
three at once.
The nine node types included (input, LLM, output, text, filter, math, API request, delay, note) exist to prove the range: one input / two outputs, two inputs / one output, a node with three fields, and a node with no ports at all — a zero-connection node is just another configuration, not a special case.
One design point worth calling out: targets and sources can be functions of
the node's current values, not just static arrays. That's how the text node
grows a port per template variable without the abstraction knowing anything about
templates.
Two behaviours. It auto-resizes — width from the longest line, height from the content, and it shrinks back when you delete. And when you write a variable in double braces, a matching input port appears on the node, labelled with the name.
Variables are validated as real JavaScript identifiers: {{ 9bad }} (leading
digit), {{ my-var }} (hyphen) and {{ class }} (reserved word) are all
correctly ignored, and the same variable twice yields one port, not two.
A node editor where the only way to undo a drag is to wipe the board is not a
node editor. Every node carries a remove control in its header that appears on
hover, and removing a node also removes any connection attached to it, so the
graph is never left holding an edge pointing at something that is gone.
Connections are removed by double-clicking them; Backspace and Delete work
on a selection.
POST /pipelines/parse takes the pipeline as JSON and returns
{num_nodes, num_edges, is_dag}.
is_dag uses Kahn's algorithm: repeatedly remove nodes with no remaining
incoming edges; anything left over is part of a cycle. It's iterative (a deep
pipeline can't exhaust the recursion limit) and survives the messy input a
browser actually sends — empty graphs, self-loops, duplicate edges between the
same pair, edges referencing nodes that no longer exist, and disconnected
subgraphs.
The frontend shows the result as a dismissible panel rather than a browser alert, stating the outcome in words first — "Pipeline looks valid" or "This pipeline has a cycle" — with the raw counts underneath.
Dark-first, because it's a canvas tool: on a dark field the node accents and the
edges become the brightest things on screen. Everything visual is a design token
in index.css, so recolouring the whole app is one file. No UI library — no
Tailwind, no component kit; the entire surface is one hand-written CSS file.
Node icons are inline SVG on a single 24-unit grid with a consistent stroke,
inheriting currentColor so each takes its node's accent automatically.
React · ReactFlow · Zustand · Python · FastAPI · one hand-built design
system, no UI kit.
MIT — see LICENSE.