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:
| Module | What it provides | Heavy deps |
|---|---|---|
@flowdrop/flowdrop/core | Types, utilities, auth providers, config helpers | None |
@flowdrop/flowdrop/editor | WorkflowEditor, mount functions, node components | @xyflow/svelte |
@flowdrop/flowdrop/form | SchemaForm, field components | None |
@flowdrop/flowdrop/form/code | Code & template editors | CodeMirror (~300KB) |
@flowdrop/flowdrop/form/markdown | Markdown editor | CodeMirror |
@flowdrop/flowdrop/display | MarkdownDisplay | marked |
@flowdrop/flowdrop/playground | Playground, chat, interrupts | Editor + Form |
@flowdrop/flowdrop/settings | Settings panel, theme toggle | Form |
@flowdrop/flowdrop/styles | CSS design tokens | None |
@flowdrop/flowdrop | Bootstrap 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.)
└── ToastContainermountWorkflowEditor() 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:
┌───────────────────────┐
│ 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
- What is a Workflow? — the mental model
- Quick Start — mount FlowDrop in your app
- Backend Implementation — build the API FlowDrop expects
- Event System — hook into every lifecycle event