Rule Writing

CLAUDE.md for Tauri Apps

Tauri uses Rust for the backend and web tech for the frontend — AI generates Electron patterns that don't apply. Rules for commands, permissions, events, and the Rust core.

7 min read·August 18, 2025

Tauri is not Electron — Rust backend, OS webview, capability-based security

Commands for IPC, permission system, events, Rust state management, and web frontend

Why Tauri Needs Rules That Prevent Electron Patterns

Tauri is Electron's modern alternative — it uses the OS webview instead of bundling Chromium, writes the backend in Rust instead of Node.js, and uses a capability-based permission system instead of giving the renderer full access. AI assistants treat Tauri like Electron: generating Node.js backend code, using Electron's IPC patterns, and ignoring Tauri's permission system. The result is code that doesn't compile.

Tauri's architecture is fundamentally different: the backend is Rust (not JavaScript), IPC uses Tauri commands (not ipcMain/ipcRenderer), and the frontend only has the capabilities you explicitly grant (not full system access by default). These differences make Tauri more secure and more efficient — but AI needs explicit rules to generate the right patterns.

These rules target Tauri v2 with Rust backend. The frontend can be any web framework (React, Vue, Svelte, vanilla) — the Tauri-specific rules apply to the Rust backend and the IPC bridge.

Rule 1: Commands for All Backend Operations

The rule: 'Use Tauri commands for all communication between frontend and Rust backend. Define commands with #[tauri::command]: #[tauri::command] async fn read_file(path: String) -> Result<String, String> { ... }. Register with .invoke_handler(tauri::generate_handler![read_file]). Call from frontend: const content = await invoke("read_file", { path: "/data/config.json" });. Commands are the only IPC mechanism — there is no ipcMain/ipcRenderer.'

For command design: 'Commands are async Rust functions that can access the filesystem, network, database, and any Rust library. Return Result<T, String> for error handling (or a custom error type that implements Serialize). Accept typed parameters — Tauri deserializes from the frontend's JSON automatically. Keep commands focused: one operation per command.'

AI generates Electron's ipcMain.handle pattern in Tauri — it doesn't exist. Tauri commands are Rust functions invoked by name from JavaScript. The pattern is simpler and type-safe: define a Rust function, register it, invoke it from JS.

  • #[tauri::command] on Rust functions — async supported
  • Return Result<T, String> — Tauri serializes to JSON automatically
  • Register: .invoke_handler(tauri::generate_handler![cmd1, cmd2])
  • Call from JS: await invoke('command_name', { param: value })
  • No ipcMain/ipcRenderer — commands are the only IPC mechanism
💡 Commands, Not IPC

Tauri has no ipcMain/ipcRenderer. Define a Rust function with #[tauri::command], register it, invoke from JS. The pattern is simpler than Electron's — one Rust function, one JS call, automatic serialization.

Rule 2: Capability-Based Permissions

The rule: 'Tauri v2 uses capability-based permissions. Define capabilities in src-tauri/capabilities/: which commands, which APIs, and which URLs the frontend can access. The default is deny-all — explicitly grant only what's needed. Use core:default for standard capabilities. Add plugin permissions for each plugin: fs:read-file, shell:open, http:default.'

For the capability file: 'src-tauri/capabilities/default.json: { "identifier": "default", "windows": ["main"], "permissions": ["core:default", "fs:read-file", "fs:write-file"] }. Each permission grants access to specific commands or APIs. Never use wildcard permissions in production — grant the minimum set needed.'

AI generates code that calls Tauri APIs without declaring permissions — the calls silently fail or error at runtime. The permission file is the security boundary: what's not declared is not accessible. This is more secure than Electron's model where the renderer has access to anything the preload script exposes.

⚠️ Deny-All Default

Tauri's capability system denies everything by default. Commands fail silently if permissions aren't declared. Check src-tauri/capabilities/ first when 'invoke' returns undefined — it's almost always a missing permission.

Rule 3: Events for Bidirectional Communication

