← Catalog

tauri-architecture

Use when reasoning about Tauri v2 internals — process boundaries (Core vs WebView), the IPC channel, binary-size tradeoffs, or deciding between commands/events/raw IPC for a given feature.

Tauri v2 Architecture

Tauri is a polyglot toolkit: a Rust-compiled binary (Core) drives one or more OS-provided WebViews (WKWebView on macOS, WebView2 on Windows, webkitgtk on Linux). WebView runtimes are dynamically linked at runtime, not bundled — that’s why Tauri apps ship small but you must mind platform webview differences.

The two processes

Tauri uses multi-process architecture (like browsers/Electron) so a crashing/compromised WebView can’t take down the app or the OS.

            +------------------------+
            |  Core process (Rust)   |
            |  - one per app         |
            |  - OS access           |
            |  - global state        |
            |  - IPC router          |
            +-----------+------------+
                        |
        +---------------+---------------+
        |               |               |
   +----v----+     +----v----+     +----v----+
   | WebView |     | WebView |     | WebView |
   | (window |     | (window |     | (tray   |
   |   1)    |     |   2)    |     | popup)  |
   +---------+     +---------+     +---------+
   HTML/CSS/JS    HTML/CSS/JS     HTML/CSS/JS
   no OS access  no OS access    no OS access
  • Core (Rust, single): app entry point, the only component with full OS access. Owns windows, tray, notifications, updater, and global state (settings, DB pools). All IPC routes through Core, so you have one chokepoint to filter/intercept/audit messages.
  • WebView (one per window): renders UI. Treat it as untrusted-ish — sanitize input, never hold secrets here, push business logic to Core. Principle of least privilege.

IPC mechanisms

Tauri uses asynchronous message passing (JSON-RPC-ish under the hood). Two primitives:

invoke() — Commands (request/response)

Frontend calls a Rust function, awaits a serializable return. Use for anything that needs a result, error handling, or capability-gated OS access.

import { invoke } from '@tauri-apps/api/core';
const usage = await invoke<UsageStats>('get_usage', { since: '2025-01-01' });
#[tauri::command]
fn get_usage(since: String) -> Result<UsageStats, String> { /* ... */ }

All args/returns must be JSON-serializable. Commands are gated by the capability/ACL system.

emit / listen — Events (fire-and-forget, bidirectional)

Either side can broadcast; no response. Use for lifecycle/state-change notifications (e.g. “auth-token-refreshed”, “sidecar-ready”, progress streams).

import { emit, listen } from '@tauri-apps/api/event';
const unlisten = await listen<Token>('token-refreshed', (e) => update(e.payload));
await emit('user-action', { kind: 'logout' });
app.emit("token-refreshed", &token)?;

Raw IPC

window.__TAURI_INTERNALS__.postMessage is the low-level transport. Don’t touch it directly except inside the Isolation pattern’s __TAURI_ISOLATION_HOOK__.

Isolation pattern

A sandboxed <iframe> containing a small “isolation app” that intercepts every IPC message before it reaches Core. Tauri encrypts the validated message with a runtime-generated AES-GCM key before forwarding.

Threat model: Development Threats — supply-chain compromise of frontend deps (deeply-nested npm graphs). Even if a malicious dep injects JS to call invoke('write_file', { path: '/etc/passwd' }), the isolation hook can reject it.

Enable in tauri.conf.json:

{
  "app": {
    "security": {
      "pattern": {
        "use": "isolation",
        "options": { "dir": "../dist-isolation" }
      }
    }
  }
}

Hook signature (runs sandboxed):

window.__TAURI_ISOLATION_HOOK__ = (payload) => {
  // validate / reject / mutate; return the (modified) payload
  if (payload.cmd === 'write_file' && !payload.path.startsWith(APP_DIR)) {
    throw new Error('path outside app dir');
  }
  return payload;
};

Keep the isolation app dependency-free — its whole point is to be a trusted minimal layer. Limitation: ES Modules don’t load inside the sandboxed iframe on Windows; rely on inlined <script src> tags (Tauri inlines them at build time).

Brownfield pattern

The default pattern — no config needed. Tauri behaves like a regular browser, so an existing webapp drops in with minimal change. Choose this when integrating Tauri into a mature web frontend. No isolation guarantees; trust your dep tree. Explicit form:

{ "app": { "security": { "pattern": { "use": "brownfield" } } } }

Size optimization

Tauri binaries are already small (no bundled runtime), but the Cargo release profile is the biggest lever:

# src-tauri/Cargo.toml
[profile.dev]
incremental = true

[profile.release]
codegen-units = 1   # better LLVM optimization (slower compile)
lto = true          # link-time optimization
opt-level = "s"     # optimize for size; "z" is smaller still, "3" for speed
panic = "abort"     # drop unwinding tables
strip = true        # drop debug symbols

Additional levers:

  • build.removeUnusedCommands: true in tauri.conf.json (Tauri 2.4+) — strips commands not listed in any capability/ACL. Pair with explicit ACLs (no defaults) for max benefit. Note: dynamic-ACL apps must validate this works for them.
  • Feature flags — disable unused tauri crate features; only enable plugins you ship.
  • Frontend tree-shaking — standard Vite/Rollup hygiene; the WebView ships your JS as-is.
  • Nightly extrastrim-paths = "all" (strips build paths from binaries), -Cdebuginfo=0.

See size.mdx for the full reference.

Decision matrix

I need to…Use
Call a Rust function from JS and await a resultCommand (invoke)
Read/write OS resources (fs, network, shell)Command (gated by ACL)
Broadcast “something happened” to all windowsEvent (emit from Core)
Stream progress updates from a long-running Rust taskEvent (emit repeatedly) or Channel
React in Rust to a frontend lifecycle moment (logout, route)Event (listen in Core)
Defend against supply-chain attacks in frontend depsIsolation pattern
Wrap an existing webapp with minimal changesBrownfield pattern (default)
Share state between windowsCore-owned state + Events/Commands
Synchronous call from JSNot supported — IPC is async
Manipulate the raw IPC envelopeIsolation hook, never directly

Gotchas

  • All IPC payloads round-trip through JSON. Binary blobs → base64 or use Tauri’s Channel (binary-friendly, v2-only).
  • The WebView runtime version varies per OS/user. Don’t assume bleeding-edge browser features without polyfills.
  • Secrets in Core only. The frontend bundle is trivially extractable from the .app/.exe.
  • One Core, many WebViews — global state belongs in Core (Rust tauri::State), not in any single window.