Flexible Architecture

How Tauris composable layers let you freely choose frontend frameworks native code languages and plugins while controlling exactly what the frontend can access

Tauri does not force a single UI framework, a locked-down backend language, or a fixed set of native features. Instead, its architecture is built from independent, composable parts: a frontend shell, a Rust core, an IPC bridge, separate windowing and rendering layers, and a plugin system that can pull in native code written in other languages. Each layer can be configured, extended, or replaced. This is not just theoretical flexibility — it is the reason teams with wildly different technology stacks can all use Tauri without having to rewrite existing code or abandon familiar tools.

The sections that follow explain each layer in detail, from the frontend-agnostic rendering model down to the capability-based permission system that gates what code in the WebView is allowed to call.

Frontend-Agnostic by Design

Tauri does not include a UI toolkit. It serves whatever HTML, CSS, and JavaScript you give it inside the operating system’s native WebView. The only requirement is that your frontend compiles to a set of static files — an index.html entry point plus any linked assets. React, Vue, Svelte, Solid, Qwik, plain HTML with a script tag, even a hand-rolled WebAssembly module: they all work because Tauri treats the frontend as opaque static content.

This works because Tauri loads the frontend through a custom protocol (tauri://) rather than spinning up a local HTTP server. At build time, the frontend files are embedded into the Rust binary, and the custom protocol handler intercepts tauri:// requests and serves them directly from memory. This eliminates port conflicts, reduces attack surface, and keeps the final binary self-contained.

The path to the production build directory is configured in tauri.conf.json:

src-tauri/tauri.conf.json
{
  "build": {
    "frontendDist": "../dist"
  }
}

After a frontend build step populates that directory, tauri build bundles its contents into the final executable. The same frontendDist path works whether you build with Vite, webpack, Next.js (with static export), or a simple cp command.

No runtime restrictions:

Tauri does not inspect or modify your frontend source code. Any framework that produces static output is supported. You can even drop a single index.html file with inline scripts — no JavaScript framework required.

The trade‑off is that you are responsible for the frontend build pipeline. Tauri provides configuration templates for popular frameworks in its documentation, but the architecture itself is deliberately uninvolved.

IPC — The Bridge Between Frontend and Backend

The WebView runs untrusted JavaScript code. To let that code interact with the operating system — reading files, showing notifications, accessing hardware — Tauri provides an inter-process communication (IPC) layer (see Connecting Backend to Frontend). JavaScript calls a function on the Rust side, passes serializable arguments, and receives a serializable response. The mechanism is the invoke function from the @tauri-apps/api/core package.

A Rust function becomes callable from the frontend by adding the #[tauri::command] attribute and registering it with the Tauri builder:

src-tauri/src/lib.rs
#[tauri::command]
fn get_system_info() -> String {
    format!(
        "OS: {}, Hostname: {}",
        std::env::consts::OS,
        whoami::devicename()
    )
}
pub fn run() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![get_system_info])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

On the JavaScript side, the call looks like any async function:

import { invoke } from '@tauri-apps/api/core';
const info = await invoke<string>('get_system_info');
console.log(info); // "OS: macos, Hostname: amits-macbook"

Arguments and return values are serialized as JSON by default. Rust deserializes the JSON into the expected types using Serde. For large binary payloads, Tauri v2 introduced raw payload channels that avoid the overhead of JSON serialization entirely.

Never trust frontend input:

The WebView runs in a sandbox, but its JavaScript is untrusted. A user could open DevTools and call invoke with arbitrary arguments. Always validate and sanitize every input on the Rust side as if it came from an external attacker. Do not rely on the frontend to perform validation.

Commands can also access shared state through Tauri’s managed state system:

src-tauri/src/lib.rs
use std::sync::Mutex;
use tauri::State;
struct AppState {
    counter: Mutex<i32>,
}
#[tauri::command]
fn increment(state: State<AppState>) -> i32 {
    let mut count = state.counter.lock().unwrap();
    *count += 1;
    *count
}
// In the builder:
// .manage(AppState { counter: Mutex::new(0) })

