This page explains how FlowDrop is structured internally, so you can make
informed decisions about what to import, how to integrate, and where to extend.
High-level architecture
FlowDrop is a frontend library that communicates with your backend via REST.
Module structure
FlowDrop is tree-shakable. Each sub-module has different dependencies and bundle cost:
Module What it provides Heavy deps @flowdrop/flowdrop/coreTypes, utilities, auth providers, config helpers None @flowdrop/flowdrop/editorWorkflowEditor, mount functions, node components @xyflow/svelte @flowdrop/flowdrop/formSchemaForm, field components None @flowdrop/flowdrop/form/codeCode & template editors CodeMirror (~300KB) @flowdrop/flowdrop/form/markdownMarkdown editor CodeMirror @flowdrop/flowdrop/displayMarkdownDisplay marked @flowdrop/flowdrop/playgroundPlayground, chat, interrupts Editor + Form @flowdrop/flowdrop/settingsSettings panel, theme toggle Form @flowdrop/flowdrop/stylesCSS design tokens None @flowdrop/flowdropBootstrap front door (App, mount, instances) Bootstrap surface
Component hierarchy
When you mount mountFlowDropApp(), this is the component tree:
App
├── Navbar
│ ├── Logo
│ ├── WorkflowName (editable)
│ ├── Save / Export buttons
│ ├── Custom NavbarActions
│ └── ThemeToggle / Settings
├── NodeSidebar
│ ├── Search
│ └── CategoryGroups
│ └── NodeCards (draggable)
├── WorkflowEditor (@xyflow/svelte canvas)
│ ├── Nodes (WorkflowNode, SimpleNode, GatewayNode, etc.)
│ │ └── Ports (input/output handles)
│ ├── Edges (styled by category)
│ └── ConnectionLine
├── ConfigPanel (right side, on node click)
│ ├── NodeHeader (name, type, icon)
│ └── SchemaForm (generated from configSchema)
│ └── FormFields (text, select, code, template, etc.)
└── ToastContainer
mountWorkflowEditor() mounts just the canvas — no navbar, no sidebar.
Each mount produces one such tree backed by its own instance; node/field
registries and settings are shared across all trees on the page.
Stores
FlowDrop uses Svelte 5 runes for state management. Each mount creates a
per-instance FlowDropInstance container that holds these stores:
Store Purpose Key state workflowStore Central workflow state nodes, edges, metadata, isDirty historyStore Undo/redo past states, future states, canUndo/canRedo settingsStore User preferences theme, editor behavior, UI config playgroundStore Playground sessions sessions, messages, isExecuting interruptStore Human-in-the-loop pending/resolved interrupts categoriesStore Node categories category definitions, colors portCoordinateStore Handle positions port coordinates for edge rendering
Instance model
Every mount creates an isolated FlowDropInstance container holding the stores
above (workflow, history, playground, interrupts, categories, port coordinates,
and pipeline-panel state), resolved through Svelte context. Multiple editors can
therefore coexist on one page without sharing state.
See the multiple instances guide for details.
Services
Services handle communication and side effects:
Service Purpose API client HTTP requests to your backend (nodes, workflows, execution) Draft storage Auto-save to localStorage Toast service Success/error/loading notifications Dynamic schema Fetch config schemas from API at runtime Playground service Manage sessions, poll for messages Interrupt service Submit interrupt resolutions History service Track and replay state changes Settings service Load/save preferences (localStorage + API)
Data flow
Here’s what happens when a user makes a change:
When the user saves:
Registry system
FlowDrop has two registries for extending the editor:
Node component registry
Register custom Svelte components for new node types against the instance’s
fd.nodes registry:
import { getInstance } from '@flowdrop/flowdrop/editor' ;
const fd = getInstance ();
fd . nodes . registerCustom ( 'my-custom-node' , 'My Custom Node' , MyNodeComponent );
Field component registry
Register custom form fields for config schemas against fd.fields:
import { getInstance } from '@flowdrop/flowdrop/editor' ;
const fd = getInstance ();
fd . fields . register ( 'my-field' , {
component: MyFieldComponent ,
matcher : ( schema ) => schema . format === 'my-field' ,
priority: 10
});
Both registries are instance-scoped — seeded with builtins in the instance
constructor and resolved via getInstance(). You can register after mounting.
Why registration works after mount
BaseRegistry tracks a version counter that invalidates dependent $derived
reads, so registrations made after mount still take effect.
Next steps