Skip to main content
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:
ModuleWhat it providesHeavy deps
@flowdrop/flowdrop/coreTypes, utilities, auth providers, config helpersNone
@flowdrop/flowdrop/editorWorkflowEditor, mount functions, node components@xyflow/svelte
@flowdrop/flowdrop/formSchemaForm, field componentsNone
@flowdrop/flowdrop/form/codeCode & template editorsCodeMirror (~300KB)
@flowdrop/flowdrop/form/markdownMarkdown editorCodeMirror
@flowdrop/flowdrop/displayMarkdownDisplaymarked
@flowdrop/flowdrop/playgroundPlayground, chat, interruptsEditor + Form
@flowdrop/flowdrop/settingsSettings panel, theme toggleForm
@flowdrop/flowdrop/stylesCSS design tokensNone
@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:
StorePurposeKey state
workflowStoreCentral workflow statenodes, edges, metadata, isDirty
historyStoreUndo/redopast states, future states, canUndo/canRedo
settingsStoreUser preferencestheme, editor behavior, UI config
playgroundStorePlayground sessionssessions, messages, isExecuting
interruptStoreHuman-in-the-looppending/resolved interrupts
categoriesStoreNode categoriescategory definitions, colors
portCoordinateStoreHandle positionsport 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:
ServicePurpose
API clientHTTP requests to your backend (nodes, workflows, execution)
Draft storageAuto-save to localStorage
Toast serviceSuccess/error/loading notifications
Dynamic schemaFetch config schemas from API at runtime
Playground serviceManage sessions, poll for messages
Interrupt serviceSubmit interrupt resolutions
History serviceTrack and replay state changes
Settings serviceLoad/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.
BaseRegistry tracks a version counter that invalidates dependent $derived reads, so registrations made after mount still take effect.

Next steps