Skip to content

Architecture

This document provides an overview of the Kindling codebase for contributors.

Kindling is a desktop application built with Tauri, which combines a Rust backend with a web-based frontend. The frontend uses Svelte 5 and communicates with the Rust backend via Tauri’s IPC (Inter-Process Communication) system.

┌─────────────────────────────────────────────────────────────────┐
│ Desktop Window │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ Svelte 5 Frontend │ │
│ │ ┌──────────┐ ┌──────────┐ ┌─────────────────────┐ │ │
│ │ │ Sidebar │ │ Scene │ │ References Panel │ │ │
│ │ │ │ │ Panel │ │ │ │ │
│ │ └──────────┘ └──────────┘ └─────────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────┴──────┐ │ │
│ │ │ Stores │ (Svelte 5 runes) │ │
│ │ └──────┬──────┘ │ │
│ └─────────────────────┼───────────────────────────────────┘ │
│ │ invoke() │
│ ┌─────────────────────┼───────────────────────────────────┐ │
│ │ Tauri IPC Bridge │ │
│ └─────────────────────┼───────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────┼───────────────────────────────────┐ │
│ │ Rust Backend │ │
│ │ ┌──────────┐ ┌────┴─────┐ ┌──────────────────────┐ │ │
│ │ │ Parsers │ │ Commands │ │ Database │ │ │
│ │ │ (import) │ │ (IPC) │ │ (SQLite) │ │ │
│ │ └──────────┘ └──────────┘ └──────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
kindling/
├── src/ # Svelte frontend
│ ├── lib/
│ │ ├── components/ # UI components
│ │ ├── stores/ # State management (Svelte 5 runes)
│ │ └── types.ts # TypeScript interfaces
│ ├── App.svelte # Root component
│ └── main.ts # Entry point
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── lib.rs # App initialization, command registration
│ │ ├── commands/ # Tauri IPC command handlers (by module)
│ │ ├── models/ # Data structures
│ │ ├── db/ # SQLite schema and queries
│ │ └── parsers/ # Import format parsers
│ └── tauri.conf.json # Tauri configuration
├── e2e/ # End-to-end tests (WebdriverIO)
└── test-data/ # Test fixtures
ComponentPurpose
App.svelteRoot component, routes between StartScreen and Editor
StartScreen.svelteProject selection and import UI
Sidebar.svelteChapter/scene tree navigation
ScenePanel.svelteMain editing area with beats
ReferencesPanel.svelteReference cards (characters, locations, items, objectives, organizations)
Onboarding.svelteFirst-launch tutorial flow
ContextMenu.svelteRight-click context menus

Kindling uses Svelte 5’s runes-based reactivity with class-based stores:

project.svelte.ts — Project data state:

currentProject.value; // Current Project or null
currentProject.chapters; // Chapter[]
currentProject.scenes; // Scene[] (for current chapter)
currentProject.beats; // Beat[] (for current scene)
currentProject.characters; // Character[]
currentProject.locations; // Location[]

ui.svelte.ts — UI state:

ui.currentView; // 'start' | 'editor'
ui.sidebarCollapsed; // boolean
ui.referencesPanelCollapsed; // boolean
ui.focusMode; // boolean
  1. User interacts with a component
  2. Component calls invoke() to send a command to Rust
  3. Rust processes the command and returns data
  4. Component updates the store
  5. Reactive UI updates automatically
import { invoke } from "@tauri-apps/api/core";
import { currentProject } from "./lib/stores/project.svelte";
async function loadChapters(projectId: string) {
const chapters = await invoke("get_chapters", { projectId });
currentProject.setChapters(chapters);
}

All frontend–backend communication goes through Tauri commands:

#[tauri::command]
pub async fn get_chapters(
project_id: String,
state: State<'_, AppState>
) -> Result<Vec<Chapter>, String> {
let conn = state.db.lock().map_err(|e| e.to_string())?;
db::get_chapters(&conn, &project_id).map_err(|e| e.to_string())
}

Command categories:

CategoryCommands
Importimport_plottr, import_markdown, import_ywriter, import_longform
CRUDget_*, create_*, delete_*, rename_*
Reorderreorder_chapters, reorder_scenes, move_scene_to_chapter
Syncget_sync_preview, apply_sync, reimport_project
Exportexport_to_docx, export_to_markdown, export_to_longform, export_to_epub
Settingsget_app_settings, update_app_settings, update_project_settings
ModelDescription
ProjectTop-level container, tracks source file
ChapterGroups scenes, has position for ordering
SceneWriting unit, contains synopsis and prose
BeatStory beat within a scene
CharacterCharacter reference with attributes
LocationLocation reference with attributes

SQLite, stored in the app’s data directory (kindling.db).

projects → chapters → scenes → beats
characters, locations, reference_items (with scene reference links)

Key concepts:

  • source_id — links imported items back to their source file IDs (for re-import sync)
  • position — integer for ordering within parent
  • archived — soft-delete flag
  • locked — prevents editing
ParserFile TypeNotes
plottr.rs.pltrJSON-based, extracts timeline/beats
markdown.rs.mdHeading-based outline format
ywriter.rs.yw7yWriter project import
longform.rs.mdLongform/Obsidian index or vault import

Each parser returns a ParsedProject struct that gets inserted into the database.

TypeLocationFrameworkCommand
Frontendsrc/**/*.test.tsVitest + Testing Librarynpm test
Backendsrc-tauri/src/**/*.rsRust built-incd src-tauri && cargo test
E2Ee2e/specs/WebdriverIO + Tauri driverSee e2e/README.md
  1. Add function in the appropriate src-tauri/src/commands/ module
  2. Register in lib.rs invoke_handler
  3. Add TypeScript types to types.ts
  4. Call via invoke() from the frontend
  1. Create .svelte file in src/lib/components/
  2. Add data-testid attributes for E2E tests
  3. Import and use in parent component
  1. Add migration in schema.rs
  2. Update model struct
  3. Update relevant queries
  4. Update TypeScript types