Skip to content
Documentation GitHub
Architecture

Architecture Overview

Status: Accepted Reference epics: INK-833, INK-837 ADRs: ADR-016, ADR-018, ADR-020

Inklings is a local-first workbench for authoring a world — a worldbuilder’s term for the bounded body of content a person is building. The worldbuilder may be a fiction author building a setting, a researcher building a knowledge base, a product team building a design system, or anyone else whose work is the coherent, long-lived, authored accumulation of knowledge about a bounded subject.

The architecture is organized around three commitments that the rest of this document elaborates:

  1. A workspace is a world. Each workspace contains the author’s content plus the structural records that make the content a world rather than a bag of notes — provenance, derivation, deviation, lifecycle. A single workspace has a single World Agent that inhabits that world.
  2. Every write crosses the submit boundary. The workspace’s structural records are maintained by routing every write — whether the author typed it, the agent produced it, a scheduled task emitted it, or an import pipeline delivered it — through a single domain construct that captures origin, lifecycle, and derivation. This is an invariant of the domain model, not a convention. See submit-boundary and ADR-017.
  3. The World Agent runs in the Python sidecar on LangGraph. Agent reasoning, tool calls, scheduling, and checkpointing live in a single sidecar process built directly on LangGraph. The Rust side owns the domain, storage, and tools; the sidecar calls Rust tools over MCP; the submit boundary runs on the Rust side. See ADR-016.

For the world-model primitives — World Agent, zones, provenance, derivation, deviation, retroactive revision, the submit boundary, sandbox execution — read systems/world. That directory is canonical. This page describes how the code is organized to implement those primitives.

Inklings is a polyglot modular monolith following Clean Architecture with compile-time layer enforcement via separate Rust crates. Dependencies flow inward: framework and infrastructure depend on application and domain, never the reverse.

Four foundational decisions shape the architecture:

  1. Rust owns the domain. All business logic, validation, and data integrity rules live in Rust. The TypeScript frontend is a “dumb pipe” that handles only UI/UX behavior. The Python sidecar holds the agent runtime but calls into Rust for every workspace-visible operation.
  2. SQLite per workspace. Each workspace is a single SQLite database file (inklings.db) that holds content, structural records, and agent state together. Local-first by default, with optional cloud sync via Supabase. The separate agents.db of earlier builds has been folded into inklings.db; see Database Schema.
  3. CRDT-backed content. Block content uses Loro CRDTs for persistent undo/redo and multi-device sync. The CRDT binary is the source of truth; materialized text is a derived projection for search indexing.
  4. Submit is the domain boundary. Workspace writes are only expressible in the domain through a single construct that carries origin, lifecycle, and (where applicable) derivation, deviation, and retroactive-revision impact. Every caller — Rust tool, Python tool via MCP, import pipeline, scheduled task — constructs the same world-write value on the Rust side. See Domain Rules for the invariants.

The Rust agent harness (agent-core, agent-harness) has been removed. Agent execution — planning, tool dispatch, subagent orchestration, checkpointing, streaming — runs in the Python sidecar on LangGraph. The sidecar calls Rust through the MCP server; boundary-crossing tools declare their shape at registration and the adapter constructs the domain’s world write on their behalf. See ADR-016.

Pure business entities with zero external dependencies. Every struct, enum, and validation rule here is framework-agnostic and infrastructure-agnostic.

Key contents:

  • World primitives: Origin, Lifecycle, DerivationLink, DeviationRecord, RevalidationFlag, WorldWrite
  • Entity types: Page, Block, Tag, Workspace, Attachment, Bookmark, EventLogEntry, Participant, Channel, Conversation
  • Value objects: WorkspaceName (filesystem-safe validation), Layout (CSS Grid template validation), IconConfig
  • Enums: PageType, BlockContentType, Capability, EntityType, EventType
  • Domain errors and validated identifier newtypes

See the Entity Catalog for the auto-generated inventory and Domain Rules for the invariants.

Use cases and service trait abstractions. Each use case is a single-purpose struct following the file-per-use-case pattern:

crates/application/src/page/
├── mod.rs # Re-exports
├── services.rs # PageRepository trait + errors
├── get.rs # GetPageUseCase
├── create.rs # CreatePageUseCase
└── update.rs # UpdatePageUseCase

Use cases that write to the workspace take a WorldWrite value, not loose arguments. The world-write is constructed by the caller (command, MCP adapter, import pipeline, scheduled task) and validated at the domain layer before the use case executes. This is the mechanism that makes the submit boundary a domain invariant rather than a layer convention.

The application layer depends only on domain. Infrastructure is injected via trait objects (e.g., Box<dyn PageRepository>), enforcing the dependency inversion principle at compile time.

Infrastructure layer (crates/infrastructure/)

Section titled “Infrastructure layer (crates/infrastructure/)”

Concrete implementations of application-layer traits:

CratePurpose
sqliteSQLite storage — WAL mode, 1 writer + 4 reader pool, world primitives tables
mcp-serverRust MCP server; hosts all in-process tools (content, search, memory, sandbox) and carries submit-boundary registration metadata
onnxONNX Runtime embedding (snowflake-arctic-embed-m-v2.0)
supabaseCloud services — auth, sync, analytics, realtime, storage
llm-ingestProvider SDKs for LLM-facing ingest work that runs inside Rust; the sidecar owns conversational LLM calls directly
task-runnerBackground task execution; dispatches scheduled work into the sidecar by thread_id
importExternal markdown import (Obsidian, Notion, AnyType, folders) — constructs world writes with origin: Imported, lifecycle: Draft
sandboxWasmtime-sandboxed CPython executor, exposed as an MCP tool. Distinct from the agent runtime. See sandbox-execution and ADR-021.
imageImage processing (resize, WebP conversion)
jsonJSON file-based settings repository

The framework layer encompasses the Tauri desktop shell, the React frontend, the HTTP bridge, and the Python sidecar — all of them outermost adapters translating between some external surface (user, test harness, agent loop) and the application core.

Commands (crates/commands/): Transport-agnostic validation, error sanitization, and shared types. Both Tauri and the HTTP bridge use the same command definitions. This crate strips internal error details before they reach the frontend, ensuring user-facing error messages are always safe and meaningful.

Tauri desktop app (apps/desktop/src-tauri/): Thin adapter layer that wires infrastructure implementations to application use cases via dependency injection (state.rs). Tauri commands are one-per-domain-area thin shim modules that delegate to commands/use cases.

React frontend (apps/desktop/src-react/): Single-page app with Zustand state management. Contains zero business logic — all data operations go through Tauri invoke() IPC calls. TipTap editor with Loro CRDT integration.

HTTP bridge (apps/http-bridge/): Axum REST server that replaces Tauri IPC for QA testing. Same command layer, different transport.

Python sidecar (apps/python-sidecar/): The World Agent runtime, built directly on LangGraph. Hosts the planning loop, subagent dispatch, tool-calling, streaming, checkpointing, and scheduling resumption. Calls Rust tools over MCP; carries no domain logic of its own. State is persisted in inklings.db via the LangGraph SQLite checkpointer. See ADR-016 and systems/agent/process-model.

Crate names reflect their Clean Architecture layer and responsibility:

  • inklings-domain — The single domain crate (no prefix needed within)
  • inklings-application — Use cases and service traits
  • inklings-commands — Shared validation and error sanitization (framework layer)
  • inklings-sqlite, inklings-onnx, etc. — Infrastructure crates named by technology
  • inklings-mcp-server — Rust MCP server; hosts all in-process tools
  • inklings-sandbox — Wasmtime-sandboxed CPython executor

Removed names: inklings-agent-core and inklings-agent-harness no longer exist; the sidecar replaces them. See ADR-016 for the full rationale.

Rust structs are projected to TypeScript via Specta:

