definitive-guide.md: - Add missing AsyncReadExt import in streaming example - Fix v1/v2 WindowUrl table entry (v1 used WindowUrl not WebviewUrl) - Rename 3-Step Pattern to 4-Step Pattern for plugins agent-prompts.md: - Add new Section 15: Prompt for debugging build/compile errors error-resolution-guide.md: - Improve A3 lib naming convention explanation - Clarify G3 multiple definition root cause - Add actionable WebView2 install commands to H1 - Add O9-O11 mobile build errors (Android SDK, macOS, CocoaPods) cheatsheet.md: - Add deep linking subsection with plugin example - Add gen/schemas/ to file hierarchy - Fix tokio::spawn -> tauri::async_runtime::spawn in WS example - Bump tokio-tungstenite 0.21 -> 0.24 ws_server_client_inject_guide.md: - Add WebSocket.OPEN guard to sendToAllWindows() tauri2-links.md: - Expand from 6 to 16 official documentation links
10 KiB
🚀 Tauri 2.0 Definitive Cheat Sheet
Tauri 2.0 features a complete architecture overhaul centered around mobile support (iOS/Android), modularized core plugins, and a strict Access Control List (ACL) security model. This cheat sheet covers core implementation patterns with up-to-date syntax.
🛠️ Project Setup & Structure
Tauri 2.0 splits frontend configurations and introduces modular Rust codebases via lib.rs.
CLI Management
# Initialize a new app
npm create tauri-app@latest
# Run development mode (Hot-reloads UI + Rust)
npm run tauri dev
# Build production artifacts
npm run tauri build
File Hierarchy
my-app/
├── src/ # Frontend UI (React, Vue, Svelte, etc.)
├── src-tauri/ # Backend environment
│ ├── capabilities/ # NEW: Security capability JSON/TOML files
│ │ └── default.json # Maps app windows to permissions
│ ├── gen/
│ │ └── schemas/ # Generated JSON schemas (referenced by $schema in capabilities)
│ ├── src/
│ │ ├── main.rs # Minimal platform entry-point
│ │ └── lib.rs # Core Application setup & commands
│ ├── Cargo.toml # Rust dependencies
│ └── tauri.conf.json # Desktop/Mobile global window configurations
🔒 Security: The Tauri 2.0 Permission System
Tauri 2.0 enforces explicit privileges. Frontend access to system commands or plugins requires configuring a Permission inside a Capability map.
1. Identify Plugin Permissions
Official plugins expose granular identifier strings:
- Core Permissions:
core:path:allow-home,core:event:allow-listen,core:event:allow-emit. - Plugin Permissions:
<plugin-name>:allow-<command>(e.g.,http:allow-request,fs:allow-read).
2. Define Capabilities (src-tauri/capabilities/default.json)
Capabilities attach permissions to specific application windows.
{
"\$schema": "../gen/schemas/capability.json",
"identifier": "main-window-capability",
"description": "Allowed permissions for the primary UI",
"windows": ["main"],
"permissions": [
"core:path:allow-home",
"core:event:allow-listen",
"core:event:allow-emit",
"fs:allow-read",
"websocket:allow-connect",
"websocket:allow-send"
]
}
3. Fine-Grained Scopes
Scopes restrict what a plugin command can touch (e.g., locking file-system access down to a explicit directory). Define these inside your capability entries:
{
"identifier": "secure-fs-scope",
"windows": ["main"],
"permissions": [
{
"identifier": "fs:allow-write",
"allow": [{ "path": "\$APPDATA/logs/*" }]
}
]
}
📡 WebSockets in Tauri 2.0
Tauri 2.0 approaches WebSockets from two distinct perspectives depending on requirements: utilizing the client plugin to query a external endpoint, or instantiating a native high-performance Rust WebSocket Server.
Option A: The Built-in WebSocket Client Plugin
Use this to safely initiate connections to external services from the frontend via a high-performance Rust proxy client.
1. Setup Backend Setup (src-tauri/src/lib.rs)
// Ensure 'tauri-plugin-websocket' is added to Cargo.toml
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_websocket::init()) // Initialize client plugin
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
2. Frontend Execution (JavaScript / TypeScript)
Ensure websocket:allow-connect and websocket:allow-send are added to your active capability configuration.
import WebSocket from '@tauri-apps/plugin-websocket';
// Connect to a server
const ws = await WebSocket.connect('ws://127.0.0.1:8080');
// Handle incoming messages
ws.addListener((msg) => {
console.log('Received payload:', msg.data);
});
// Push data out
await ws.send({ type: 'Text', data: 'Hello World!' });
// Disconnect gracefully
await ws.disconnect();
Option B: Built-in Custom Rust WebSocket Server
If you want your Tauri application to act as a WebSocket Server (e.g., exposing a local port so mobile apps or local interfaces can stream data directly to your backend), spin up a Tokio-backed server thread inside the Tauri setup stage.
1. Dependencies (src-tauri/Cargo.toml)
[dependencies]
tauri = { version = "2.0", features = [] }
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.24" # Industry standard high-performance WS crate
futures-util = "0.3"
2. Server Implementation (src-tauri/src/lib.rs)
use tauri::Manager;
use tokio::net::TcpListener;
use futures-util::{StreamExt, SinkExt};
use tokio_tungstenite::accept_async;
async fn start_ws_server(port: &str) {
let listener = TcpListener::bind(format!("127.0.0.1:{}", port))
.await
.expect("Failed to bind port");
while let Ok((stream, _)) = listener.accept().await {
tauri::async_runtime::spawn(async move {
if let Ok(mut ws_stream) = accept_async(stream).await {
println!("New connection established to Tauri WS Server!");
while let Some(Ok(msg)) = ws_stream.next().await {
if msg.is_text() || msg.is_binary() {
// Eco back received payloads
let _ = ws_stream.send(msg).await;
}
}
}
});
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
// Spin up native server safely using Tauri's async runtime (NOT tokio::spawn directly)
tauri::async_runtime::spawn(start_ws_server("8080"));
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
⚡ Inter-Process Communication (IPC) & State
Commands (Invoking Rust from Frontend)
Pass data directly via JSON-RPC structures across the isolation barrier.
// src-tauri/src/lib.rs
use std::sync::Mutex;
use tauri::State;
pub struct AppState {
pub counter: Mutex<u32>,
}
#[tauri::command]
fn increment_counter(state: State<'_, AppState>, value: u32) -> u32 {
let mut counter = state.counter.lock().unwrap();
*counter += value;
*counter // Return to frontend
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.manage(AppState { counter: Mutex::new(0) }) // Inject State
.invoke_handler(tauri::generate_handler![increment_counter])
.run(tauri::generate_context!())
.expect("failed to run app");
}
// Frontend Invoke Pattern
import { invoke } from '@tauri-apps/api/core';
const result = await invoke<number>('increment_counter', { value: 5 });
console.log(result); // Output: 5
Events (Asynchronous Global Communication)
Ideal for un-prompted data streaming from Rust to UI components.
// Backend Emit
use tauri::{Emitter, AppHandle};
fn stream_system_status(app_handle: &AppHandle, load: f32) {
// Emits a global event to all active window contexts
app_handle.emit("cpu-status", load).unwrap();
}
// Frontend Listen
import { listen } from '@tauri-apps/api/event';
// Make sure 'core:event:allow-listen' is in capabilities
const unlisten = await listen<number>('cpu-status', (event) => {
console.log(`CPU utilization: ${event.payload}%`);
});
// Execute unlisten() later to clean up memory hooks
📇 Deep Links & Windowing
Multiple Window Spawning
// Rust implementation
use tauri::WebviewUrl;
fn create_extra_window(app: &tauri::AppHandle) {
let _window = tauri::WebviewWindowBuilder::new(
app,
"secondary_panel",
WebviewUrl::App("index.html/#/settings".into())
)
.title("Settings Panel")
.inner_size(600.0, 400.0)
.build()
.unwrap();
}
Single Instance Lock
Ensures only one instance runs, focusing the existing window when a second launch is attempted.
// src-tauri/Cargo.toml: tauri-plugin-single-instance = "2"
// src-tauri/capabilities/default.json: add "single-instance:default" to permissions
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_single_instance::init(|app, args, cwd| {
println!("Attempted alternative launch instances: {:?}, working dir: {:?}", args, cwd);
// Focus primary window on secondary call triggers
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_focus();
}
}))
.run(tauri::generate_context!())
.expect("failed execution");
}
Deep Linking with tauri-plugin-deep-link
Registers a custom URL scheme (e.g., my-app://open?token=xyz) so the OS routes external links into your running app.
1. Setup
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-deep-link = "2"
// src-tauri/capabilities/default.json — add permission
{
"permissions": ["deep-link:default"]
}
// src-tauri/tauri.conf.json — register the scheme
{
"app": {
"deepLink": {
"desktop": {
"schemes": ["my-app"]
},
"mobile": {
"scheme": "myapp"
}
}
}
}
2. Rust Handler
// src-tauri/src/lib.rs
use tauri_plugin_deep_link::DeepLinkExt;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_deep_link::init())
.setup(|app| {
// macOS / Linux / Windows desktop handler
#[cfg(desktop)]
app.deep_link().on_open_url(|url| {
println!("Deep link opened: {}", url);
// Parse URL and route to the appropriate handler
});
Ok(())
})
.run(tauri::generate_context!())
.expect("failed execution");
}
3. Frontend Listener (optional)
import { listen } from '@tauri-apps/api/event';
// The deep-link plugin emits 'deep-link://new-url' events
await listen('deep-link://new-url', (event) => {
console.log('Received deep link:', event.payload);
});