| name | tauri-core-architecture |
| description | Use when creating new Tauri 2 apps, understanding project structure, or reasoning about the component model. Prevents mixing Tauri 1.x architecture assumptions with the v2 multi-webview and capability-based model. Covers Rust backend structure, webview layer, IPC bridge model, process model, project layout, and type hierarchy. Keywords: tauri architecture, project structure, IPC bridge, webview layer, process model, Rust backend, how Tauri works, project layout, frontend backend split, getting started, what is IPC..
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Tauri 2.x with Rust and TypeScript. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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() with Arc-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 + Clone payloads
- 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:
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
#![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())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![cmd1, cmd2])
.setup(|app| {
let handle = app.handle().clone();
Ok(())
})
.on_window_event(|window, event| { })
.on_menu_event(|app, event| { })
.menu(|app| { })
.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
Official Sources