# Tauri 2.0 Developer Guide — Part 1: Core Architecture, Config, Commands & IPC > **Version**: 2.0 | **Last Updated**: 2026-05-31 > **Official Docs**: https://v2.tauri.app > > This file covers Sections 1-7 of the Definitive Guide. Part 2 covers Windows, Plugins, WebSocket, State, Migration & Anti-Patterns. --- ## Table of Contents 1. [Introduction & Architecture Overview](#1-introduction--architecture-overview) 2. [Project Structure & Scaffolding](#2-project-structure--scaffolding) 3. [Configuration System (`tauri.conf.json`)](#3-configuration-system-tauriconfjson) 4. [Capabilities & Permissions (Security Model)](#4-capabilities--permissions-security-model) 5. [Rust Backend: `lib.rs` & Commands](#5-rust-backend-librs--commands) 6. [Frontend Integration Patterns](#6-frontend-integration-patterns) 7. [IPC: Commands, Events & Channels](#7-ipc-commands-events--channels) **See also**: [Part 2: Windows, Plugins, WebSocket, State & Migration](./02-windows-plugins-websocket-state-migration.md) --- ## 1. Introduction & Architecture Overview Tauri 2.0 is a framework for building small, fast binaries for all major desktop and mobile platforms. It uses a **hybrid architecture** where a Rust backend handles system-level operations while a web frontend (HTML/CSS/JS, or any framework like React/Vue/Svelte) renders the UI inside a native OS webview. ### Core Architecture Principles - **Rust Core (`src-tauri/`)**: Handles system access, file operations, network calls, window management, and any heavy computation. All system interactions MUST go through Rust to maintain security boundaries. - **Web Frontend (`src/`)**: Renders the UI using standard web technologies. Communicates with Rust exclusively through Tauri's IPC mechanisms (`invoke`, events, channels). - **Security Boundary**: The frontend runs in a sandboxed webview. It cannot directly access the filesystem, network, or OS APIs — it must request these through Tauri commands and plugins, which are governed by the **Capabilities & Permissions** system. - **Mobile Support (NEW in v2)**: Tauri 2.0 adds first-class iOS and Android support. This requires a specific project structure (`lib.rs` + `main.rs` pattern) and platform-specific capability configurations. ### What Changed from v1 to v2 Tauri 2.0 is a **complete architectural overhaul**, not an incremental update. The most significant changes are: | Area | v1 | v2 | |------|----|----| | Security | `allowlist` in config | **Capabilities + Permissions** (ACL-based) | | APIs | Built-in `tauri::api` module | **Everything is a plugin** | | Window type | `Window` / `WindowBuilder` | **`WebviewWindow` / `WebviewWindowBuilder`** | | Config structure | Nested `tauri >` key | Flattened: `app`, `bundle`, `build` | | Event system | Per-window scoped | **`emit()` is global; `emit_to()` for targeting** | | JS imports | `@tauri-apps/api/tauri` | **`@tauri-apps/api/core`** | | Mobile support | Community plugins | **First-class iOS/Android support** | | Project structure | `main.rs` only | **`lib.rs` + `main.rs` pattern required** | --- ## 2. Project Structure & Scaffolding ### Standard Tauri 2.0 File Hierarchy ```text my-app/ ├── src/ # Frontend UI source │ ├── index.html # Main HTML entry │ ├── main.js # Frontend JavaScript │ └── styles.css # Styles ├── src-tauri/ # Rust backend environment │ ├── capabilities/ # Security capability definitions │ │ └── default.json # Maps windows → permissions │ ├── icons/ # App icons for all platforms │ ├── src/ │ │ ├── main.rs # Minimal desktop entry-point │ │ ├── lib.rs # Core application setup & commands │ │ └── .rs # Additional Rust modules │ ├── build.rs # Tauri build script │ ├── Cargo.toml # Rust dependencies │ ├── Cargo.lock # Lockfile (COMMIT THIS) │ └── tauri.conf.json # Main Tauri configuration ├── package.json # Node.js dependencies & scripts └── package-lock.json ``` ### Creating a New Project ```bash # Using the interactive CLI scaffolder npm create tauri-app@latest # Or with specific template npm create tauri-app@latest -- --template vanilla-ts # Cargo alternative cargo create-tauri-app ``` Supported templates: Vanilla, Vue, Svelte, React, Solid, Angular, Preact, Yew, Leptos, Sycamore. ### Adding Tauri to an Existing Frontend ```bash npm install -D @tauri-apps/cli@latest npx tauri init ``` The `init` command will prompt for: app name, window title, web assets location, dev server URL, and frontend build/dev commands. ### The `lib.rs` + `main.rs` Pattern (Required in v2) Tauri 2.0 requires your core application logic to live in `lib.rs`, with a minimal `main.rs` that calls into it. This is **mandatory** for mobile support because mobile platforms require a shared library entry-point rather than a standard `main()` function. **`src-tauri/src/main.rs`** — Desktop-only entry-point: ```rust // Prevents additional console window on Windows in release mode #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { my_app_lib::run() } ``` **`src-tauri/src/lib.rs`** — Core application: ```rust #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() // plugins, commands, setup, etc. .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` **`src-tauri/Cargo.toml`** — Must declare the library: ```toml [lib] name = "my_app_lib" crate-type = ["staticlib", "cdylib", "rlib"] ``` The `_lib` suffix in the crate name prevents naming conflicts with the binary on Windows. The three crate types cover: static linking (mobile), dynamic linking (mobile), and Rust library usage (desktop/tests). ### Development Commands ```bash # Run in development mode (hot-reloads UI + auto-recompiles Rust) npm run tauri dev # or: pnpm tauri dev, bun tauri dev, cargo tauri dev # Build production artifacts npm run tauri build ``` --- ## 3. Configuration System (`tauri.conf.json`) ### Main Configuration File The primary configuration file is `src-tauri/tauri.conf.json`. Tauri 2.0 supports three formats: | Format | Feature Flag Required | |--------|-----------------------| | **JSON** (default) | None | | **JSON5** | `config-json5` on both `tauri` and `tauri-build` | | **TOML** | `config-toml` on both `tauri` and `tauri-build` | ### Complete v2 Configuration Structure ```json { "$schema": "https://tauri.app", "productName": "my-app", "version": "1.0.0", "identifier": "com.mycompany.myapp", "build": { "frontendDist": "../dist", "devUrl": "http://localhost:1420", "beforeDevCommand": "npm run dev", "beforeBuildCommand": "npm run build" }, "app": { "withGlobalTauri": true, "windows": [ { "label": "main", "title": "My App", "width": 800, "height": 600, "resizable": true, "fullscreen": false, "center": true, "dragDropEnabled": true, "useHttpsScheme": false } ], "security": { "csp": "default-src 'self'; style-src 'self' 'unsafe-inline'", "assetProtocol": { "scope": ["$APPDATA/**", "$RESOURCE/**"] } } }, "bundle": { "active": true, "targets": "all", "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" ], "licenseFile": "LICENSE", "copyright": "", "category": "Utility" } } ``` ### Key Configuration Changes from v1 | v1 Path | v2 Path | Notes | |---------|---------|-------| | `package.productName` | Top-level `productName` | Moved out of `package` | | `package.version` | Top-level `version` | Moved out of `package` | | `package` | *(removed)* | Fields redistributed | | `build.distDir` | `build.frontendDist` | Renamed | | `build.devPath` | `build.devUrl` | Renamed | | `build.withGlobalTauri` | `app.withGlobalTauri` | Moved to `app` section | | `tauri.*` | `app.*` | Top-level key renamed | | `tauri.allowlist` | *(removed)* | Replaced by capabilities | | `tauri.windows.fileDropEnabled` | `app.windows.dragDropEnabled` | Renamed | | `tauri.bundle` | Top-level `bundle` | Promoted to top-level | | `tauri.updater` | `plugins.updater` | Moved to plugins | | `tauri.systemTray` | `app.trayIcon` | Renamed | | `tauri.cli` | `plugins.cli` | Moved to plugins | | `bundle.identifier` | Top-level `identifier` | Promoted to top-level | ### Platform-Specific Configuration Create platform override files that **merge** with the main config using JSON Merge Patch (RFC 7396). Arrays are replaced entirely (not element-by-element merged). | Platform | File Pattern | |----------|-------------| | Linux | `tauri.linux.conf.json` or `Tauri.linux.toml` | | Windows | `tauri.windows.conf.json` or `Tauri.windows.toml` | | macOS | `tauri.macos.conf.json` or `Tauri.macos.toml` | | Android | `tauri.android.conf.json` or `Tauri.android.toml` | | iOS | `tauri.ios.conf.json` or `Tauri.ios.toml` | ### Cargo.toml Dependencies ```toml [build-dependencies] tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" ``` **Important**: Keep `tauri`, `tauri-build`, and `@tauri-apps/cli` on the same minor version. Always commit `Cargo.lock` for reproducible builds. ### package.json Scripts ```json { "scripts": { "tauri": "tauri", "dev": "tauri dev", "build": "tauri build" }, "devDependencies": { "@tauri-apps/cli": "^2" } } ``` The `"tauri"` script is only required when using `npm` (not needed for yarn/pnpm/bun). --- ## 4. Capabilities & Permissions (Security Model) Tauri 2.0 introduces a strict **Access Control List (ACL)** security model that replaces the v1 `allowlist`. This is the most important new concept to understand. ### Core Concepts - **Capabilities** define **which permissions** are granted to **which windows or webviews**. - **Permissions** describe explicit privileges for specific commands (e.g., `fs:allow-read`, `http:allow-request`). - **Scopes** further restrict what a permission can access (e.g., only certain file paths). - Multiple capabilities can apply to the same window — permissions **merge**. - Security boundaries are based on **window labels** (not titles). ### Capability File Location ``` src-tauri/capabilities/.json (or .toml) ``` All files in this directory are **automatically loaded** by default. ### Basic Capability File ```json { "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main-window-capability", "description": "Permissions for the primary application window", "platforms": ["linux", "macOS", "windows"], "windows": ["main"], "permissions": [ "core:default" ] } ``` **Field reference:** - `$schema` — Points to generated schema for IDE autocompletion. Use `desktop-schema.json` for desktop, `mobile-schema.json` for mobile. - `identifier` — Unique capability name (ASCII lowercase, max 116 chars). - `description` — Human-readable purpose. - `windows` — Array of window labels this capability applies to. Use `["*"]` for all windows. - `permissions` — Array of permission identifiers. - `platforms` — Optional. Restricts to specific platforms. Defaults to all platforms if omitted. ### Permission Identifier Naming Convention | Pattern | Meaning | Example | |---------|---------|---------| | `:default` | Default permission set for a plugin | `fs:default` | | `:allow-` | Allow a specific command | `fs:allow-read` | | `:deny-` | Deny a specific command | `fs:deny-write` | | `core::` | Core Tauri module permission | `core:event:allow-emit` | ### Common Permission Identifiers **Core permissions:** - `core:default` — Basic runtime permissions - `core:path:default` — Path resolution - `core:event:default` — Event system (includes listen/emit) - `core:event:allow-listen` — Allow listening to events - `core:event:allow-emit` — Allow emitting events - `core:window:default` — Window management - `core:window:allow-set-title` — Allow changing window title **Plugin permissions:** - `fs:allow-read`, `fs:allow-write`, `fs:allow-exists`, `fs:allow-mkdir`, `fs:allow-remove` - `http:allow-request` — HTTP client requests - `dialog:allow-open`, `dialog:allow-save`, `dialog:allow-message` - `shell:allow-open`, `shell:allow-execute` - `notification:allow-notify`, `notification:allow-is-permission-granted` - `clipboard-manager:allow-read-text`, `clipboard-manager:allow-write-text` - `websocket:allow-connect`, `websocket:allow-send` - `store:default` — Persistent key-value store ### Fine-Grained Scopes Scopes restrict what a permission can actually access: ```json { "identifier": "secure-fs-capability", "windows": ["main"], "permissions": [ { "identifier": "fs:allow-read", "allow": [{ "path": "$APPDATA/logs/*" }, { "path": "$HOME/Documents/*.txt" }] }, { "identifier": "fs:allow-write", "allow": [{ "path": "$APPDATA/logs/*.log" }] }, { "identifier": "fs:deny-write", "deny": [{ "path": "$APPDATA/config/*" }] } ] } ``` Available scope variables: `$APPDATA`, `$HOME`, `$APPCONFIG`, `$APPCACHE`, `$APPLOG`, `$RESOURCE`, `$EXE`. ### Restricting Custom Commands By default, all `#[tauri::command]` functions are accessible from all windows. To restrict a custom command to specific windows only, use `build.rs`: ```rust fn main() { tauri_build::try_build( tauri_build::Attributes::new() .app_manifest( tauri_build::AppManifest::new() .commands(&["my_restricted_command", "admin_only_command"]) ), ) .unwrap(); } ``` Then grant access in the capability: ```json { "identifier": "admin-capability", "windows": ["admin-panel"], "permissions": [ { "identifier": "allow-my-restricted-command" }, { "identifier": "allow-admin-only-command" } ] } ``` ### Referencing Capabilities Explicitly By default, all capability files in `src-tauri/capabilities/` are auto-discovered. To control which ones are used explicitly: ```json { "app": { "security": { "capabilities": ["my-capability", "admin-capability"] } } } ``` ### Inline Capabilities You can define capabilities directly in `tauri.conf.json` instead of separate files: ```json { "app": { "security": { "capabilities": [ { "identifier": "inline-cap", "windows": ["*"], "permissions": ["core:default"] } ] } } } ``` --- ## 5. Rust Backend: `lib.rs` & Commands ### Application Builder Pattern All Tauri 2.0 applications follow the `tauri::Builder` pattern: ```rust use tauri::Manager; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) // Register plugins .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) .setup(|app| { // Setup hook (runs once at startup) // Initialize resources, spawn servers, etc. Ok(()) }) .manage(AppState { /* ... */ }) // Inject managed state .invoke_handler(tauri::generate_handler![ // Register commands my_command, another_command, module::command_in_module ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` **Critical rules:** - `invoke_handler` can only be called **once**. All commands must be in a single `generate_handler![]` call. - Commands defined in `lib.rs` **cannot** be marked `pub`. - Commands defined in separate modules **must** be marked `pub`. - The `#[cfg_attr(mobile, tauri::mobile_entry_point)]` attribute is required for the `run()` function. ### Defining Commands Commands are the primary IPC mechanism. They are Rust functions annotated with `#[tauri::command]` and callable from the frontend via `invoke()`. #### Basic Command ```rust #[tauri::command] fn greet(name: String) -> String { format!("Hello, {}! Welcome from Rust.", name) } ``` #### Command with Error Handling ```rust #[tauri::command] fn login(user: String, password: String) -> Result { if user == "admin" && password == "secret" { Ok("authenticated".to_string()) } else { Err("invalid credentials".to_string()) } } ``` #### Async Command ```rust #[tauri::command] async fn fetch_data(url: String) -> Result { let response = reqwest::get(&url) .await .map_err(|e| e.to_string())?; response.text().await.map_err(|e| e.to_string()) } ``` **IMPORTANT limitation**: Async commands cannot accept borrowed references (`&str`, `&Path`) directly. Convert to owned types (`String`, `PathBuf`) or wrap the return in `Result`: ```rust // Option A: Use owned types #[tauri::command] async fn process(value: String) -> String { /* ... */ } // Option B: Wrap return in Result #[tauri::command] async fn process(value: &str) -> Result { /* ... */ } ``` #### Custom Error Types (Recommended) ```rust use thiserror::Error; #[derive(Debug, Error)] enum AppError { #[error("IO error: {0}")] Io(#[from] std::io::Error), #[error("Network error: {0}")] Network(#[from] reqwest::Error), } // Implement Serialize so the error can cross the IPC boundary impl serde::Serialize for AppError { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { serializer.serialize_str(self.to_string().as_ref()) } } #[tauri::command] fn read_config() -> Result { let content = std::fs::read_to_string("config.toml")?; Ok(content) } ``` #### Structured Error with Kind/Tag ```rust #[derive(Debug, Error)] enum AppError { #[error(transparent)] Io(#[from] std::io::Error), #[error("{0}")] Custom(String), } #[derive(serde::Serialize)] #[serde(tag = "kind", content = "message")] #[serde(rename_all = "camelCase")] enum ErrorKind { Io(String), Custom(String), } impl serde::Serialize for AppError { fn serialize(&self, serializer: S) -> Result where S: serde::ser::Serializer, { let kind = match self { Self::Io(e) => ErrorKind::Io(e.to_string()), Self::Custom(msg) => ErrorKind::Custom(msg.clone()), }; kind.serialize(serializer) } } ``` Frontend receives: `{ kind: 'io', message: '...' }` — making it easy to handle errors by type. #### Accessing Special Objects in Commands **AppHandle** — Access to the application instance: ```rust #[tauri::command] fn do_something(app_handle: tauri::AppHandle) { let app_dir = app_handle.path().app_dir(); // Spawn windows, emit events, access state... } ``` **WebviewWindow** — The window that invoked the command: ```rust #[tauri::command] fn get_window_info(webview_window: tauri::WebviewWindow) -> String { webview_window.label().to_string() } ``` **Managed State** — Shared application state: ```rust use tauri::State; use std::sync::Mutex; struct DbConnection(Mutex); #[tauri::command] fn query_database(state: State<'_, DbConnection>) -> String { let conn = state.0.lock().unwrap(); conn.clone() } ``` **Raw Request** — Access headers and raw body: ```rust #[tauri::command] fn upload(request: tauri::ipc::Request) -> Result<(), String> { let tauri::ipc::InvokeBody::Raw(data) = request.body() else { return Err("Expected raw body".to_string()); }; let auth = request.headers().get("Authorization") .ok_or("Missing auth header")?; Ok(()) } ``` #### Commands in Separate Modules ```rust // src-tauri/src/commands.rs #[tauri::command] pub fn create_user(name: String) -> String { format!("Created user: {}", name) } #[tauri::command] pub fn delete_user(id: u32) -> bool { // ... true } ``` ```rust // src-tauri/src/lib.rs mod commands; pub fn run() { tauri::Builder::default() .invoke_handler(tauri::generate_handler![ commands::create_user, commands::delete_user ]) // ... } ``` Note: The `commands::` prefix is Rust path resolution only — the frontend still calls `invoke('create_user', ...)` without any prefix. ### Returning Large Data (ArrayBuffers) For large binary data like file contents, use `tauri::ipc::Response`: ```rust use tauri::ipc::Response; #[tauri::command] fn read_file() -> Response { let data = std::fs::read("/path/to/file").unwrap(); Response::new(data) } ``` ### Streaming Data (Channels) For streaming large amounts of data to the frontend: ```rust use tauri::ipc::Channel; use tokio::io::AsyncReadExt; #[tauri::command] async fn stream_file(path: std::path::PathBuf, on_chunk: Channel>) { let mut file = tokio::fs::File::open(path).await.unwrap(); let mut buf = vec![0u8; 4096]; loop { let n = file.read(&mut buf).await.unwrap(); if n == 0 { break; } on_chunk.send(&buf[..n]).unwrap(); } } ``` --- ## 6. Frontend Integration Patterns ### Using `@tauri-apps/api` npm Package (Recommended) ```bash npm install @tauri-apps/api ``` ```javascript // Imports in v2 — NOTE the path changes from v1 import { invoke } from '@tauri-apps/api/core'; import { listen, emit, emitTo, once } from '@tauri-apps/api/event'; import { getCurrentWebviewWindow, WebviewWindow } from '@tauri-apps/api/webviewWindow'; import { Channel } from '@tauri-apps/api/core'; ``` ### Using Global `window.__TAURI__` (No npm package needed) Requires `app.withGlobalTauri: true` in `tauri.conf.json`: ```javascript // No import needed — accessed via the global object const { invoke } = window.__TAURI__.core; const { listen, emit } = window.__TAURI__.event; ``` This pattern is ideal for vanilla JS projects that don't use a bundler, avoiding the need for any npm packages beyond `@tauri-apps/cli`. ### Import Path Changes from v1 | v1 Import | v2 Import | |-----------|-----------| | `@tauri-apps/api/tauri` | `@tauri-apps/api/core` | | `@tauri-apps/api/window` | `@tauri-apps/api/webviewWindow` | | `@tauri-apps/api/cli` | `@tauri-apps/plugin-cli` | | `@tauri-apps/api/clipboard` | `@tauri-apps/plugin-clipboard-manager` | | `@tauri-apps/api/dialog` | `@tauri-apps/plugin-dialog` | | `@tauri-apps/api/fs` | `@tauri-apps/plugin-fs` | | `@tauri-apps/api/global-shortcut` | `@tauri-apps/plugin-global-shortcut` | | `@tauri-apps/api/http` | `@tauri-apps/plugin-http` | | `@tauri-apps/api/notification` | `@tauri-apps/plugin-notification` | | `@tauri-apps/api/shell` | `@tauri-apps/plugin-shell` | | `@tauri-apps/api/updater` | `@tauri-apps/plugin-updater` | | `@tauri-apps/api/os` | `@tauri-apps/plugin-os` | | `@tauri-apps/api/process` | `@tauri-apps/plugin-process` | **Rule of thumb in v2**: If it's not `core`, `event`, or `webviewWindow`, it's a plugin. ### Plugin JS Package Naming All v2 plugin JS packages follow: `@tauri-apps/plugin-` ```bash npm install @tauri-apps/plugin-store npm install @tauri-apps/plugin-fs npm install @tauri-apps/plugin-http ``` --- ## 7. IPC: Commands, Events & Channels ### Commands (Frontend → Rust) The primary request-response mechanism: ```javascript // Frontend (v2 import) import { invoke } from '@tauri-apps/api/core'; // Simple invocation const result = await invoke('greet', { name: 'World' }); console.log(result); // "Hello, World! Welcome from Rust." // With error handling try { const token = await invoke('login', { user: 'admin', password: 'secret' }); console.log('Authenticated:', token); } catch (error) { console.error('Login failed:', error); } ``` **Argument naming convention**: Arguments are passed as a JSON object with **camelCase** keys by default. Use `#[tauri::command(rename_all = "snake_case")]` to accept snake_case from the frontend. ### Events (Bidirectional, Multi-Consumer) Events are fire-and-forget messages. Use them for notifications, streaming status updates, or any data that multiple components might need. #### Emitting Events from Rust ```rust use tauri::{AppHandle, Emitter}; // Global event (all listeners receive it) app_handle.emit("download-progress", 42)?; // Target a specific window app_handle.emit_to("settings-panel", "config-changed", new_config)?; // Filter to specific windows use tauri::EventTarget; app_handle.emit_filter("notification", payload, |target| { matches!(target, EventTarget::WebviewWindow { label } if label == "main") })?; ``` #### Listening for Events on the Frontend ```javascript import { listen, once, emit, emitTo } from '@tauri-apps/api/event'; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; // Global listen (receives ALL events with this name) const unlisten = await listen('download-progress', (event) => { console.log(`Progress: ${event.payload}%`); }); // Targeted listen (only events emitted to THIS window) const appWebview = getCurrentWebviewWindow(); const unlisten2 = await appWebview.listen('config-changed', (event) => { console.log('New config:', event.payload); }); // Listen once, then auto-cleanup await once('initialization-complete', (event) => { console.log('App ready!', event.payload); }); // Emit from frontend to Rust await emit('user-action', { type: 'click', target: 'button' }); // Emit to a specific window from frontend const target = new WebviewWindow('settings-panel'); await target.emit('settings-request', { key: 'theme' }); ``` **Always call `unlisten()`** when the component unmounts or the listener is no longer needed to prevent memory leaks. #### Listening for Events on the Rust Side ```rust use tauri::{Listener, Manager}; pub fn run() { tauri::Builder::default() .setup(|app| { // Listen globally app.listen("user-action", |event| { println!("Received: {}", event.payload()); }); // Listen on a specific window let main = app.get_webview_window("main").unwrap(); main.listen("config-changed", |event| { println!("Config updated: {}", event.data); }); // Listen once app.once("ready", |event| { println!("App is ready!"); }); Ok(()) }) // ... } ``` ### Channels (High-Throughput Streaming) Channels are the recommended mechanism for streaming large amounts of ordered data from Rust to the frontend. They are faster and more memory-efficient than events for high-throughput scenarios. ```rust use tauri::ipc::Channel; use serde::Serialize; #[derive(Clone, Serialize)] #[serde(tag = "event", content = "data")] enum StreamEvent { Started { total: usize }, Progress { current: usize, chunk: Vec }, Finished { total: usize }, } #[tauri::command] fn download_file(url: String, on_event: Channel) { on_event.send(StreamEvent::Started { total: 1000 }).unwrap(); // ... streaming logic ... on_event.send(StreamEvent::Finished { total: 1000 }).unwrap(); } ``` ```javascript import { invoke, Channel } from '@tauri-apps/api/core'; const onEvent = new Channel(); onEvent.onmessage = (event) => { switch (event.event) { case 'started': console.log(`Starting: ${event.data.total} bytes`); break; case 'progress': /* handle chunk */ break; case 'finished': console.log('Done!'); break; } }; await invoke('download_file', { url: 'https://...', onEvent }); ``` ### Evaluating JavaScript from Rust ```rust use tauri::Manager; // In a command or setup hook let webview = app.get_webview_window("main").unwrap(); webview.eval("document.getElementById('status').textContent = 'Loaded!'")?; ``` For complex data passing, use the `serialize-to-javascript` crate. --- **Continue to**: [Part 2: Windows, Plugins, WebSocket, State & Migration](./02-windows-plugins-websocket-state-migration.md)