On the JavaScript side, invoke('increment') returns the new count. The mutex ensures that concurrent calls from the frontend do not cause data races.

Chatty IPC can hurt responsiveness:

Each invoke call crosses the process boundary and incurs serialization cost. For high-frequency calls — updating a slider position, processing real‑time sensor data — batch updates or use a dedicated channel. Tauri v2’s raw payload and event system can transmit data without repeated JSON round‑trips.

Beyond request‑response commands, Tauri supports a pub‑sub event system. The backend can emit events to all windows (app.emit("tick", payload)) and the frontend listens with listen("tick", (event) => {...}). This is useful for push notifications from long‑running Rust tasks.

Window Creation and WebView Rendering — TAO and WRY

Tauri does not implement window handling or WebView management itself. It delegates those responsibilities to two separate upstream libraries maintained by the Tauri project:

  • TAO — a cross‑platform window creation library, forked from the winit crate and extended with menu bar and system tray support.
  • WRY — a cross‑platform WebView rendering library that provides a unified interface over the system’s native web engine: WKWebView on macOS and iOS, WebView2 on Windows, and WebKitGTK on Linux.

These two crates are the components that interact directly with the operating system. Tauri sits on top of them through a runtime abstraction layer (tauri-runtime and tauri-runtime-wry) that translates generic window and webview commands into TAO/WRY calls.

+-------------------+
|   Tauri (Core)    |
+--------+----------+
         |
+--------v----------+
|   tauri-runtime   |  <-- runtime abstraction
+--------+----------+
         |
+--------v----------+
| tauri-runtime-wry |  <-- WRY-specific glue
+--------+----------+
         |
+--------v----------+
|   WRY (WebView)   |  <-- rendering
+-------------------+
         |
+--------v----------+
|   TAO (Window)    |  <-- windowing
+-------------------+

Because TAO and WRY are separate crates, an advanced user can swap out the windowing or rendering backend without touching the rest of Tauri. This is not a common workflow, but it is the architectural property that makes Tauri viable on platforms where the native webview differs significantly — the abstraction layer isolates those differences.

The practical outcome for developers is that Tauri windows look and behave like native windows. Window decorations, resizing, full‑screen transitions, and system tray integration are all handled by TAO, not by JavaScript or CSS. The WebView content inside the window cannot escape its container.

TAO and WRY are working if you see a native window:

When you run npm run tauri dev and a window appears with the correct OS title bar, proper shadow, and standard resize handles, TAO and WRY have initialized correctly. The WebView is rendering the frontend if you see your HTML content inside that window — no additional verification needed.

Plugin System — Extending Native Capabilities

Core Tauri exposes a minimal set of APIs. Most native functionality — filesystem access, SQLite, biometrics, push notifications — lives in plugins. A plugin is a Rust crate that registers additional commands and, optionally, ships a corresponding JavaScript package for type‑safe invoke calls.

The architecture of a plugin mirrors Tauri’s own structure: Rust code does the actual work, JS code provides a friendly API. Adding a plugin involves two steps:

  1. Add the crate to Cargo.toml in src-tauri.
  2. Register the plugin in the Tauri builder.

For example, the filesystem plugin:

src-tauri/Cargo.toml
[dependencies]
tauri-plugin-fs = "2"
src-tauri/src/lib.rs
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .run(tauri::generate_context!())
        .expect("error");
}

The frontend then imports the plugin’s JS package:

import { readTextFile } from '@tauri-apps/plugin-fs';
const contents = await readTextFile('/path/to/file.txt');

Plugins are not monolithic. The official set includes file system access, SQL databases, shell command execution, stronghold (encrypted storage), global shortcuts, notifications, and many more. Community plugins extend the ecosystem further.

Silent failure from missing permissions:

Tauri v2 requires explicit capability declarations for plugin commands. If a command is not allowed in the app’s capabilities, the invoke call fails with a permission error. This error may appear only in the browser console as a warning — check there first when a plugin call mysteriously returns an error.

Plugins can also include platform‑specific native code. On iOS, a plugin may contain Swift files that access UIKit or HealthKit. On Android, it may contain Kotlin classes that call into Android SDKs. The plugin’s Rust layer acts as a bridge, exposing those native implementations to the JavaScript frontend through the same invoke mechanism.

