Chapter 15: Desktop Apps with Tauri 2
Chapter 15: Desktop Apps with Tauri 2
Last Updated: 2026-03
15.2 Architecture
┌─────────────────────────────────────────┐
│ Tauri App │
│ │
│ ┌─────────────────┐ ┌───────────────┐ │
│ │ Frontend (Web) │ │ Backend (Rust)│ │
│ │ │ │ │ │
│ │ React │ │ Tauri core │ │
│ │ TypeScript │ │ IPC commands │ │
│ │ Zustand (state) │ │ File system │ │
│ │ Tailwind CSS │ │ Keychain │ │
│ │ │ │ SQLite (rusqlite) │
│ └────────┬─────────┘ └───────┬───────┘ │
│ │ │ │
│ └──── IPC (invoke) ──┘ │
└─────────────────────────────────────────┘
│
▼
Native OS APIs
(Keychain, filesystem, notifications)
15.3 Project Structure
my-tauri-app/
├── src/ # React/TypeScript frontend
│ ├── components/ # React components
│ ├── stores/ # Zustand state stores
│ ├── services/ # Frontend service layer (IPC calls)
│ │ ├── AgentRouter.ts # From ITI shared library
│ │ └── ConversationManager.ts
│ ├── App.tsx # Root component
│ └── main.tsx # Entry point
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── main.rs # Application entry point
│ │ ├── commands/ # IPC command handlers
│ │ │ ├── ai_commands.rs # AI/Claude commands
│ │ │ └── db_commands.rs # Database commands
│ │ └── db.rs # SQLite database setup
│ ├── Cargo.toml # Rust dependencies
│ └── tauri.conf.json # Tauri configuration
├── package.json # Node dependencies
├── tsconfig.json # TypeScript config
├── vite.config.ts # Vite build config
└── CLAUDE.md # AI context
15.4 IPC Commands
IPC (Inter-Process Communication) is how the React frontend calls Rust backend functions. Each command is defined in Rust and invoked from TypeScript.
Defining a Rust command
// src-tauri/src/commands/ai_commands.rs
use tauri::command;
#[command]
pub async fn call_claude(
system: String,
user_message: String,
) -> Result<String, String> {
// Use ITI shared claude_client
let client = ClaudeClient::new()?;
let response = client.complete(&system, &user_message).await
.map_err(|e| e.to_string())?;
Ok(response)
}
Registering the command in main.rs
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
commands::ai_commands::call_claude,
commands::db_commands::save_session,
commands::db_commands::get_sessions,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Calling from TypeScript (frontend)
import { invoke } from '@tauri-apps/api/core';
// Call a Rust command
const response = await invoke<string>('call_claude', {
system: 'You are a helpful assistant.',
userMessage: 'What is the capital of France?',
});
15.5 State Management with Zustand
Zustand is the state management library for ITI Tauri apps. It is lightweight and works well with React and TypeScript.
Store structure
// stores/conversationStore.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface Message {
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
interface ConversationState {
messages: Message[];
isLoading: boolean;
addMessage: (message: Message) => void;
setLoading: (loading: boolean) => void;
clearMessages: () => void;
}
export const useConversationStore = create<ConversationState>()(
persist(
(set) => ({
messages: [],
isLoading: false,
addMessage: (message) =>
set((state) => ({ messages: [...state.messages, message] })),
setLoading: (isLoading) => set({ isLoading }),
clearMessages: () => set({ messages: [] }),
}),
{ name: 'conversation-storage' }
)
);
15.6 SQLite with rusqlite
Desktop apps use SQLite for local data persistence. The Rust rusqlite crate provides a synchronous interface to SQLite.
// src-tauri/src/db.rs
use rusqlite::{Connection, Result};
pub fn initialize_db(app_data_dir: &str) -> Result<Connection> {
let db_path = format!("{}/app.db", app_data_dir);
let conn = Connection::open(&db_path)?;
conn.execute_batch("
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
created_at INTEGER NOT NULL,
data TEXT NOT NULL
);
")?;
Ok(conn)
}
Use Tauri’s managed state to share the connection across commands:
// In main.rs setup
use std::sync::Mutex;
struct DbState(Mutex<Connection>);
tauri::Builder::default()
.manage(DbState(Mutex::new(db_connection)))
// ...
15.7 Secure Credential Storage
API keys (Anthropic, etc.) must be stored in the macOS Keychain, not in files or app state.
Use KeychainService from the ITI shared library:
// From ITI shared library (desktop/auth/Keychain.ts)
import { KeychainService } from '../shared/desktop/auth/Keychain';
// Store a key
await KeychainService.store('ANTHROPIC_API_KEY', apiKey);
// Retrieve a key
const apiKey = await KeychainService.get('ANTHROPIC_API_KEY');
The Tauri plugin for Keychain access (tauri-plugin-stronghold or native OS Keychain) is configured in src-tauri/Cargo.toml and tauri.conf.json.
15.8 Building and Running
Development mode (hot reload)
cd my-tauri-app
npm install
npm run tauri dev
Build for production
npm run tauri build
Outputs a .dmg (macOS), .exe (Windows), or .deb/.AppImage (Linux) depending on the host platform.
Build for a specific target
npm run tauri build -- --target aarch64-apple-darwin # Apple Silicon
npm run tauri build -- --target x86_64-apple-darwin # Intel Mac
15.9 Common Tauri Pitfalls
| Pitfall | Prevention |
|---|---|
| Async Rust commands blocking the UI thread | Always mark commands async and use .await |
| SQLite writes from multiple threads | Wrap connection in Mutex |
| Storing API keys in localStorage | Always use Keychain via IPC command |
| Large payloads over IPC | Paginate or use file I/O for large data |
| Missing permissions in tauri.conf.json | Review capability list before building |
Previous: Chapter 14 — WordPress Plugin Development | Next: Chapter 16 — Python Services
