Skip to main content
< All Topics
Print

Chapter 15: Desktop Apps with Tauri 2

Chapter 15: Desktop Apps with Tauri 2

Last Updated: 2026-03

## 15.1 Overview

Tauri 2 is the framework for ITI desktop applications. It provides a native application shell (Rust backend) with a web-based UI (React/TypeScript frontend). Tauri apps compile to small, fast, native binaries for macOS, Windows, and Linux from a single codebase.

ITI Tauri apps include:

personal-assistant (Personal/personal-assistant/) — Tauri 2 desktop AI assistant

estate-manager (ITI/products/estate-manager/desktop/) — Estate planning desktop app

Why Tauri over Electron? Tauri uses the OS’s native webview (WebKit on macOS) instead of bundling Chromium, resulting in ~20x smaller binaries and significantly lower memory usage.

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

Table of Contents