Multi‑Language Backend — Rust, Swift, Kotlin, and Beyond

The core of every Tauri app is compiled from Rust. That is non‑negotiable. But the architecture does not limit you to Rust for all native logic. Several escape hatches let you bring in code written in other languages without abandoning Tauri’s tooling.

Mobile plugin code. As described, plugins can contain Swift and Kotlin source files. When building for iOS, the Swift code is compiled by Xcode and linked into the final app. On Android, the Kotlin code becomes part of the APK. The Tauri CLI orchestrates these builds, but the logic itself is written in the platform’s native language. This is how a single Tauri codebase can access HealthKit on iOS and Google Fit on Android while sharing a common React frontend.

Sidecar binaries. Tauri can embed and spawn external executables — called sidecars — that run alongside the main process. A sidecar can be written in any language that compiles to a standalone binary. Communication between the frontend and the sidecar typically goes through the Rust core using commands that spawn the process and read its stdout, or via a local socket. This pattern lets you integrate a Python machine‑learning model, a Go microservice, or a legacy C++ library without rewriting them in Rust.

Foreign function interfaces. From Rust, you can call C libraries directly through FFI. If you have an existing native library compiled as a shared object, you can write a thin Rust wrapper that exposes its functions as Tauri commands. The rest of the app — frontend, bundling, updates — remains unchanged.

The combination means that a team with existing native codebases can adopt Tauri incrementally. They wrap native capabilities behind Tauri commands, keep the frontend in their preferred web framework, and deploy on every platform.

The Permission System as an Architectural Lever

Tauri v2 replaced the v1 allowlist with a capability‑based permission model. This is not just a security feature — it directly shapes how you architect the boundary between frontend and backend.

Every Tauri command, whether from core or a plugin, requires an explicit capability grant before the frontend can invoke it. Capabilities are declared in JSON files inside the capabilities directory and can be scoped to specific windows, URLs, or patterns. For example, a capability that allows reading files only from the app’s data directory:

src-tauri/capabilities/default.json
{
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    {
      "identifier": "fs:allow-read-text-file",
      "allow": [{ "path": "$APPDATA/**" }]
    }
  ]
}

Without this declaration, any invoke to read_text_file fails — even if the plugin is installed and registered. This forces you to think about exactly which native operations each part of the frontend needs. It also means you can disable entire APIs at the configuration level. The tauri.conf.json build configuration even lets you strip unused Rust features from the final binary by toggling API flags.

The permission system integrates with Tauri’s isolation pattern, which can run the frontend’s JavaScript in a sandboxed iframe that is not allowed to call invoke directly. A trusted script outside the iframe mediates calls, applying allow‑listing logic before forwarding them to Rust. This is an advanced setup, but it shows how the architecture accommodates security‑sensitive applications without requiring a fork of Tauri itself.

By making the capability model declarative and files‑based, Tauri keeps the architecture flexible: you can change what the frontend can do without touching Rust or JavaScript source, just by editing a JSON file. This is valuable during development, when you want to open up broad access, and in production, when you want to lock everything down to the minimum needed.


What Makes the Architecture Flexible

The flexibility of Tauri’s architecture does not come from a single design choice — it comes from the fact that each layer is an independent, replaceable component with a well‑defined boundary. The frontend is just static files. The IPC layer is a serialization contract. The windowing and rendering are delegated to swappable crates. Plugins follow the same command‑registration pattern as core. Permissions gate everything declaratively.

The practical result is that Tauri can fit into an organization’s existing technology stack rather than requiring the stack to fit Tauri. A team that standardizes on React can keep React. A team that needs native iOS features can write Swift inside a plugin. A team with a legacy C++ library can expose it through Rust FFI. None of these choices require forking Tauri or fighting the framework.

If you are evaluating Tauri against other cross‑platform options, ask not “does it have feature X?” but “can I build feature X without leaving the architecture?” In Tauri, the answer is almost always yes — because the architecture was designed to get out of the way.