tauri-core-architecture
tauri-core-architecture
Quick Reference
Architecture Layers (Tauri 2.x)
| Layer | Technology | Location | Role |
|---|---|---|---|
| Rust Backend | Rust + Tokio | src-tauri/src/ |
Application logic, system access, state management |
| Webview Layer | Platform-native webview | Embedded | Renders HTML/CSS/JS frontend |
| IPC Bridge | JSON-serialized messages | Internal | Connects frontend to backend via invoke() and events |
| Plugin System | Rust crate + npm package | Cargo.toml + package.json |
Extends capabilities (fs, dialogs, HTTP, etc.) |
| Permission System | Capabilities + permissions | src-tauri/capabilities/ |
Fine-grained access control (replaces v1 allowlist) |
Platform Webview Engines
| Platform | Webview Engine |
|---|---|
| Windows | WebView2 (Chromium-based) |
| macOS | WKWebView |
| Linux | WebKitGTK |
| iOS | WKWebView |
| Android | Android WebView |
Key Dependencies
| Package | Type | Purpose |
|---|---|---|
@tauri-apps/cli |
devDependency (npm) | CLI tooling for dev/build |
@tauri-apps/api |
dependency (npm) | Frontend API for invoke, events, paths |
tauri |
dependency (Cargo) | Core Rust framework |
tauri-build |
build-dependency (Cargo) | Build script helpers |
serde + serde_json |
dependency (Cargo) | Serialization for IPC |
Critical Warnings
NEVER block the main thread with synchronous I/O in commands -- ALWAYS use async for file, network, or long-running operations. Blocking freezes the entire UI.
NEVER call .invoke_handler() more than once on Builder -- only the LAST call takes effect. Put ALL commands in a single generate_handler![] macro.
NEVER mark command functions as pub when defined directly in lib.rs -- the glue code generation prevents it. Move commands to a separate module if they need to be public.
NEVER wrap managed state in Arc -- Tauri wraps state in Arc internally. Using app.manage(Arc::new(data)) adds a redundant layer.
NEVER use &str in async command parameters -- borrowed references do not work with async command spawning. ALWAYS use String instead.
NEVER use State<'_, T> when you registered Mutex<T> -- this causes a runtime panic, not a compile error. ALWAYS match the exact registered type: State<'_, Mutex<T>>.
Process Model
Tauri 2 uses a two-process architecture:
Main Process (Rust)
- Runs the Rust backend on the main thread + Tokio async runtime
- Has full system access (filesystem, network, OS APIs)
- Manages application lifecycle, windows, menus, tray icons
- Exposes
#[tauri::command]functions as IPC endpoints - Manages state via
app.manage()withArc-wrapped storage
Webview Process (Frontend)
- Runs in a platform-native webview (NOT Electron/Chromium bundle)
- Renders HTML/CSS/JS like a browser tab
- Communicates with Rust ONLY through the IPC bridge
- Has NO direct system access -- all system operations go through
invoke() - Event listeners enable bidirectional pub/sub messaging
IPC Bridge
The bridge between processes uses JSON-serialized messages:
Frontend Rust Backend
| |
|--- invoke('cmd', {args}) -------->| (JSON request)
| |--- execute command
|<-- Result<T, E> (JSON) ----------| (JSON response)
| |
|--- emit('event', payload) ------->| (event pub/sub)
|<-- emit('event', payload) --------| (event pub/sub)
| |
|<-- Channel.send(data) -----------| (streaming)
invoke()sends a JSON object with camelCase keys; Rust receives snake_case parameters- Events use
Serialize + Clonepayloads - Channels enable streaming multiple messages from Rust to JS
- Binary data can bypass JSON via
tauri::ipc::Response
Project Structure
my-tauri-app/
├── src/ # Frontend source (React/Vue/Svelte/vanilla)
│ ├── index.html
│ ├── main.ts
│ └── styles.css
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── lib.rs # Main app logic (mobile entry point)
│ │ └── main.rs # Desktop entry point (calls lib.rs)
│ ├── capabilities/ # Permission capability files (JSON/TOML)
│ │ └── default.json # Default capabilities
│ ├── permissions/ # Custom command permissions (TOML only)
│ ├── icons/ # Application icons (all required sizes)
│ ├── gen/ # Generated files (DO NOT edit manually)
│ │ └── schemas/
│ │ ├── desktop-schema.json
│ │ ├── mobile-schema.json
│ │ └── remote-schema.json
│ ├── Cargo.toml # Rust dependencies
│ ├── Cargo.lock # Deterministic build lock
│ ├── tauri.conf.json # Main configuration
│ ├── build.rs # Cargo build script
│ └── .taurignore # Exclude files from dev watcher
├── package.json # Frontend dependencies
├── tsconfig.json # TypeScript config (if applicable)
└── vite.config.ts # Bundler config (if Vite)
Source Control Rules
- ALWAYS commit:
src-tauri/Cargo.lock(deterministic builds) - ALWAYS ignore:
src-tauri/target/(build artifacts) - NEVER edit:
src-tauri/gen/(auto-generated schemas)
Entry Point Pattern (Tauri 2.x)
Desktop and mobile share logic through lib.rs:
// src-tauri/src/lib.rs
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![/* commands */])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
// src-tauri/src/main.rs (desktop only)
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
}
ALWAYS use the lib.rs + main.rs split pattern for Tauri 2 -- this is required for mobile support and is the standard project layout.
Type Hierarchy
Core Types
| Type | Description | Traits |
|---|---|---|
App |
Full application instance (only in setup()) |
Manager, Emitter, Listener |
AppHandle |
Lightweight clone-safe handle to the app | Manager, Emitter, Listener, Clone + Send + Sync |
WebviewWindow |
Combined window + webview (most common) | Manager, Emitter, Listener |
Window |
OS-level window (without webview) | Manager, Emitter, Listener |
Webview |
Webview inside a window | Manager, Emitter, Listener |
State<'_, T> |
Managed state accessor in commands | -- |
Builder |
Application configuration builder | -- |
The Manager Trait
The Manager trait is the unifying interface. It is implemented by App, AppHandle, Webview, WebviewWindow, and Window. Key methods:
| Method | Return Type | Purpose |
|---|---|---|
app_handle() |
&AppHandle<R> |
Get the AppHandle |
config() |
&Config |
Access tauri.conf.json |
state::<T>() |
State<'_, T> |
Access managed state |
try_state::<T>() |
Option<State<'_, T>> |
Access state without panic |
manage(state) |
bool |
Register new state |
get_webview_window(label) |
Option<WebviewWindow<R>> |
Get window by label |
webview_windows() |
HashMap<String, WebviewWindow<R>> |
Get all windows |
get_window(label) |
Option<Window<R>> |
Get OS window by label |
path() |
&PathResolver<R> |
Access path resolver |
package_info() |
&PackageInfo |
Get package metadata |
The Emitter Trait
| Method | Purpose |
|---|---|
emit(event, payload) |
Broadcast to ALL targets |
emit_to(target, event, payload) |
Emit to a specific target |
emit_filter(event, payload, filter_fn) |
Emit to targets matching a filter |
emit_str(event, json_string) |
Emit with pre-serialized JSON |
The Listener Trait
| Method | Purpose |
|---|---|
listen(event, handler) |
Listen for events (returns EventId) |
once(event, handler) |
Listen once, auto-remove after first event |
unlisten(id) |
Remove a listener |
listen_any(event, handler) |
Listen from any source |
Runtime Generic
Most types are generic over R: Runtime. With the default wry feature, R resolves to Wry. Use the generic form in plugins or for test mocking:
#[tauri::command]
async fn my_command<R: Runtime>(
app: AppHandle<R>,
window: WebviewWindow<R>,
) -> Result<(), String> {
Ok(())
}
Builder Pattern
The Builder is the single entry point for configuring a Tauri application:
tauri::Builder::default()
.manage(MyState::default()) // state
.plugin(tauri_plugin_shell::init()) // plugins
.invoke_handler(tauri::generate_handler![cmd1, cmd2]) // commands
.setup(|app| { // init hook
let handle = app.handle().clone();
// app is &mut App -- full access, runs once
Ok(())
})
.on_window_event(|window, event| { /* ... */ }) // window events
.on_menu_event(|app, event| { /* ... */ }) // menu events
.menu(|app| { /* build menu */ }) // app menu
.run(tauri::generate_context!())
.expect("error running app");
Builder Method Reference
| Method | Purpose |
|---|---|
manage(T) |
Register managed state |
plugin(P) |
Register a plugin |
invoke_handler(F) |
Register ALL command handlers |
setup(F) |
App initialization hook |
menu(F) |
Set the application menu |
on_menu_event(F) |
Menu event handler |
on_window_event(F) |
Window event handler |
on_webview_event(F) |
Webview event handler |
on_page_load(F) |
Page load handler |
on_tray_icon_event(F) |
Tray icon event handler |
build(Context) |
Build without running (returns App) |
run(Context) |
Build and run the app |
any_thread(self) |
Allow running on any thread |
Frontend API Overview
| API Area | Key Functions | Import Path |
|---|---|---|
| Commands | invoke<T>(), Channel<T> |
@tauri-apps/api/core |
| Events | listen(), emit(), emitTo(), once() |
@tauri-apps/api/event |
| Windows | getCurrentWindow(), getAllWindows() |
@tauri-apps/api/window |
| Webviews | getCurrentWebview() |
@tauri-apps/api/webview |
| Paths | appDataDir(), join(), BaseDirectory |
@tauri-apps/api/path |
| Utilities | convertFileSrc(), isTauri() |
@tauri-apps/api/core |
| Testing | mockIPC(), mockWindows(), clearMocks() |
@tauri-apps/api/mocks |
Reference Links
- references/methods.md -- API signatures for App, AppHandle, Manager, Runtime, Webview, WebviewWindow
- references/examples.md -- Working code examples verified against Tauri 2.x documentation
- references/anti-patterns.md -- What NOT to do, with WHY explanations