Tauri Commands
Codebase: apps/desktop/src-tauri/src/commands/ Shared Error Types: crates/commands/ State:
apps/desktop/src-tauri/src/state.rs
Diagram
Overview
Tauri commands are the IPC bridge between the React frontend and the Rust backend. Each command is a thin adapter that validates input at the system boundary, constructs the appropriate use case, executes it, and returns a typed result. Commands contain no business logic — they delegate everything to the application layer.
Command Organization
Commands are organized in src/commands/mod.rs by domain area:
| Module | Purpose | Key Commands |
|---|---|---|
workspace | Workspace CRUD and initialization | initialize_workspace, open_workspace, create_workspace |
page | Page CRUD, block content, tree operations | create_page, get_page, save_block_content, list_page_tree |
attachment | File attachments (upload, retrieve, delete) | upload_attachment, get_attachment_file, delete_attachment |
tag | Tag and tag group management | create_tag, set_page_tags, merge_tags |
type_def | Type definitions + type-property linking | create_type, add_property_to_type |
property | Property definition CRUD | create_property, update_property |
property_value | Get/set property values on pages | get_property_values, set_property_value |
layout | Layout definition CRUD + page layout ops | create_layout, apply_layout, assign_block_area |
collection_view | Browse pages filtered by type | get_collection_view |
container_rule | Automatic type assignment rules | create_container_rule |
filtered_tree | Sidebar tree filtered by type | get_filtered_tree |
event_log | Event log and timeline queries | query_page_events, query_page_timeline, query_timeline |
bookmark | Named history bookmarks | create_bookmark, list_bookmarks, delete_bookmark |
search | Full-text and semantic search | search_pages |
import | External markdown import (Obsidian, etc.) | import_markdown |
settings | Application settings | get_settings, update_setting |
recent_workspaces | Recent workspace list | list_recent_workspaces, add_to_recent_workspaces |
mcp | MCP server control | get_mcp_status, set_mcp_enabled, regenerate_mcp_token |
sync | Sync engine lifecycle | start_sync, stop_sync, get_workspace_sync_status |
embedding | Embedding pipeline status | get_embedding_status |
template | Template catalog browsing | list_templates, apply_template |
agent | Agent harness lifecycle | start_agent, stop_agent, get_agent_status |
ollama | Ollama local model management | check_ollama_health, list_ollama_models, pull_ollama_model |
openrouter | OpenRouter OAuth + model access | start_openrouter_auth, list_openrouter_models |
presence | User/agent presence tracking | get_presence, update_presence |
proactive | Proactive assistant suggestions | get_proactive_suggestions, dismiss_suggestion |
task_runner | Background task status | get_task_runner_status |
auth | Authentication (login, logout, session) | login, logout, get_session |
memory | Agent memory CRUD | save_memory, get_memories, delete_memory, search_memories |
skill | Agent skill management | list_skills, get_skill, execute_skill |
model_download | Embedding model download control | start_model_download, get_download_status, cancel_download |
logging | Frontend log forwarding | log_frontend_event |
The Adapter Pattern
Every command follows the same structure:
#[tauri::command]#[specta::specta]pub fn create_page( state: State<AppState>, // 1. Inject shared state title: String, // 2. Accept frontend parameters parent_slug: Option<String>,) -> Result<CreatePageResult, CommandError> { let _span = command_span("create_page").entered(); // 3. Tracing span
// 4. Validate at system boundary validate_title(&title)?; if let Some(ref parent) = parent_slug { validate_slug_input(parent, "parent slug")?; }
// 5. Get workspace (most commands need this) let workspace = state.require_workspace()?;
// 6. Resolve permissions let guard = state.resolve_owner_guard()?;
// 7. Construct and execute use case let use_case = CreatePageUseCase::new(state.page_repo()); let response = use_case .execute(&guard, &workspace.path, CreatePageRequest { title, parent_slug, content: None }) .map_err(|e| sanitize_error(e, "create_page"))?;
// 8. Trigger side effects (embedding, sync queue) WriteEffectCoordinator::on_page_created(&state);
// 9. Return typed result Ok(CreatePageResult { slug: response.slug, ... })}Key characteristics:
#[tauri::command]— Makes the function callable from the frontend viainvoke()#[specta::specta]— Generates TypeScript types for parameters and return values- No business logic — Commands never make decisions about data; they only wire inputs to use cases
- Validation at boundary — Input validation (
validate_title,validate_slug_input,validate_uuid) happens before entering the use case layer - Error sanitization — All errors pass through
sanitize_error()orsanitize_validation_error()to strip technical details before reaching the frontend
State Injection
Commands receive AppState via Tauri’s dependency injection (State<AppState>). The state struct at src/state.rs is
the composition root — it holds all repositories, use cases, and lifecycle managers:
Repository Access
Repositories are accessed via helper methods that clone stateless unit structs:
let use_case = CreatePageUseCase::new(state.page_repo());let use_case = CreateTagUseCase::new(state.tag_repo());Most SQLite repositories are stateless unit structs (they open the per-workspace database on demand using the workspace
path). Arc-wrapped repositories (e.g., page_repository) are shared when multiple components need the same instance.
Workspace Resolution
Most commands need the active workspace. Two convenience methods:
// Returns Workspace or CommandError::NoWorkspacelet workspace = state.require_workspace()?;
// Returns PermissionGuard or CommandError::Permissionlet guard = state.resolve_owner_guard()?;Lifecycle Managers
State also holds lifecycle managers for background services:
| Manager | Purpose |
|---|---|
embedding_manager | Background embedding pipeline |
sync_manager | Cloud sync engine |
mcp_server | MCP server handle |
analytics_manager | Background analytics flush |
Error Handling
Shared Error Types (crates/commands/)
The commands crate provides transport-agnostic error types shared between Tauri IPC and the HTTP bridge (MCP server):
#[serde(tag = "type", content = "data")]pub enum CommandError { NotFound { entity: String, id: String }, Validation { message: String }, Internal { message: String }, NoWorkspace, Permission { message: String }, Unauthenticated { message: String },}This serializes to { type: "Validation", data: { message: "..." } } on the frontend. The parseCommandError() utility
in lib/errors.ts handles all variants.
Error Sanitization
The UserFacingError trait and sanitize_error() / sanitize_validation_error() functions ensure that technical
details (file paths, SQL errors, Rust stack traces) never reach the frontend:
- Validation errors (user-correctable) map to
CommandError::Validation - System errors (infrastructure failures) map to
CommandError::Internalwith a generic message - Expected errors (NotFound, AlreadyExists) are logged at
warn!level - Unexpected errors (IO, database) are logged at
error!level with full technical details
Validation Functions
The commands crate provides boundary validation:
| Function | Purpose |
|---|---|
validate_title() | Non-empty, max length, no control characters |
validate_slug_input() | Valid slug format, no path traversal |
validate_uuid() | Valid UUID v4 format |
validate_content_size() | Content within size limits |
Type Generation
Commands annotated with #[specta::specta] automatically generate TypeScript types in packages/contracts/generated/.
Run pnpm generate:types after modifying command signatures or return types.
The generated types use snake_case (Specta preserves Rust naming conventions), so the frontend accesses fields as
result.storage_path, not result.storagePath.
Adding a New Command
- Create the use case in
crates/application/src/{domain}/ - Add the command function in
src/commands/{module}.rs - Annotate with
#[tauri::command]and#[specta::specta] - Register in
main.rs(add to theinvoke_handlermacro) - Run
pnpm generate:typesto update TypeScript bindings - Call from the frontend via
invoke<ReturnType>("command_name", { params })
Related Documentation
- Page System — Page use cases consumed by page commands
- Attachment System — Attachment use cases and file storage
- MCP System — MCP server that reuses the same error types
Was this page helpful?
Thanks for your feedback!