Rust structs (with #[derive(Type)])
→ Specta code generation
→ packages/contracts/generated/bindings.ts
→ TypeScript consumers import generated types

Run pnpm generate:types after modifying any Rust type that crosses the IPC boundary. Specta preserves snake_case field names (Rust storage_path → TypeScript storage_path, not storagePath).

The world primitives (Origin, Lifecycle, DerivationLink, DeviationRecord, RevalidationFlag) are exposed to the frontend so candidate-content UX, deviation inboxes, and re-validation queues can render without round-tripping descriptions.

The application is organized into four functional groups of systems. Canonical world-model concepts live under systems/world/; the other groups describe how the code is organized to implement them.

The canonical surface for world-model primitives. Every other system in the codebase cross-links up to these pages for primitive definitions.

SystemDescription
World (index)Orientation — what the world model is and how to read this directory
World AgentResident-inhabitant framing, six properties, author authority
World ZonesThree-zone topology (Known, Unknown, Unknowable)
ProvenanceOrigin, lifecycle, weight, standing
Derivation LinksDerivation relation and how it feeds standing
Deviation RecordsRecord-not-entity, four conflict types, triage
Retroactive RevisionFlag-not-rewrite queue, three options
Submit BoundaryDomain invariant + adapter mechanism
Sandbox ExecutionWasmtime CPython executor as capability, not runtime
Worked ExampleFiction-author magic-system walkthrough across the primitives

Core content management — pages, blocks, and the structures that connect them.

SystemDescription
Page SystemCore content CRUD, hierarchy, blocks
Block Content SystemBlock content types (Markdown, Image)
Attachment SystemFile attachments with SHA-256 dedup
Wiki-Link SystemWiki-link parsing, reference index, ghost detection
Tag SystemTag management with groups, FTS5, frontmatter bridge
Property SystemInline property references and autocomplete
Layout SystemCSS Grid template areas with per-cell CRDT
Loro CRDT SystemCRDT integration, binary passthrough, TipTap sync
Workspace SystemWorkspace initialization, switching, lifecycle
Type SystemType definitions, properties, container rules
Import SystemExternal markdown and Obsidian/Notion/AnyType vault import; lands content as lifecycle: Draft through the submit boundary

How the World Agent is implemented. These pages describe participation; for the primitives the agent participates in, read systems/world.

SystemDescription
Agent Core SystemSidecar runtime, LangGraph subgraph dispatch
Agent Memory SystemFour-tier memory, MCP-exposed memory tools
Conversation SystemConversation and scratchpad lifecycle
LLM SystemProvider SDKs called from sidecar nodes
MCP SystemRust MCP server, submit-boundary registration metadata
Process ModelSidecar-as-single-Python-host; IPC surface
Skill SystemSkill Composer as subagent subgraph; no compilation pipeline
Scheduling SystemCategory-based scope grants; checkpoint resumption on trigger
Prompt Injection BoundaryRuntime trust boundary (distinct from the submit boundary)

Infrastructure that supports all content and agent systems.

SystemDescription
Analytics SystemPseudonymous offline-first analytics event queue
Auth SystemAuthentication via Supabase Auth
Bookmark SystemNamed anchors into the event log timeline
Embedding SystemONNX Runtime embedding pipeline
Event Log SystemContinuous event log with named bookmarks; World Agent autonomous-task entries
Frontend SystemReact frontend architecture and IPC patterns
Model DownloaderAuto-download embedding model from HuggingFace
Permission SystemCapability-based access control, with the World Agent as a participant
Python Sidecar SecuritySandbox hardening and trust boundary
Search SystemFTS5 + semantic search with intent classification
Sync SystemMulti-device sync via Supabase
Task Runner SystemBackground task execution; IPC triggers sidecar against thread_id
User Settings SystemApplication preferences
Write-Effect CoordinatorSide-effect dispatch for writes
Command PaletteKeyboard-driven command search
First Launch ExperienceOnboarding tour

Each workspace is a single SQLite database (inklings.db) using WAL mode with a connection pool (1 writer, 4 readers). It holds content, structural records, and agent state together. Key tables:

  • Contentpages, blocks (with content_loro BLOB source of truth), frontmatter, tags, page_tags, tag_groups, references, attachments, attachment_references
  • World primitivesorigin, lifecycle, derivation_link, deviation_record, revalidation_flag. Origin and lifecycle attach to every page and block write; the other three are first-class tables keyed into workspace content.
  • Type systemtypes, properties, type_property_refs, page_type_assignments, container_rules, collection_views, layouts
  • Event historyevent_log, bookmarks, event_log_summaries
  • Searchpage_embeddings, pages_fts
  • Agent runtimechannels, conversations, scratchpads, memory_entries, langgraph_checkpoints, langgraph_writes

The previously separate agents.db has been folded in. See Database Schema for the full schema reference and the migration that consolidates storage.

Application settings stored as JSON (settings.json). Schema-versioned with migration support. The agents.db consolidation does not touch settings; application-wide preferences continue to live outside workspace databases.

The app uses environment-aware paths to isolate dev/test data from production:

EnvironmentWorkspacesSettings
Dev{project}/.data/workspaces/{project}/.data/settings.json
Production~/Inklings/Workspaces/~/Library/Application Support/Inklings/settings.json

Debug builds (pnpm desktop:dev) use dev mode; release builds (pnpm desktop:build) use production mode.

PrincipleImplementation
Dependencies flow inwardSeparate crates enforce at compile time — infrastructure cannot import domain directly
Submit is a domain invariantThe only way to modify workspace content is to construct a WorldWrite; the domain rejects malformed ones
Agent runtime is LangGraph on the sidecarOne Python host, narrow IPC surface, checkpointer on workspace SQLite
Vertical slicingFeatures implemented end-to-end through all layers, not layer-by-layer
CRDT binary passthroughNever re-serialize a CRDT doc from materialized text — pass the binary through untouched
Frontend as dumb pipeReact contains no business logic — only UI/UX behavior
Identity vs auth vs analyticsThree distinct concerns with separate storage and lifecycle
Infrastructure naming disciplineVendor-specific names (e.g., supabase_*) only in infrastructure layer
  • Domain Rules — Business invariants enforced in Rust, including submit-boundary, origin immutability, lifecycle transitions, derivation-link constraints
  • Entity Catalog — Auto-generated domain entity inventory
  • Database Schema — Complete schema reference, including world primitives and LangGraph checkpointer tables
  • Identifier Strategy — Three-tier identifier model: UUID, slug, and ref_code
  • Error Handling — Error patterns across layers
  • systems/world — Canonical world-model primitives

Was this page helpful?