Architecture Overview
Sigilweaver is a desktop application with a clear separation between frontend (UI) and backend (data processing). Understanding this split is key to navigating the codebase.
The Big Picture
┌─────────────────────────────────────────────────────────────────┐
│ Electron Shell │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ React Frontend │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │
│ │ │ Canvas │ │ State │ │ Config Panels │ │ │
│ │ │ (Xyflow) │ │ (Zustand) │ │ (React) │ │ │
│ │ └─────────────┘ └──────────────┘ └─────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ HTTP │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ Python Backend │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │ │
│ │ │ FastAPI │ │ Tool │ │ Polars │ │ │
│ │ │ Routes │ │ Registry │ │ Execution │ │ │
│ │ └─────────────┘ └──────────────┘ └─────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
Monorepo Structure
Everything lives in one repository. This isn't arbitrary—it solves real problems.
sigilweaver-app/
├── backend/ # Python: FastAPI + Polars
│ ├── app/
│ │ ├── api/ # REST endpoints
│ │ │ ├── routes/ # Route handlers
│ │ │ └── models/ # Pydantic request/response schemas
│ │ ├── domain/ # Business logic
│ │ │ ├── tools/ # Tool implementations (Select, Filter, etc.)
│ │ │ ├── workflow/ # Execution engine
│ │ │ └── datasources/ # CSV, Parquet handlers
│ │ └── utils/ # Shared utilities
│ └── tests/ # pytest suite
│
├── frontend/ # TypeScript: React + Electron
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── state/ # Zustand stores
│ │ ├── domain/ # Pure TS business logic
│ │ ├── api/ # Backend client
│ │ ├── data/ # Tool definitions
│ │ └── types/ # TypeScript interfaces
│ ├── electron/ # Main process
│ └── tests/ # Vitest suite
│
├── scripts/ # Build and dev utilities
└── docs/ # Project documentation
Why This Architecture?
Separation of Concerns
The frontend handles:
- Visual workflow representation (canvas, tools, wires)
- User interaction (drag-drop, selection, configuration)
- State management (undo/redo, tabs, settings)
- Workflow serialization (save/load
.swwffiles)
The backend handles:
- Data loading and type inference
- Tool execution (actual transformations)
- Schema propagation (what columns exist after each step)
- Preview data generation
This split means the frontend never touches actual data. It works with schemas and metadata, keeping the UI responsive regardless of dataset size.
Communication Pattern
The frontend and backend talk over HTTP:
- User adds a data source → Frontend sends path to backend → Backend returns schema (column names, types)
- User configures a tool → Frontend sends workflow JSON to backend → Backend returns output schema
- User requests preview → Frontend sends workflow + tool ID → Backend executes upstream tools, returns sample rows
- User runs workflow → Frontend sends complete workflow → Backend executes everything, writes output
All requests are synchronous from the user's perspective—they click, something happens. Under the hood, Polars' lazy evaluation means we only compute what's actually needed.
Frontend Architecture
See Frontend Architecture for details. Key points:
- React for UI components
- Zustand for state management (workflow, history, UI, tabs)
- Xyflow for the visual canvas (nodes and edges)
- Vite for development and bundling
- Electron for desktop packaging
State is split across multiple Zustand stores:
workflowStore: Tools, wires, selectionhistoryStore: Undo/redo stacktabStore: Multi-tab workflowsuiStore: UI preferences (minimap, snap-to-grid)settingsStore: Persisted settings
Backend Architecture
See Backend Architecture for details. Key points:
- FastAPI for REST API
- Polars for dataframe operations (lazy evaluation)
- Pydantic for request/response validation
- Tool Registry pattern for extensibility
The tool system uses a decorator-based registry:
@register_tool("Filter")
class FilterTool(BaseTool):
async def execute(self, config, inputs):
# Transformation logic
This makes adding new tools straightforward—implement the interface, register it, done.
Data Flow Example
Here's what happens when a user connects an Input tool to a Filter tool:
- Input tool configured with
data.csv - Frontend calls
POST /api/workflow/tool/schemawith workflow JSON - Backend loads CSV, infers schema, returns
{columns: [{name: "age", type: "Int64"}, ...]} - Frontend stores schema, shows columns in Filter config dropdown
- User configures Filter:
pl.col('age') > 30 - Frontend calls
POST /api/workflow/tool/schemaagain with updated workflow - Backend computes Filter's output schema (same columns, different row count unknown until execution)
- User clicks Preview
- Frontend calls
POST /api/workflow/tool/executewith workflow + Filter tool ID - Backend executes Input → Filter lazily, collects 100 rows, returns preview data
- Frontend displays preview in data grid
All of this happens in milliseconds for reasonable-sized data because Polars is lazy—it only reads enough of the CSV to return the preview.
Key Design Decisions
For the rationale behind major architectural choices, see Architectural Decisions.
Next: Frontend Architecture or Backend Architecture for deeper dives.