Development

Architecture

Copy Markdown

How Inkfinite's Rust document engine, TypeScript editor, desktop bridge, renderer, and CLI fit together.

Inkfinite has one canonical document model and a separate editor-facing projection. Rust owns the canonical document, transactions, Automerge state, native files, headless rendering, and desktop sessions. TypeScript owns interactive editor state, tools, browser input, and Canvas rendering.

The boundary between them is intentional: the editor can keep pointer movement and gesture previews local, while completed desktop edits still commit through the same Rust transaction engine used by the CLI.

Implemented foundation

The current system includes:

  • an Automerge-backed Rust document engine with validated atomic transactions, history, undo and redo, causal heads, sync, deterministic repair, and native file recovery
  • generated TypeScript contracts, semantic metadata, ordered layers, bindings, built-in shapes, and stencils shared across interfaces
  • deterministic SVG and PNG rendering, static SVG import and export, and native path geometry with hierarchy editing
  • native desktop files, browser WASM sessions, and shared editor, runtime, input, and renderer packages
  • CLI workflows for file and live-session inspection, queries, validation, structured mutations, dry runs, rendering, schemas, and machine-readable output

The linked pages in this section describe these components in detail. Start with Documents for the record model, Transactions and sync for commits and merges, SVG import for interchange, and Native path geometry for vector editing.

Architecture

Browser app ───────┐
                   ├── @inkfinite/ui
Desktop frontend ──┘        │
                            └── @inkfinite/editor ── @inkfinite/core

                                      ├── DOM input
                                      ├── interaction runtime
                                      └── Canvas 2D renderer

Desktop frontend
      │ generated contracts + Tauri commands

Tauri backend ── inkfinite-core ── Automerge / .inkfinite
      │                 │
      │                 ├── transactions + validation
      │                 ├── queries + history
      │                 ├── deterministic SVG rendering
      │                 ├── files + recovery
      │                 └── sync + IPC

      └── authenticated local IPC ── inkfinite CLI

The web and desktop applications share the editor UI and interaction packages, but they do not use the same persistence adapter. The web editor persists its editor document in IndexedDB. The desktop editor projects the Rust-owned native document into editor state and sends completed document edits back through Tauri.

See Web editor and Desktop editor for the application-specific behavior.

Dependency direction and model ownership

Rust owns the canonical document and all operations that can change it. inkfinite-core defines Document, ShapeRecord, PageRecord, LayerRecord, and BindingRecord, validates them, applies transactions, and owns Automerge state, native files, sessions, and headless rendering.

Its source layout follows the same ownership split:

  • src/model/ contains canonical IDs, value types, shape registry types, records, snapshots, and shape-property validation. lib.rs re-exports the existing root model names.
  • src/geometry/ contains boolean operations, connector resolution, graph layout, path topology, path metrics, and routing. Existing root module paths remain available as re-exports; the engine’s world-transform and bounds helpers remain under engine::geometry because the transaction engine owns those document traversals.
  • src/editor/ contains editor-facing projection data, canonical-to-editor projection, and editor patch reconciliation. inkfinite_core::editor keeps the public editor API while its implementation is split into model, projection, and reconciliation modules.
  • src/wasm.rs, the CLI, MCP, and Tauri code are adapters. They call the core APIs and do not define parallel canonical records or transaction semantics.

The binding generator is the only path from those Rust contracts to TypeScript contract types:

inkfinite-core Rust model and services

        └── generate-bindings ──> @inkfinite/bindings

                                      └── generated snapshots, projections, patches, and protocols

@inkfinite/core/src/editor-model.ts owns the interactive model. Its public records are named EditorDocument, EditorShapeRecord, EditorPageRecord, EditorLayerRecord, and EditorBindingRecord so they cannot be mistaken for the generated Rust records. These values use editor property names, world-space transforms, flat draw order, and the mutable shape union needed by tools and Canvas rendering. They are not another serialized document contract.

@inkfinite/core/src/persistence/canonical.ts is the TypeScript adapter boundary. It converts Rust snapshots and generated Rust editor projections into the interactive model, and turns completed editor changes into generated EditorPatch requests. Browser and desktop adapters call this module instead of translating records themselves. The adapter preserves native property names and hierarchy only at the Rust boundary; it does not write .inkfinite bytes.

The package and adapter dependency direction is:

Rust model ──> Rust geometry and transaction engine ──> Rust editor projection/reconciliation
      │                         │                              │
      └── generate-bindings ────┴──────────────────────────────┘

                              @inkfinite/bindings

                         @inkfinite/core/canonical adapter

                    @inkfinite/editor ──> @inkfinite/ui
                                      │             │
                           web and desktop apps ───┘

Rust inkfinite-wasm ──> web persistence adapter
Rust Tauri commands ──> desktop persistence adapter
CLI and MCP ──> inkfinite-core

Core model code does not import UI, editor runtime, applications, or platform services. Core geometry and transaction code may use the model, while the editor projection and reconciliation layer may use canonical geometry and transaction APIs. Applications and adapters depend on the core; the core does not depend on them.

@inkfinite/editor owns normalized input, interaction state, commands, Canvas rendering, and browser Canvas export adapters. The UI package owns Svelte presentation, inspector controls, file browser and status-bar view models. Applications own browser storage, filesystem access, Tauri or WASM calls, and composition. No UI, editor runtime, or application code owns canonical records or applies native transactions directly.

