FlowDrop Docs

Architecture overview

How FlowDrop's modules, components, stores, and services fit together.

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.

┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐                                         
           Browser                                                    
│                           │                                         
   ┌─────────────────────┐                                            
│  │      FlowDrop       │  │                                         
   ├─────────────────────┤             ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│  │User Interface       │  │                   Your Backend          
   │ │                   │             │                             │
│  │ ├─╼ Navbar          │  │             ┌────────────────────────┐  
   │ ├─╼ Canvas          │             │  │   Python/PHP/NodeJS    │ │
│  │ ╰─╼ Config Panel    │  │             │       Framework        │  
   │                     │             │  ├────────────────────────┤ │
│  ├─────────────────────┤  │             │Storage and Business    │  
   │Browser Storage      │      REST   │  │Logic                   │ │
│  │ │                   │  │───API───▶   │ │                      │  
   │ ├─╼ Workflow        │             │  │ ├─╼ Nodes              │ │
│  │ ├─╼ History         │  │             │ ├─╼ Workflows          │  
   │ ╰─╼ Settings        │             │  │ ├─╼ Access             │ │
│  │                     │  │             │ ├─╼ Persistant Storage │  
   ├─────────────────────┤             │  │ ╰─╼ Execution          │ │
│  │Services             │  │             │                        │  
   │ │                   │             │  │                        │ │
│  │ ├─╼ API Client      │  │             └────────────────────────┘  
   │ ├─╼ Drafts          │             └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
│  │ ╰─╼ Toasts          │  │                                         
   │                     │                                            
│  └─────────────────────┘  │                                         
 ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ 

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:

┌───────────────────────┐                         
│      User action      │                         
│     (drag node, edit  │                         
│  config, draw edge)   │                         
└───────────────────────┘                         


┌───────────────────────┐                         
│Component event handler│                         
└───────────────────────┘                         



┌───────────────────────┐                         
│ workflowStore update  │                         
│   (state mutation)    │                         
└───────────────────────┘                         

            │       ┌────────────────────────────┐
            │       │        historyStore        │
            ├──────▶│      records snapshot      │
            │       │         (for undo)         │
            │       └────────────────────────────┘
            │       ┌────────────────────────────┐
            │       │          isDirty           │
            ├──────▶│      flag set to true      │
            │       │                            │
            │       └────────────────────────────┘
            │       ┌────────────────────────────┐
            │       │                            │
            ├──────▶│       UI re-renders        │
            │       │                            │
            │       └────────────────────────────┘
            │       ┌────────────────────────────┐
            │       │                            │
            │       │ onWorkflowChange(workflow, │
            │       │        changeType)         │
            │       │                            │
            ├──────▶│                            │
            │       │ your callback — analytics, │
            │       │      validation, etc.      │
            │       │                            │
            │       └────────────────────────────┘
            │       ┌────────────────────────────┐
            │       │  onDirtyStateChange(true)  │
            │       │                            │
            └──────▶│your callback — update save │
                    │        button, etc.        │
                    │                            │
                    └────────────────────────────┘

When the user saves:

                ┌─────────────────────┐                             
                │  User clicks Save   │                             
                └─────────────────────┘                             


             ┌───────────────────────────┐                          
             │  onBeforeSave(workflow)   │                          
             └───────────────────────────┘                          


             ┌──False──────┴──────────────┐                         
             │                            │                         
             ▼                            ▼                         
  ┌─────────────────────┐      ┌─────────────────────┐              
  │                     │      │     API client:     │              
  │       Cancel        │      │                     │              
  │                     │      │ PUT /workflows/{id} │              
  └─────────────────────┘      └─────────────────────┘              



                ┌─────Success─────────────┴───Error─┐               
                │                                   │               
                │                                   │               
                ▼                                   ▼               
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓    ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃isDirty = false, draft cleared┃    ┃      Toast notification      ┃
┃                              ┃    ┃                              ┃
┣──────────────────────────────┫    ┣──────────────────────────────┫
│    onAfterSave(workflow)     │    │ onSaveError(error, workflow) │
└──────────────────────────────┘    ├──────────────────────────────┤
                                    │  onApiError(error, 'save')   │
                                    └──────────────────────────────┘

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.

Next steps

On this page