File: README.md
๐ง Tolaria
Tolaria is a desktop app for macOS, Windows, and Linux for managing markdown knowledge bases. People use it for a variety of use cases:
- Operate second brains and personal knowledge
- Organize company docs as context for AI
- Store OpenClaw/assistants memory and procedures
Personally, I use it to run my life (hey ๐ Luca here). I have a massive workspace of 10,000+ notes, which are the result of my Refactoring work + a ton of personal journaling and second braining.
Sponsors
Tolaria is supported by a small panel of tools that help keep the project healthy, tested, and ready for AI-assisted development. I use these tools every day.
Walkthroughs
You can find some Loom walkthroughs below โ they are short and to the point:
Principles
- ๐ Files-first โ Your notes are plain markdown files. They're portable, work with any editor, and require no export step. Your data belongs to you, not to any app.
- ๐ Git-first โ Every vault is a git repository. You get full version history, the ability to use any git remote, and zero dependency on Tolaria servers.
- ๐ Offline-first, zero lock-in โ No accounts, no subscriptions, no cloud dependencies. Your vault works completely offline and always will. If you stop using Tolaria, you lose nothing.
- ๐ฌ Open source โ Tolaria is free and open source. I built this for myself and for sharing it with others.
- ๐ Standards-based โ Notes are markdown files with YAML frontmatter. No proprietary formats, no locked-in data. Everything works with standard tools if you decide to move away from Tolaria.
- ๐ Types as lenses, not schemas โ Types in Tolaria are navigation aids, not enforcement mechanisms. There's no required fields, no validation, just helpful categories for finding notes.
- ๐ชAI-first but not AI-only โ A vault of files works very well with AI agents, but you are free to use whatever you want. We support Claude Code, Codex CLI, and Gemini CLI setup paths, but you can edit the vault with any AI you want. We provide an AGENTS file for your agents to figure out.
- โจ๏ธ Keyboard-first โ Tolaria is designed for power-users who want to use keyboard as much as possible. A lot of how we designed the Editor and the Command Palette is based on this.
- ๐ช Built from real use โ Tolaria was created for manage my personal vault of 10,000+ notes, and I use it every day. Every feature exists because it solved a real problem.
Installation
Homebrew
Install via Homebrew on macOS:
brew install --cask tolariaDownload from releases
Download the latest release here for macOS, Windows, or Linux. Windows installers are Authenticode-signed; company-managed devices may still require IT approval of the Tolaria publisher before first install.
Getting started
When you open Tolaria for the first time you get the chance of cloning the getting started vault โ which gives you a walkthrough of the whole app.
The public user docs live in site/ and are published to GitHub Pages. Start with Install Tolaria, then First Launch.
Open source and local setup
Tolaria is open source and built with Tauri, React, and TypeScript. If you want to run or contribute to the app locally, here is how to get started. You can also find the gist below ๐
Prerequisites
- Node.js 20+
- pnpm 8+
- Rust stable
- macOS or Linux for development
Linux system dependencies
Tauri 2 on Linux requires WebKit2GTK 4.1 and GTK 3:
- Arch / Manjaro:bash
sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file openssl \ appmenu-gtk-module libappindicator-gtk3 librsvg - Debian / Ubuntu (22.04+):bash
sudo apt install libwebkit2gtk-4.1-dev build-essential curl wget file \ libxdo-dev libssl-dev libayatana-appindicator3-dev librsvg2-dev \ libsoup-3.0-dev patchelf - Fedora 38+:bash
sudo dnf install webkit2gtk4.1-devel openssl-devel curl wget file \ libappindicator-gtk3-devel librsvg2-devel
The bundled MCP server still spawns the system node binary at runtime on Linux, so install Node from your distro package manager if you want the external AI tooling flow.
Quick start
pnpm install
pnpm devOpen http://localhost:5173 for the browser-based mock mode, or run the native desktop app with:
pnpm tauri devTech Docs
- ๐ ARCHITECTURE.md โ System design, tech stack, data flow
- ๐งฉ ABSTRACTIONS.md โ Core abstractions and models
- ๐ GETTING-STARTED.md โ How to navigate the codebase
- ๐ ADRs โ Architecture Decision Records
Security
If you believe you have found a security issue, please report it privately as described in SECURITY.md.
License
Tolaria is licensed under AGPL-3.0-or-later. The Tolaria name and logo remain covered by the projectโs trademark policy.
File: docs/adr/0001-tauri-react-stack.md
type: ADR id: "0001" title: "Tauri v2 + React as application stack" status: active date: 2026-02-14
Context
Laputa is a desktop app for macOS (with iPad as a future target) that reads and writes a vault of markdown files. The app needs a native feel, filesystem access, git integration, and a rich text editor. A single developer (with AI assistance) is building it.
Decision
Use Tauri v2 (Rust backend + WebView frontend) with React + TypeScript for the UI, BlockNote for the editor, and Vitest + Playwright for testing.
Alternatives considered
- Electron: heavier runtime (~150MB), slower, but more mature ecosystem. Rejected โ Tauri is lighter and has better native integration.
- SwiftUI: best native macOS/iOS experience, but locks to Apple platforms only, no code sharing with a potential web version, and requires rewriting the entire UI. Rejected for the initial version โ revisited in ADR-0005.
- Flutter: cross-platform but WebView-based editor would have been poor; Dart ecosystem is thin for markdown tooling.
- Pure web app: no filesystem access, no git, would require a backend server. Rejected โ offline-first is a core principle.
Consequences
- React frontend can be shared with a future web version
- Rust backend provides safe, fast filesystem/git operations
- Tauri v2 supports iOS (beta) โ see ADR-0005 for iPad strategy
- CodeScene code health monitoring applies to both Rust and TypeScript code
- Claude Code can work on both layers without context switching
- Triggers re-evaluation if: Tauri iOS proves unstable for production, or if SwiftUI becomes the primary target platform
File: docs/adr/0002-filesystem-source-of-truth.md
type: ADR id: "0002" title: "Filesystem as the single source of truth" status: active date: 2026-02-14
Context
Laputa needs a persistence model. The core question: does the app own the data, or does the filesystem? This affects sync, conflict resolution, offline support, portability, and long-term trust with users.
Decision
The vault is the source of truth. The app never owns the data โ it only reads and writes .md files. All cache, React state, and in-memory representations are derived from the filesystem and must be reconstructible by deleting them. When in doubt, the file on disk wins.
Alternatives considered
- Database-first (SQLite): faster queries, easier relationships. Rejected โ creates lock-in, makes files unreadable outside the app, complicates sync.
- Cloud-first (proprietary sync): easier multi-device. Rejected โ zero lock-in is a core principle; git handles sync.
- Hybrid (DB + files): DB as primary, files as export. Rejected โ two sources of truth always diverge.
Consequences
- Notes are plain markdown files, readable and editable by any text editor
- Git provides history, sync, and collaboration for free
- Vault can be opened/edited externally without app corruption
- App rebuilds cache on startup โ acceptable cost for integrity guarantees
- No "save" button needed โ autosave writes to disk immediately
- Triggers re-evaluation if: vault size grows to millions of files and filesystem scanning becomes a bottleneck
File: docs/adr/0003-single-note-model.md
type: ADR id: "0003" title: "Single note open at a time (no tabs)" status: active date: 2026-03-24
Context
The app originally had a tab bar allowing multiple notes to be open simultaneously (similar to a browser or code editor). After building and shipping it, the tab model was found to add significant UI complexity, state management overhead, and confusion โ without a proportional benefit for a notes app.
Decision
Remove the tab bar. Only one note is open at a time. Navigation history (Back/Forward with Cmd+[/]) replaces tabs for moving between recently visited notes. Closed tab history and useTabManagement are removed.
Alternatives considered
- Keep tabs: familiar UX, allows comparing notes side by side. Rejected โ adds ~2000 lines of complexity, confusing state (which tab is "active"?), and breaks the "editor is sacred" principle.
- Tabs + single-note toggle: configurable per user. Rejected โ doubles the state surface and testing burden.
- Split pane (two notes at once): useful for reference. Deferred โ can be added later without tabs, via a dedicated split layout.
Consequences
- Removes ~2000 lines of code (
TabBar,useClosedTabHistory,useEditorTabSwap,tabLayout) handleSelectNotereplaces the current note instead of adding a tab- Cmd+W (close tab) and Cmd+Shift+T (reopen closed tab) removed from shortcuts
- Back/Forward navigation (Cmd+[/Cmd+]) preserves history without tab state
- Significant simplification of
App.tsxand editor state - Triggers re-evaluation if: multi-note workflows become a top user request
File: docs/adr/0004-vault-vs-app-settings-storage.md
type: ADR id: "0004" title: "Vault vs app settings for state storage" status: active date: 2026-03-24
Context
As features were added, there was recurring ambiguity about where to persist configuration and state: in the vault (as frontmatter in .md files) or in app settings (~/.config/com.laputa.app/settings.json / localStorage). Without a clear rule, some decisions were inconsistent.
Decision
Ask: "Would the user want this to follow the vault across all their installations?"
- If yes โ store in the vault (as frontmatter in the relevant
.mdfile, using the_convention for system properties) - If no โ store in app settings
Examples:
| Data | Storage | Reason |
|---|---|---|
Note type icon (_icon) |
Vault frontmatter | Follows the vault everywhere |
Note type color (_color) |
Vault frontmatter | Follows the vault everywhere |
| Note sort preference | Vault frontmatter (type file) | Per-vault, consistent across devices |
| API keys (Anthropic, OpenAI) | App settings | Installation-specific |
| GitHub token | App settings | Installation-specific |
| Window size / zoom | App settings | Device-specific |
| Editor zoom level | App settings | Device-specific |
| Telemetry consent | App settings | Installation-specific |
Alternatives considered
- Everything in localStorage: simple, but breaks cross-device sync for vault-level config.
- Everything in vault: pure, but makes device-specific settings (zoom, window size) propagate to all devices โ confusing.
Consequences
- Config that "belongs to a note or type" lives in frontmatter โ readable/diffable in git
- The
_prefix convention (see ABSTRACTIONS.md) distinguishes system properties from user properties - App rebuilds from vault state on open โ no stale config files to manage
- Triggers re-evaluation if: vault files become too polluted with system frontmatter properties
File: docs/adr/0005-tauri-ios-for-ipad.md
type: ADR id: "0005" title: "Tauri v2 iOS for iPad support (vs SwiftUI rewrite)" status: active date: 2026-03-27
Context
Laputa runs on macOS via Tauri v2. The goal is to also support iPad without changing the stack or redesigning the app from scratch. The core question: extend the existing stack to iOS, or rewrite in SwiftUI for a fully native experience?
Decision
Use Tauri v2 iOS (beta) for the iPad prototype. The React frontend stays identical. The Rust backend compiles for iOS with #[cfg(desktop)] / #[cfg(mobile)] guards for platform-specific features. Desktop-only features (git CLI, macOS menu bar, MCP server, Claude CLI) are stubbed or skipped on mobile.
The prototype (feat: add iPad/iOS prototype via Tauri v2 mobile target, build b492) successfully builds and runs on iPad Pro 13" simulator (iOS 18.3.1).
Alternatives considered
- SwiftUI rewrite: best native macOS/iPad experience, full App Store integration, native TextKit 2 editor. Rejected for now โ would discard all existing React code, Rust backend, 2200+ tests, and Claude Code's accumulated context. Worth revisiting if Laputa becomes iOS-first.
- Capacitor: replaces Tauri layer, keeps React, but the Rust backend is lost entirely โ git and file operations would need reimplementation in JS or Swift.
- React Native + WebView: wraps the React app in a WebView. Too hacky, performance concerns, App Store review risks.
Git on iPad
git CLI is unavailable on iOS. Options for production:
- Option A (recommended):
isomorphic-gitโ pure JS git implementation, no native dependencies, runs in WebView. Replaces Rust git commands on mobile. - Option B (prototype): Working Copy as iOS Files provider โ user manages git separately.
- Option C: iCloud Drive sync โ no git history. Not recommended.
Consequences
- Zero frontend changes needed for basic iPad support
- Desktop features (git, MCP, Claude CLI) unavailable on iPad until isomorphic-git is integrated
- Tauri v2 iOS is still beta โ production stability unknown
- App Store distribution requires Apple Developer account and TestFlight
- Triggers re-evaluation if: Tauri iOS remains unstable after 6 months, or iPad becomes the primary target (in which case SwiftUI rewrite becomes rational)
File: docs/adr/0006-flat-vault-structure.md
type: ADR id: "0006" title: "Flat vault structure (no type-based folders)" status: active date: 2026-03-15
Context
Originally, notes were organized into type-based subfolders (project/, person/, topic/, etc.). Changing a note's type required moving it between folders, which broke wikilinks, complicated wikilink resolution (paths vs titles), and created friction for users who wanted to reorganize their knowledge. It also made vault scanning more complex and introduced edge cases around folder creation/deletion.
Decision
All user notes live as flat .md files at the vault root. Type is determined solely from the type: frontmatter field โ never inferred from folder location. Only a small set of protected folders exist: type/ (type definition documents), config/ (meta-configuration), and attachments/.
Options considered
- Option A (chosen): Flat vault with frontmatter-only type โ simple wikilink resolution (title/filename only), no file moves on type change, vault scanning restricted to root + protected folders. Downside: large vaults may look cluttered in Finder.
- Option B: Keep type-based folders โ familiar Obsidian-like structure. Downside: type changes require file moves, wikilink resolution needs path awareness, scanning is recursive and slower.
- Option C: Hybrid (folders optional, type still from frontmatter) โ maximum flexibility. Downside: two ways to do the same thing, confusing for AI agents and automation.
Consequences
- Wikilink resolution is simplified to multi-pass title/filename matching โ no path-based matching needed.
- Changing a note's type is a frontmatter edit, not a file move.
- A
flatten_vaultmigration command and wizard were added for existing vaults with type folders. vault_health_checkdetects stray files in non-protected subfolders.scan_vaultonly indexes root-level.mdfiles plus protected folders โ non-protected subdirectories are ignored.- Re-evaluation trigger: if users need nested folder hierarchies for non-type organization (e.g., project-specific subdirectories).
File: docs/adr/0007-title-filename-sync.md
type: ADR id: "0007" title: "Title equals filename (slug sync)" status: superseded date: 2026-03-15 superseded_by: "0044"
Context
With the move to a flat vault structure (ADR-0006), filenames became the primary identifier for notes. Previously, titles were extracted from the first H1 heading, which was fragile (users could delete or change the H1 without realizing it affected the note's identity). A clear, deterministic mapping between title and filename was needed.
Decision
Every note's filename is slugify(title).md. The title frontmatter field is the source of truth for the human-readable title. On note open, the system syncs the title field to match the filename if they've diverged (filename wins). On rename, both title and filename are updated atomically.
Options considered
- Option A (chosen):
title = slugify(filename)with bidirectional sync โ deterministic, predictable, wikilinks resolve by title/filename stem. Downside: titles with special characters get simplified in filenames. - Option B: UUID-based filenames with title only in frontmatter โ filenames never change. Downside: vault is unreadable in Finder/terminal, breaks the "plain markdown files" principle.
- Option C: H1-based title extraction โ no explicit title field. Downside: fragile, H1 can be accidentally deleted or changed, decoupled from filename.
Consequences
extract_titlereads from frontmattertitle:field, never from H1. Falls back toslug_to_title()(hyphens โ spaces, title-case).sync_title_on_openauto-corrects desynced frontmatter on note open.rename_noteupdates bothtitle:frontmatter and filename atomically, plus cross-vault wikilink updates.- The H1 block inside BlockNote is hidden via CSS; a dedicated
TitleFieldcomponent above the editor is the primary title editing surface. - Slug collision detection prevents duplicate filenames.
- Re-evaluation trigger: if users need filenames that don't match titles (e.g., short slugs for long titles).
File: docs/adr/0008-underscore-system-properties.md
type: ADR id: "0008" title: "Underscore convention for system properties" status: active date: 2026-03-24
Context
As Laputa added more features that store configuration in note frontmatter (pinned properties, type icons, colors, sidebar labels, sort order), the Properties panel became cluttered with internal fields that users shouldn't normally edit. A convention was needed to distinguish user-visible properties from system-internal ones.
Decision
Any frontmatter field whose name starts with _ is a system property. It is hidden from the Properties panel, not exposed in search/filters, but remains editable in the raw editor. The frontmatter parser filters out _* fields before passing properties to the UI.
Options considered
- Option A (chosen): Underscore prefix convention (
_icon,_color,_order,_pinned_properties) โ simple, readable in raw files, universal rule. Downside: users must know the convention to access system fields. - Option B: Separate YAML block or nested
_system:key โ cleaner separation. Downside: more complex parsing, breaks flat key-value frontmatter model. - Option C: Store system properties in a separate sidecar file (
.meta.yml) โ complete separation. Downside: doubles the number of files, harder to keep in sync.
Consequences
- All future system-level frontmatter fields must use the
_field_nameconvention. - Both Rust (
vault/mod.rs) and TypeScript (utils/frontmatter.ts) parsers filter_*fields before passingpropertiesto the UI. - Power users can still access and edit system properties via the raw editor.
- Type documents use
_icon,_color,_order,_sidebar_label,_pinned_properties. - Re-evaluation trigger: if the number of system properties grows large enough to warrant a structured sub-object.
Normalized system properties
| Canonical key | Old keys (read with fallback) | Written by |
|---|---|---|
_archived |
Archived, archived |
Archive action |
_trashed |
Trashed, trashed |
Trash action |
_trashed_at |
Trashed at, trashed_at |
Trash action |
_favorite |
โ | Favorite toggle |
_favorite_index |
โ | Favorite reorder |
Write rule: always use the canonical _-prefixed key.
Read rule: accept both canonical and legacy keys (case-insensitive). Do NOT rewrite on read โ migration is a separate concern.
File: docs/adr/0009-keyword-only-search.md
type: ADR id: "0009" title: "Keyword-only search (remove semantic indexing)" status: active date: 2026-03-24
Context
Laputa previously used QMD (a Go binary) for semantic vector indexing, enabling similarity-based search. This added significant complexity: a bundled Go binary requiring code-signing, an indexing step on vault open, status bar progress tracking, auto-install logic, and a separate tools/qmd/ directory. The semantic search quality did not justify the operational burden, especially as the AI agent (with MCP vault tools) became a more natural way to do exploratory queries.
Decision
Remove QMD semantic indexing entirely and keep only keyword-based search. Search uses walkdir to scan all .md files, matching against titles and content with case-insensitive substring matching and relevance scoring.
Options considered
- Option A (chosen): Keyword-only search via
walkdirโ zero dependencies, no indexing step, instant results, no binary to sign/bundle. Downside: no fuzzy or semantic matching. - Option B: Keep QMD semantic search โ richer search results, similarity matching. Downside: bundled Go binary, code-signing, indexing latency, maintenance burden.
- Option C: Replace QMD with a Rust-native embedding library โ no external binary. Downside: large model files, cold start time, still needs indexing.
Consequences
- No external search binary to bundle, sign, or install.
- No indexing step on vault open โ search is instant.
search_vaultTauri command scans files directly withwalkdir, runs in a blocking Tokio task.- Title matches rank higher than content-only matches; exact title matches rank highest.
- The AI agent (via MCP
search_notestool) provides an alternative for exploratory/semantic queries. - Re-evaluation trigger: if users report keyword search is insufficient for large vaults (9000+ notes).