8.5 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
│ ├── 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.21" # 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 {
tokio::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 in an isolated async tokio environment
tokio::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 & Deep Linking
Ensures only one instance runs, piping external application schemas (e.g. my-app://open?token=xyz) into the active runtime.
// 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");
}