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
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.
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
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