The rule: 'Use Tauri events for bidirectional communication and broadcasting. Emit from Rust to frontend: window.emit("download-progress", payload)?. Listen in frontend: listen("download-progress", (event) => { ... }). Emit from frontend: emit("user-action", payload). Listen in Rust: window.listen("user-action", |event| { ... }). Use events for push notifications, progress updates, and status changes.'

For when to use commands vs events: 'Commands are request/response: frontend asks, Rust answers (read file, query database, process data). Events are fire-and-forget or push: Rust notifies frontend (download progress, background task complete, system event). Use commands for operations the frontend initiates. Use events for operations the backend initiates.'

For typed events: 'Define event payload types in both Rust and TypeScript. Use serde for Rust serialization, and TypeScript interfaces for the frontend. Keep payloads small — events are serialized to JSON. Use unique event names with namespacing: app:download-progress, app:sync-complete.'

Rule 4: Rust Backend Patterns

The rule: 'The Rust backend (src-tauri/src/) handles: file system access, database operations (SQLite via rusqlite or sqlx), network requests (reqwest), native OS integration, and heavy computation. Use Tauri's managed state for shared data: app.manage(AppState { db: pool }). Access state in commands: async fn handler(state: State<'_, AppState>) -> Result<T, E>.'

For state management: 'Use tauri::State for dependency injection into commands. Create state at setup: Builder::default().setup(|app| { app.manage(Database::new()?); Ok(()) }). Access in commands: #[tauri::command] async fn query(state: State<'_, Database>) -> Result<Vec<Item>, String>. State is thread-safe (Send + Sync required) and accessible from all commands.'

For plugins: 'Use Tauri plugins for cross-cutting concerns: tauri-plugin-store for persistent key-value storage, tauri-plugin-http for network requests, tauri-plugin-fs for scoped file access, tauri-plugin-shell for running system commands. Register in main: .plugin(tauri_plugin_store::init()). Plugins add permissions that must be granted in capabilities.'

  • Rust backend: file system, database, network, native OS, computation
  • tauri::State for DI — manage() at setup, State<T> in command params
  • SQLite via rusqlite/sqlx — reqwest for HTTP — tokio for async
  • Plugins for common needs: store, http, fs, shell, window-state
  • All plugins need permissions declared in capabilities
ℹ️ Rust = Backend

The Rust code in src-tauri/src/ is your backend: file system, database, network, computation. The web frontend in src/ is your UI. They communicate through commands and events. Neither has direct access to the other's world.

Rule 5: Frontend Integration

The rule: 'The frontend is standard web tech — React, Vue, Svelte, or vanilla HTML/CSS/JS in the src/ directory (not src-tauri/). Use @tauri-apps/api for Tauri API access: import { invoke } from "@tauri-apps/api/core". Use the appropriate meta-framework: Vite for React/Vue/Svelte, Next.js with output: "export" for static generation. The frontend builds to static files that Tauri bundles.'

For TypeScript types: 'Type command responses on the frontend: const result = await invoke<string>("read_file", { path }). Define shared types that mirror Rust structs: interface User { id: number; name: string; email: string }. Keep frontend and Rust types in sync — consider using specta or ts-rs for automatic TypeScript type generation from Rust types.'

For dev workflow: 'Use cargo tauri dev for development — it starts both the Rust backend and the frontend dev server with hot reload. Use cargo tauri build for production — it compiles Rust, bundles the frontend, and creates platform-specific installers. Never use the frontend dev server in production — only the bundled static files.'

Complete Tauri Rules Template

Consolidated rules for Tauri v2 desktop applications.

  • #[tauri::command] for all IPC — invoke from JS — Result<T, E> for error handling
  • Capabilities in src-tauri/capabilities/ — deny-all default — grant minimum permissions
  • Events for push communication (Rust → frontend) — commands for request/response
  • tauri::State for DI — manage() at setup — State<T> in command params
  • Plugins for common needs — each plugin needs permissions in capabilities
  • Frontend: standard web tech + @tauri-apps/api — builds to static files
  • specta/ts-rs for Rust → TypeScript type generation — keep types in sync
  • cargo tauri dev for development — cargo tauri build for production installers