This document provides an overview of the Kindling codebase for contributors. It explains how the pieces fit together and where to find things.
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 (Project, Scene, ReferenceItem, etc.)
│ │ ├── db/ # SQLite schema and queries
│ │ └── parsers/ # Import format parsers (Plottr, Markdown, yWriter, Longform)
│ └── tauri.conf.json # Tauri configuration
│
├── e2e/ # End-to-end tests (WebdriverIO)
├── docs/ # Documentation
└── test-data/ # Test fixtures
src/lib/components/)| Component | Purpose |
|---|---|
App.svelte |
Root component, routes between StartScreen and Editor |
StartScreen.svelte |
Project selection and import UI |
Sidebar.svelte |
Chapter/scene tree navigation |
ScenePanel.svelte |
Main editing area with beats |
ReferencesPanel.svelte |
Reference cards across types (characters, locations, items, objectives, organizations) |
Onboarding.svelte |
First-launch tutorial flow |
ContextMenu.svelte |
Right-click context menus |
ConfirmDialog.svelte |
Confirmation modals |
RenameDialog.svelte |
Rename modals |
ArchivePanel.svelte |
View and restore archived items |
src/lib/stores/)Kindling uses Svelte 5's runes-based reactivity. State is managed through class-based stores:
project.svelte.ts - Project data state:
// Access via: currentProject
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:
// Access via: ui
ui.currentView; // 'start' | 'editor'
ui.sidebarCollapsed; // boolean
ui.referencesPanelCollapsed; // boolean
ui.focusMode; // boolean
ui.showOnboarding; // boolean
ui.isImporting; // boolean
invoke() to send command to RustExample:
// In a component
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);
}
src-tauri/src/commands/)All frontend-backend communication goes through Tauri commands. Commands are async functions decorated with #[tauri::command]:
#[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())
}
Commands are registered in lib.rs:
.invoke_handler(tauri::generate_handler![
commands::get_chapters,
commands::create_chapter,
// ...
])
Command categories:
import_plottr, import_markdown, import_ywriter, import_longformget_*, create_*, delete_*, rename_*reorder_chapters, reorder_scenes, move_scene_to_chapterget_sync_preview, apply_sync, reimport_projectarchive_*, restore_*, get_archived_itemslock_*, unlock_*export_to_docx, export_to_markdown, export_to_longform, export_to_epubcreate_snapshot, list_snapshots, preview_snapshot, restore_snapshot, delete_snapshotget_app_settings, update_app_settings, update_project_settingssrc-tauri/src/models/)Each model maps to a database table:
| Model | Description |
|---|---|
Project |
Top-level container, tracks source file |
Chapter |
Groups scenes, has position for ordering |
Scene |
Writing unit, contains synopsis and prose |
Beat |
Story beat within a scene |
Character |
Character reference with attributes |
Location |
Location reference with attributes |
ReferenceItem |
Typed reference (characters, locations, items, objectives, organizations) |
src-tauri/src/db/)schema.rs: Table definitions and migrationsqueries.rs: CRUD operationsmod.rs: Module exportsThe database is SQLite, stored in the app's data directory (kindling.db).
Key tables:
projects → chapters → scenes → beats
↓
characters, locations, reference_items (with scene reference links)
src-tauri/src/parsers/)Native Rust parsers for importing outlines:
| Parser | File Type | Notes |
|---|---|---|
plottr.rs |
.pltr |
JSON-based, extracts timeline/beats |
markdown.rs |
.md |
Heading-based outline format |
ywriter.rs |
.yw7 |
yWriter project import |
longform.rs |
.md |
Longform/Obsidian index or vault import |
Each parser returns a ParsedProject struct that gets inserted into the database.
Project
├── Chapters (ordered by position)
│ └── Scenes (ordered by position)
│ └── Beats (ordered by position)
├── Characters
│ └── Attributes (key-value pairs)
└── Locations
└── Attributes (key-value pairs)
Key concepts:
source_id: Links imported items back to their source file IDs (for re-import sync)position: Integer for ordering within parentarchived: Soft-delete flaglocked: Prevents editingimport_* commandsource_id mappingsget_sync_preview parses source file and compares to DBSyncPreview with additions and changesapply_sync updates only selected itemssrc/**/*.test.tsnpm testsrc-tauri/src/**/*.rs (inline #[cfg(test)] modules)cd src-tauri && cargo teste2e/specs/e2e/README.mdResult<T, String>invoke() in try/catch$effect() for reactive data loadinglocalStoragesrc-tauri/src/commands/ modulelib.rs invoke_handlertypes.tsinvoke() from frontend.svelte file in src/lib/components/data-testid attributes for E2E testsschema.rs