@inkfinite/core is headless and has no DOM, Svelte, application, or platform imports. Its root entry point is a convenience API; capability-oriented consumers can use the explicit model, geometry, commands, selection, interchange, and persistence entry points. The core root keeps SVG export and pure paint serialization, while @inkfinite/editor/export owns PNG helpers that require HTMLCanvasElement. Browser and desktop persistence implementations stay in their application roots, and the desktop file-operation contract lives beside the Tauri adapter.

For the record structure, see Document model. The native path geometry guide documents the path representation used by SVG interoperability and vector editing. The SVG import guide documents the parser’s native mappings, transform rules, styles, text behavior, and asset handling. The testing guide documents shared fixtures and focused verification. Performance corpus and profiling describes the native, process, renderer, and browser measurement layers.

Edit flow

@inkfinite/editor/runtime is a framework-neutral interaction state machine. It routes normalized actions through camera and tool state, applies gesture previews to the editor store, and emits a transaction draft when an interaction reaches a commit point such as pointer-up or an editor commit.

A desktop document edit follows this path:

DOM input

@inkfinite/editor/input-dom

@inkfinite/editor/runtime

editor-state preview
   ↓ completed gesture
runtime transaction draft

desktop persistence adapter

Rust TransactionDraft

Tauri command

inkfinite-core transaction engine

Automerge change + validated snapshot

updated editor projection

The Rust transaction engine checks schema and preconditions, validates document policy and invariants, applies the operations to Automerge state, materializes and validates the result, and returns the new heads plus the affected records and regions. A rejected transaction does not partly modify the native document.

Causal heads and record versions are used for optimistic concurrency. See Transactions and sync for the public transaction model.

Codebase map

UnitResponsibility
crates/inkfinite-coreData model, transaction engine, Automerge integration, files, queries, sessions, sync, IPC, and headless rendering
crates/inkfinite-cliinkfinite command parsing, human/JSON output, file-mode operations, live desktop control, and binding/schema generation
apps/desktop/src-tauriTauri command surface and native application integration around inkfinite-core
packages/bindingsGenerated TypeScript contracts derived from Rust. Do not edit these by hand
packages/coreHeadless editor model, geometry, actions, tools, stencils, interchange, SVG export, and persistence projections
packages/editorDOM input normalization, interaction state, transaction drafts, and Canvas 2D rendering
packages/uiShared Svelte editor, panels, controls, themes, and UI components
apps/webBrowser composition root, documentation site, and IndexedDB-backed editor persistence
apps/desktopDesktop composition root and TypeScript adapter for Tauri-owned native document sessions

The Rust workspace contains inkfinite-core, inkfinite-cli, inkfinite-mcp, inkfinite-wasm, the Tauri crate, and xtask. The pnpm workspace contains the shared packages plus the web and desktop application roots.

Rendering

Interactive rendering is Canvas 2D. @inkfinite/editor/renderer subscribes to the editor store, marks the canvas dirty when state changes, and draws on the next animation frame. It maps the camera into world coordinates, culls shapes outside an expanded viewport, and keeps fixed-size caches for text and Markdown layout. Selection handles, binding previews, and snapping guides are rendered from editor-only state and are not native document records.

Headless rendering is separate. inkfinite-core renders the canonical document directly to deterministic SVG for CLI output, fixtures, and inspection. @inkfinite/core also provides pure SVG serialization for the interactive editor model. PNG export remains in @inkfinite/editor because it requires a browser Canvas. This keeps headless output independent of the browser renderer.

Desktop sessions, CLI, and MCP

The Tauri backend owns native desktop document sessions. A session contains the Rust-owned document state and exposes typed commands for snapshots, commits, undo/redo, saves, queries, validation, proposal handling, and peer sync. The TypeScript desktop adapter invokes those commands and updates its editor projection from returned session state.

The CLI calls the same Rust core for closed-file operations. For open desktop documents it uses the authenticated local IPC protocol exposed by the Tauri process rather than racing the desktop file writer.

The local inkfinite-mcp stdio server also uses the core transaction and query APIs. It discovers open sessions through authenticated desktop IPC and opens only the standalone files supplied to the process. MCP applies source-specific permissions before core validation and can submit proposals to an open desktop session for review.

The Rust protocol supports direct live commits and optional proposals. See Command-line interface and Agent workflows for the supported commands, MCP policy, and review flow.

Files and generated contracts

Canonical desktop files are Automerge-backed .inkfinite documents. Rust owns native reads, validation, file locking, recovery state, and atomic replacement. JSON is an inspection projection. SVG, PNG, Excalidraw, and Obsidian Canvas are interchange or presentation formats rather than alternate native documents. See File format for the user-facing format contract.

Rust types are serialized with Serde, described with Schemars, and exported to TypeScript with ts-rs. pnpm bindings:generate regenerates @inkfinite/bindings, while pnpm bindings:check verifies that checked-in generated contracts still match Rust.

This generated boundary is used for document, transaction, protocol, and browser WASM payloads. The hand-written @inkfinite/core Editor* types remain a separate interaction-oriented representation; persistence/canonical.ts is the only adapter between the two.