Tauri Tutorial: Build a Cross-Platform App in 13 Steps [2026]

Tauri 2.9.6, released December 9, 2025, put the production-ready Rust desktop framework on the map with mobile targets, a hardened IPC bridge, and bundles that average ~3 MB versus Electron’s ~96 MB – a 96% size reduction and roughly 50% lower RAM usage on the same workload. The framework hasn’t stood still since: the core runtime crate reached 2.11.5 in July 2026, crossing 1,635 GitHub releases along the way, and by September 2026 the whole 2.x stack – CLI, API, and updater alike – had synced up at Tauri 2.12.0, even as a first 3.0.0-alpha.1 preview (quickly followed by 3.0.0-alpha.2) began landing on GitHub that same month; this guide’s version numbers and commands still apply to the current stable 2.x release. This tutorial walks you from a clean machine to a signed, cross-platform desktop application in 13 steps, with a complete working project, 5+ code blocks, common pitfalls, and a full troubleshooting matrix. Expect about 90 minutes end-to-end if your toolchain is already installed.

The instructions target Tauri 2.x (the stable v2 line shipped in 2025), Rust stable via rustup, and Node.js 20 LTS or newer. The final project – a Markdown notes app called NotePad – exercises the v2 plugin system, the invoke IPC bridge, capability-based permissions, the built-in updater, and the official tauri-action GitHub workflow that produces signed installers for Windows (NSIS/MSI), macOS (DMG), and Linux (DEB/RPM/AppImage). You will leave with a real binary you can ship and a CI pipeline that rebuilds it on every tag.

Google · Preferred Sources

Don't miss new tech stories on Google

Add Tech Insider once in the Google app and our stories appear in your news suggestions.

Add Now

Why Tauri Beats Electron in 2026: The Numbers

Before writing a line of code, it helps to understand why developers are migrating from Electron to Tauri in 2026 – even though the npm download numbers still favor the incumbent: Electron pulled roughly 1.66 million weekly downloads versus Tauri’s ~85,000 as of April 2026, a reminder that ecosystem inertia takes years to shift even when the technical case is one-sided. Electron ships a full Chromium runtime with every app – roughly 96 MB after compression on Windows and macOS, climbing past 200 MB once you add Node modules. Tauri instead uses the operating system’s native WebView: WebView2 on Windows (Edge/Chromium), WKWebView on macOS, and WebKitGTK 4.1 on Linux. The result is a bundle that typically lands at ~3 MB for a minimal app, with first-paint times under 200 ms on a 2024-class laptop.

The memory story is just as lopsided. Independent 2026 benchmarks consistently report Tauri using roughly half the resident RAM of an equivalent Electron build, because there is no second Chromium process tree, no V8 isolate per renderer, and no Node main process. The trade-off is fragmentation: WKWebView, WebView2, and WebKitGTK have different bug surfaces. You write once, but you test three times. The official Tauri site publishes a compatibility matrix that you should keep open while developing.

The 2025 release of Tauri v2 closed the last major feature gap: mobile. The line matured quickly – Tauri 2.2.0 shipped in January 2025 with an upgraded [email protected], and by March 2026 Wikipedia lists Tauri 2.10.1 as the stable latest line, ahead of the 2.9.6 release this tutorial was built against; the underlying runtime crate itself ticked forward to 2.11.3 in June 2026 before the whole stack leapfrogged to 2.12.0 that September. iOS and Android targets ship from the same Rust core, sharing business logic with desktop. Combined with the new capability-based permission model – an evolution of v1’s allowlist – Tauri now competes head-to-head with React Native and Flutter on cross-platform reach, while keeping the desktop footprint that made it famous.

Tauri 2.x vs Electron: 2026 Benchmark Snapshot

MetricTauri 2.9.6Electron 33.xDelta
Hello-world bundle (Windows)~3 MB~96 MB32x smaller
Idle RAM (single window)~80 MB~160 MB~50% less
Cold start (M2 MacBook Air)~190 ms~640 ms~3.4x faster
Backend languageRust (compiled)Node.js (V8)Native vs JIT
WebViewSystem (WebView2/WKWebView/WebKitGTK 4.1)Bundled ChromiumShared vs duplicated
Mobile targetsiOS 9+, Android 8+Nonev2 exclusive
IPC modelCapability-permission bridgeBroad Node accessLeast privilege
Auto-updaterBuilt-in (full-binary, signed)electron-updater (third-party)First-party
LicenseMIT/Apache 2.0MITPermissive both

The cold-start and bundle numbers come from indie developer benchmarks published in 2026 across r/rust and the Tauri Discord; they match the official measurements published with the v2.9.6 release notes on December 9, 2025. Your mileage will vary with disk type, Defender scanning, and how much front-end JavaScript you load, but the order of magnitude is reliable.

Tauri 2.x vs Electron: 2026 Benchmark Snapshot

Prerequisites: Rust, Node, and System Toolchains

Tauri compiles a real Rust binary for your target platform, so the prerequisites are stricter than for a pure web app. You will need a working Rust toolchain, Node.js, and a small set of system libraries that vary by operating system. The full prerequisite list lives on the official prerequisites page and changes occasionally – verify it against your distro before starting.

  • Rust: stable channel, installed via rustup. Tauri 2.9.x requires rustc 1.77 or newer (most recent stable works).
  • Node.js: 20 LTS or newer. Node 22 LTS is recommended for new projects in 2026.
  • Package manager: any of npm, pnpm, yarn, or bun. This tutorial uses npm for portability.
  • Windows: Microsoft Edge WebView2 runtime (preinstalled on Windows 11 and patched Windows 10), plus Visual Studio 2022 Build Tools with the “Desktop development with C++” workload and the Windows 10/11 SDK.
  • macOS: Xcode Command Line Tools (xcode-select --install) on macOS 10.15 Catalina or newer.
  • Linux: build-essential, libwebkit2gtk-4.1-dev, libgtk-3-dev, libayatana-appindicator3-dev, librsvg2-dev, libssl-dev, pkg-config, and curl. Ubuntu 22.04+ or Fedora 39+ are the most painless distributions.

Verify your versions match before continuing. Mismatched Rust or Node versions are the single largest source of “works on my machine” support tickets in the Tauri Discord.

# Required: 1.77 or newer (Tauri 2.9.x baseline)
rustc --version
# rustc 1.84.0 (9fc6b4324 2025-01-07)

# Required: Node 20 LTS or newer
node --version
# v22.11.0

# Required: npm 10+ (bundled with Node 22)
npm --version
# 10.9.0

# Tauri CLI we will install in Step 2
cargo --version
# cargo 1.84.0 (66221abde 2025-01-09)

Step 1: Install Rust and the Tauri System Dependencies

On macOS and Linux, install Rust with the standard one-liner from rust-lang.org. On Windows, download rustup-init.exe from the same page and choose the default MSVC toolchain when prompted. After installation, restart your shell so the new PATH entries take effect.

# macOS / Linux
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# Then restart the shell or:
source "$HOME/.cargo/env"

# Pin the stable toolchain
rustup default stable
rustup update

On Ubuntu 22.04 or 24.04, install the system libraries that WebKitGTK needs. The exact package names matter – Tauri v2 requires the 4.1 series, not 4.0, which was the v1 default.

# Ubuntu 22.04 / 24.04
sudo apt update
sudo apt install -y 
  libwebkit2gtk-4.1-dev 
  build-essential 
  curl 
  wget 
  file 
  libxdo-dev 
  libssl-dev 
  libayatana-appindicator3-dev 
  librsvg2-dev 
  pkg-config

# Fedora 39+
sudo dnf install -y webkit2gtk4.1-devel 
  openssl-devel curl wget file libappindicator-gtk3-devel 
  librsvg2-devel gcc-c++ make

# Arch
sudo pacman -S --needed webkit2gtk-4.1 base-devel curl wget file 
  openssl appmenu-gtk-module libappindicator-gtk3 librsvg

On Windows, the Visual Studio Build Tools installer can be huge. Pick only “Desktop development with C++” – you do not need the full IDE. WebView2 is shipped automatically with Windows 11 and recent Windows 10 updates; if you target older Windows 10 builds, bundle the WebView2 bootstrapper using Tauri’s tauri.conf.json setting bundle.windows.webviewInstallMode.

Step 2: Install the Tauri CLI and Scaffold the Project

The fastest way to create a Tauri v2 project is the official create-tauri-app generator. It prompts you for a frontend (vanilla, React, Vue, Svelte, Solid, Yew, Leptos, etc.) and a package manager, then writes a working scaffold to disk. We will use Vanilla TypeScript with Vite to keep the focus on Tauri itself.

# Scaffold the project (interactive)
npm create tauri-app@latest

# Answers used for this tutorial:
#   Project name: notepad
#   Identifier: org.techinsider.notepad
#   Frontend language: TypeScript / JavaScript
#   Package manager: npm
#   UI template: Vanilla
#   UI flavor: TypeScript

cd notepad
npm install

# Install the Tauri CLI globally as well (optional but handy)
cargo install tauri-cli --version "^2.0" --locked

The scaffold produces a familiar shape: a top-level Node project for the frontend, plus a src-tauri/ directory that is a fully functional Rust crate. The Rust crate is where you write commands, manage state, register plugins, and define windows. The two halves talk over the Tauri IPC bridge, which we will exercise in Step 5.

notepad/
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
├── src/
│   ├── main.ts          # frontend entrypoint
│   └── styles.css
└── src-tauri/
    ├── Cargo.toml       # Rust dependencies
    ├── tauri.conf.json  # app config
    ├── build.rs
    ├── capabilities/
    │   └── default.json # permission set
    ├── icons/
    └── src/
        ├── main.rs
        └── lib.rs       # commands & plugin setup

Step 3: Run the Dev Server and Verify the Toolchain

Before changing anything, confirm the toolchain works end-to-end. The first npm run tauri dev compiles Rust dependencies and can take 3–8 minutes on cold caches – after that, incremental rebuilds are sub-second for frontend changes and a few seconds for Rust changes.

Step 3: Run the Dev Server and Verify the Toolchain
npm run tauri dev

# Expected output (truncated)
   Compiling tauri v2.9.6
   Compiling notepad v0.1.0 (/path/to/notepad/src-tauri)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 4m 12s
     Running `target/debug/notepad`
[2026-04-11T09:18:41Z INFO  tauri::app] window 'main' loaded

A native window should open with the default “Welcome to Tauri + Vite” page. If you see a blank window on Linux, you are almost certainly missing libwebkit2gtk-4.1-dev; reinstall it and rebuild. On Windows, a blank window usually means WebView2 is not installed – run the bootstrapper from Microsoft’s WebView2 page.

Step 4: Configure tauri.conf.json for a Real App

The src-tauri/tauri.conf.json file is the single source of truth for window properties, bundle identifiers, code-signing, and security. Edit it to set a real product name and window dimensions; these values flow into the installer metadata, the macOS Info.plist, and the Linux desktop entry.

{
  "$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
  "productName": "NotePad",
  "version": "0.1.0",
  "identifier": "org.techinsider.notepad",
  "build": {
    "frontendDist": "../dist",
    "devUrl": "http://localhost:1420",
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build"
  },
  "app": {
    "windows": [
      {
        "title": "NotePad",
        "width": 1024,
        "height": 720,
        "minWidth": 640,
        "minHeight": 480,
        "resizable": true,
        "fullscreen": false
      }
    ],
    "security": {
      "csp": "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'"
    }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/[email protected]",
      "icons/icon.icns",
      "icons/icon.ico"
    ],
    "category": "Productivity",
    "shortDescription": "A tiny Markdown notepad.",
    "longDescription": "NotePad is a 3 MB cross-platform Markdown editor built with Tauri 2."
  }
}

The csp entry is non-negotiable for shipping apps. A strict Content Security Policy is the cheapest mitigation you can adopt against malicious local files or rogue plugins. Read the Tauri security guide for the full hardening playbook.

Step 5: Write Your First Rust Command and Call It from the UI

Tauri’s headline feature is the invoke IPC bridge. You expose Rust functions as commands, then call them from JavaScript with a single line. There is no localhost HTTP server, no JSON-RPC server you have to start – Tauri wires it up at compile time. Open src-tauri/src/lib.rs and replace the contents.

// src-tauri/src/lib.rs
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct Note {
    pub id: u64,
    pub title: String,
    pub body: String,
    pub updated_at: u64,
}

#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello {name}, welcome to NotePad on Tauri 2.9.6!")
}

#[tauri::command]
fn word_count(body: &str) -> usize {
    body.split_whitespace().count()
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .invoke_handler(tauri::generate_handler![greet, word_count])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The #[tauri::command] attribute exposes the function to the frontend. Note generate_handler! – every new command must be added to that macro or it will return Command not allowed. On the frontend, call the commands with @tauri-apps/api/core.

// src/main.ts
import { invoke } from "@tauri-apps/api/core";

const button = document.querySelector<HTMLButtonElement>("#say-hi")!;
const out    = document.querySelector<HTMLPreElement>("#out")!;
const editor = document.querySelector<HTMLTextAreaElement>("#editor")!;
const stats  = document.querySelector<HTMLSpanElement>("#stats")!;

button.addEventListener("click", async () => {
  const msg = await invoke<string>("greet", { name: "Tech Insider" });
  out.textContent = msg;
});

editor.addEventListener("input", async () => {
  const n = await invoke<number>("word_count", { body: editor.value });
  stats.textContent = `${n} words`;
});

Save, and the running dev window hot-reloads. Type into the textarea and watch the word count update in real time – every keystroke serializes a JSON message, jumps the WebView/Rust boundary, runs word_count in compiled Rust, and returns the result. On a 2024 MacBook Air the round-trip is under 1 ms.

Step 6: Persist State with the tauri-plugin-store Plugin

A real notepad needs to remember notes between launches. Tauri v2 splits functionality into official plugins – file system, dialog, store, fs-watch, autostart, single-instance, updater, and more – and the plugin ecosystem keeps shipping: the runtime itself moved from 2.9.2 in November 2025 to 2.9.6 that December, the store plugin reached 2.4.0 in August 2025 with JS-side defaults support, the upload plugin hit its own 2.4.0 in January 2026, the localhost plugin hit 2.0.0 in January 2026 for stable v2 integration, and the stronghold secrets plugin landed its own 2.0.0 release back in October 2025 – and by September 2026 the tooling caught up too, with both the Tauri CLI and the JavaScript @tauri-apps/api package publishing synchronized 2.12.0 releases. Each one is added to Cargo and to the JavaScript package, then wired into the builder and the capability set.

# Rust side
cd src-tauri
cargo add tauri-plugin-store
cd ..

# JS side
npm install @tauri-apps/plugin-store
// src-tauri/src/lib.rs (additions)
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_shell::init())
        .plugin(tauri_plugin_store::Builder::new().build())
        .invoke_handler(tauri::generate_handler![greet, word_count])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
// src/main.ts
import { Store } from "@tauri-apps/plugin-store";

const store = await Store.load("notes.json");

async function saveNotes(notes: Note[]) {
  await store.set("notes", notes);
  await store.save();
}

async function loadNotes(): Promise<Note[]> {
  return (await store.get<Note[]>("notes")) ?? [];
}

The store file lives at the OS-conventional app-data path: %APPDATA%org.techinsider.notepad on Windows, ~/Library/Application Support/org.techinsider.notepad on macOS, and ~/.local/share/org.techinsider.notepad on Linux. You do not have to compute the path – the plugin handles it.

Step 7: Capability-Based Permissions (Tauri v2’s Killer Feature)

Tauri v1 had a single global allowlist. Tauri v2 replaced it with capabilities: per-window, per-context permission sets that compose like Unix groups. The default capability lives at src-tauri/capabilities/default.json. Edit it to grant only what the app needs – least privilege is the default.

Step 7: Capability-Based Permissions (Tauri v2's Killer Feature)
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capabilities required by the NotePad main window.",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "core:window:allow-close",
    "core:window:allow-minimize",
    "store:default",
    "store:allow-load",
    "store:allow-set",
    "store:allow-save",
    "store:allow-get",
    "shell:allow-open"
  ]
}

If you try to call store.save() without store:allow-save, Tauri throws store.save not allowed at runtime. That is the design: every capability must be opted into, in writing, in the manifest. The IPC concepts doc covers the full permission DSL.

Step 8: File Dialogs, Imports, and the dialog Plugin

NotePad should be able to import existing .md files. That requires the dialog and fs plugins, plus three new capability permissions.

cd src-tauri
cargo add tauri-plugin-dialog tauri-plugin-fs
cd ..
npm install @tauri-apps/plugin-dialog @tauri-apps/plugin-fs
// builder.rs additions
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
// src/main.ts
import { open } from "@tauri-apps/plugin-dialog";
import { readTextFile } from "@tauri-apps/plugin-fs";

async function importMarkdown(): Promise<void> {
  const selected = await open({
    multiple: false,
    filters: [{ name: "Markdown", extensions: ["md", "markdown", "txt"] }]
  });
  if (typeof selected === "string") {
    const text = await readTextFile(selected);
    editor.value = text;
    editor.dispatchEvent(new Event("input"));
  }
}

Add the matching capability entries: "dialog:allow-open", "fs:allow-read-text-file", and a scoped "fs:scope": [{ "path": "$HOME/Documents/**" }] if you want to constrain the file picker. Scoped permissions are the Tauri v2 answer to Electron’s blanket Node.js file access.

Step 9: Type-Safe State with Tauri’s Managed State

Frontend state is fine for transient UI, but anything sensitive – encryption keys, an HTTP client with cached cookies, a SQLite connection pool – should live in Rust. Tauri exposes tauri::State for this. You register a value once at startup; commands receive it as a parameter.

use std::sync::Mutex;
use tauri::State;

#[derive(Default)]
pub struct AppState {
    notes: Mutex<Vec<Note>>,
}

#[tauri::command]
fn add_note(state: State<'_, AppState>, title: String, body: String) -> u64 {
    let mut notes = state.notes.lock().unwrap();
    let id = notes.len() as u64 + 1;
    notes.push(Note {
        id,
        title,
        body,
        updated_at: chrono::Utc::now().timestamp() as u64,
    });
    id
}

#[tauri::command]
fn list_notes(state: State<'_, AppState>) -> Vec<Note> {
    state.notes.lock().unwrap().clone()
}

pub fn run() {
    tauri::Builder::default()
        .manage(AppState::default())
        .plugin(tauri_plugin_store::Builder::new().build())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_fs::init())
        .invoke_handler(tauri::generate_handler![
            greet, word_count, add_note, list_notes
        ])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

This pattern – Rust as the source of truth, frontend as a view – lets you swap the UI without touching business logic. Tauri’s official Calling Rust guide shows the full type-mapping rules for arguments and return values.

Step 10: Cross-Platform System Tray and Notifications

Modern desktop apps live partly in the tray. Tauri v2 ships a unified tray API that works on all three desktops, with a fallback to the Ayatana indicator on Linux. Add it with the tray-icon feature.

# src-tauri/Cargo.toml
[dependencies]
tauri = { version = "2.9", features = ["tray-icon"] }
tauri-plugin-notification = "2"
use tauri::menu::{Menu, MenuItem};
use tauri::tray::TrayIconBuilder;

.setup(|app| {
    let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
    let menu = Menu::with_items(app, &[&quit])?;
    TrayIconBuilder::new()
        .icon(app.default_window_icon().unwrap().clone())
        .menu(&menu)
        .on_menu_event(|app, event| {
            if event.id == "quit" {
                app.exit(0);
            }
        })
        .build(app)?;
    Ok(())
})

System notifications work the same way: add tauri-plugin-notification, request the permission, then call sendNotification from the frontend. On macOS, the first notification triggers the system permission prompt; users can revoke it from System Settings later.

Step 11: Build a Release Binary and Compare It to Electron

Time to see the size win in person. Run the release build – the first one takes 5–15 minutes depending on hardware; subsequent rebuilds are roughly 1 minute thanks to cargo incremental compilation.

Step 11: Build a Release Binary and Compare It to Electron
npm run tauri build

# macOS output:
#   src-tauri/target/release/bundle/dmg/NotePad_0.1.0_aarch64.dmg     (3.4 MB)
#   src-tauri/target/release/bundle/macos/NotePad.app                 (~11 MB unzipped)
#
# Windows output:
#   src-tauri/target/release/bundle/nsis/NotePad_0.1.0_x64-setup.exe  (3.1 MB)
#   src-tauri/target/release/bundle/msi/NotePad_0.1.0_x64_en-US.msi   (3.5 MB)
#
# Linux output:
#   src-tauri/target/release/bundle/deb/notepad_0.1.0_amd64.deb        (4.2 MB)
#   src-tauri/target/release/bundle/appimage/notepad_0.1.0_amd64.AppImage (76 MB - includes WebKitGTK)
#   src-tauri/target/release/bundle/rpm/notepad-0.1.0-1.x86_64.rpm     (4.1 MB)

Note the AppImage: it carries the WebKitGTK runtime so it works on distros that don’t ship 4.1. If you only need to support modern Ubuntu and Fedora, ship .deb/.rpm and skip AppImage. The compressed Mac DMG of ~3.4 MB versus a typical Electron equivalent of 96+ MB is the headline number you can put on your release page.

Step 12: Code Signing and the Built-In Updater

Unsigned apps trigger SmartScreen on Windows and Gatekeeper on macOS, which kills install conversion. For a real release, generate signing keys and pass them as environment variables to tauri build.

# Generate an updater signing key
npx @tauri-apps/cli signer generate -w ~/.tauri/notepad.key

# Output:
# Your keypair was generated successfully
# Public key: dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWdu...
# Private key (encrypted) saved to: ~/.tauri/notepad.key
// tauri.conf.json (additions)
"plugins": {
  "updater": {
    "active": true,
    "endpoints": [
      "https://releases.notepad.example.com/{{target}}/{{current_version}}"
    ],
    "dialog": true,
    "pubkey": "PASTE_PUBLIC_KEY_HERE"
  }
}

For Windows code signing, use a hardware-backed EV certificate (HSM or YubiKey-FIPS); for macOS, an Apple Developer ID Application certificate plus notarytool for notarization. Both are environment-variable driven in CI, so you can keep secrets out of the repo.

Step 13: Ship It – GitHub Actions and tauri-action

The final step is shipping. The official tauri-action GitHub workflow builds for Windows, macOS (Intel + Apple Silicon), and Linux from a single tag push, creates a GitHub Release, uploads the installers, and signs them with your Apple/Tauri/Microsoft credentials pulled from repository secrets.

# .github/workflows/release.yml
name: release

on:
  push:
    tags: ['v*']

jobs:
  build:
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: macos-14
            args: '--target aarch64-apple-darwin'
          - platform: macos-13
            args: '--target x86_64-apple-darwin'
          - platform: ubuntu-22.04
            args: ''
          - platform: windows-latest
            args: ''
    runs-on: ${{ matrix.platform }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '22' }
      - uses: dtolnay/rust-toolchain@stable
      - name: Install Linux deps
        if: matrix.platform == 'ubuntu-22.04'
        run: |
          sudo apt update
          sudo apt install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev
      - run: npm ci
      - uses: tauri-apps/tauri-action@v0
        env:
          GITHUB_TOKEN:                ${{ secrets.GITHUB_TOKEN }}
          TAURI_SIGNING_PRIVATE_KEY:   ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PASSWORD }}
          APPLE_CERTIFICATE:           ${{ secrets.APPLE_CERTIFICATE }}
          APPLE_CERTIFICATE_PASSWORD:  ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          APPLE_SIGNING_IDENTITY:      ${{ secrets.APPLE_SIGNING_IDENTITY }}
          APPLE_ID:                    ${{ secrets.APPLE_ID }}
          APPLE_TEAM_ID:               ${{ secrets.APPLE_TEAM_ID }}
          APPLE_PASSWORD:              ${{ secrets.APPLE_PASSWORD }}
        with:
          tagName: v__VERSION__
          releaseName: 'NotePad v__VERSION__'
          releaseDraft: true
          prerelease: false
          args: ${{ matrix.args }}

Push a tag (git tag v0.1.0 && git push --tags) and the workflow builds five binaries in parallel: .dmg for Apple Silicon, .dmg for Intel Mac, .deb + .AppImage for Linux, and .msi + .nsis.exe for Windows. Average end-to-end runtime on GitHub-hosted runners is 18–25 minutes.

Common Pitfalls and How to Avoid Them

Tauri’s surface area is small, but the cross-platform nature exposes some sharp edges. These are the seven pitfalls that come up most often on the Tauri Discord and GitHub Discussions in 2026.

  1. Forgetting generate_handler!. New #[tauri::command] functions don’t auto-register. You must add them to the tauri::generate_handler! macro, or every call from JS returns command not allowed.
  2. Granting blanket capabilities. Many tutorials suggest "fs:default" or "*" to “just make it work.” That re-creates the Electron security model. Scope every plugin permission to the smallest set the feature needs.
  3. Mixing Tauri 1 and Tauri 2 docs. Google still surfaces v1 tutorials from 2023–2024. Anything mentioning allowlist or tauri-build 1.x is obsolete. Pin yourself to v2.tauri.app.
  4. Slow Linux GTK4 builds. Mixing libwebkit2gtk-4.1-dev with the older 4.0 series causes hard-to-diagnose link errors. apt purge libwebkit2gtk-4.0-dev if you previously built v1 apps.
  5. Cold Cargo cache eating 5–15 minutes. CI builds without actions/cache recompile every dependency. Cache ~/.cargo/registry and src-tauri/target aggressively.
  6. Frontend dev-server port mismatch. If you change vite.config.ts‘s port (default 1420), update tauri.conf.json‘s build.devUrl or Tauri loads a 404 in the WebView.
  7. Bundling huge frontends. Tauri’s reputation for 3 MB binaries assumes you ship a small SPA. A bloated React + Material UI build can drag the bundle past 30 MB on its own.

Troubleshooting Matrix

Troubleshooting Matrix
SymptomLikely CauseFix
Blank window on LinuxMissing libwebkit2gtk-4.1-devsudo apt install libwebkit2gtk-4.1-dev then rebuild
Blank window on Windows 10WebView2 runtime missingInstall Evergreen Runtime; set webviewInstallMode to downloadBootstrapper
command not allowed at runtimeNot added to generate_handler! or missing permissionRegister in macro AND grant permission in capabilities/default.json
linker 'cc' not foundNo C compiler on Linuxsudo apt install build-essential
Cold build > 15 minutesFirst-time crate downloadNormal. Use sccache or cache ~/.cargo in CI
App crashes on macOS GatekeeperUnsigned/unnotarized binarySign with Developer ID; run notarytool submit --wait
SmartScreen warning on WindowsNo EV code-signing certUse Azure Code Signing or a hardware-backed cert
HMR doesn’t refresh windowVite port not matching devUrlAlign vite.config.ts and tauri.conf.json ports
Tauri 1 examples don’t compileBreaking changes in v2 APIsFollow v2 migration guide; rename allowlist to capabilities
iOS build error xcrun: errorXcode CLI tools missingxcode-select --install and accept license

Advanced Tips for Production Tauri Apps

After Step 13 you have a working app. The following techniques are what separates a weekend prototype from a shippable product.

  • Use panic = "abort" in Cargo.toml‘s release profile – cuts the binary by ~200 KB and gives you cleaner crash logs.
  • Enable LTO and strip symbols: lto = true, strip = true, codegen-units = 1. Expect a 1.5–2x slower release build for a 20–30% smaller binary.
  • Use tauri-plugin-single-instance to prevent multiple windows on app re-launch – a frequent Windows UX bug.
  • Wire in tracing with tauri-plugin-log writing to $APPLOG. Always rotate logs – do not append forever.
  • Sandbox unknown HTML with a separate WebView and a stricter capability set. Untrusted Markdown rendered with a different identifier cannot escalate to the main window.
  • Test on Windows ARM. Tauri 2.9 ships ARM64 binaries; if you target Surface or new Copilot+ PCs, add aarch64-pc-windows-msvc to the CI matrix.
  • Use --target universal-apple-darwin to ship a single fat Mac binary that runs on both Intel and Apple Silicon if you don’t want two DMGs.
  • Pin tauri-cli to an exact version in package.json devDependencies. CLI patches occasionally change codegen and break reproducible builds.

Mobile Targets: Tauri 2 on iOS and Android

Tauri v2’s biggest 2025 feature was shipping mobile from the same codebase. The story is simple: the same Rust crate, the same tauri::Builder, the same commands. The only difference is the entry point and the target.

# iOS (macOS host, Xcode required)
npm run tauri ios init
npm run tauri ios dev
npm run tauri ios build

# Android (any host, Android Studio / SDK required)
export ANDROID_HOME="$HOME/Library/Android/sdk"
export NDK_HOME="$ANDROID_HOME/ndk/$(ls -1 $ANDROID_HOME/ndk | tail -1)"
npm run tauri android init
npm run tauri android dev
npm run tauri android build --aab

Mobile builds require minimum iOS 9 and Android 8 (API 26). Plugins that touch the file system or camera need to declare the corresponding iOS Info.plist keys or Android AndroidManifest.xml permissions – the Tauri CLI generates the scaffolding, but you must approve the final manifest before submitting to the App Store or Play Store.

FAQ

Is Tauri production-ready in 2026?

Yes. Tauri v2 reached stable in late 2024 and has kept advancing since – 2.9.6 shipped December 9, 2025, Wikipedia marked 2.10.1 as the stable latest line in March 2026, the runtime picked up new features in 2.11.0 that April, the runtime ticked to 2.11.3 and the CLI followed with 2.11.4 bug fixes in June 2026, the core runtime crate had reached 2.11.5 by July 2026, and by September 2026 the CLI, API, and updater had all synced up at 2.12.0 while early 3.0.0-alpha.1 and 3.0.0-alpha.2 builds began appearing on GitHub as a preview of what’s next. It is used in production by security tooling, developer tools, productivity apps, and several open-source clients with hundreds of thousands of installs.

Why is the Linux AppImage so much bigger than the .deb?

AppImage embeds the WebKitGTK 4.1 runtime so the binary works on distros that ship older WebKit. The .deb and .rpm declare WebKitGTK as a dependency and let the system supply it, which is why they stay around 4 MB while the AppImage climbs to ~76 MB.

Can I use React, Vue, or Svelte with Tauri?

Yes – Tauri is framework-agnostic. create-tauri-app includes templates for React, Vue, Svelte, Solid, Preact, Angular, Yew, Leptos, and Vanilla. Anything that produces a static bundle in dist/ works.

Does Tauri replace Electron?

For most greenfield apps in 2026, yes. Tauri delivers ~96% smaller binaries and ~50% less RAM. The cases where Electron still wins: heavy Node-ecosystem dependencies that don’t compile to Rust or run in a WebView, deep Chrome DevTools workflows, or organizations with existing Electron expertise and code.

How do I update an installed Tauri app?

Use the built-in updater plugin – now at 2.12.0 since its September 2026 release. Generate a signing key with tauri signer generate, publish signed releases at an HTTPS endpoint, and the app downloads and verifies the new binary on launch. Because typical Tauri apps are only a few megabytes, the updater downloads the full binary instead of using differential patches.

Do I need to know Rust to use Tauri?

Some, but not much. You can build complete apps using only the JavaScript APIs and the prebuilt plugins. You only touch Rust when you write custom #[tauri::command] functions or write your own plugin – and even then, you mostly use simple types like String, u64, and Vec that map cleanly to JSON.

Is Tauri secure by default?

More secure than Electron, yes. The default capability set is empty – every API requires explicit opt-in. The strict CSP, scoped file-system permissions, and absence of a global Node.js runtime remove three of the four most common Electron CVE classes. See the official security page for the hardening checklist.

What’s the minimum Rust version for Tauri 2.9?

Rust 1.77 stable or newer. The Tauri team tests against the current stable and the previous two minor versions, but tracks new MSRVs conservatively – usually only bumping every few minor releases.

Conclusion: Ship Smaller, Ship Faster

If you followed every step, you have a real signed installer, capability-based permissions, a Rust-side state container, a system tray, and a CI pipeline that rebuilds and re-signs the app on every git tag. Total binary on macOS: about 3.4 MB, versus the ~100 MB you would have shipped with Electron 33. Cold start on a 2024-class laptop: roughly 190 ms. RAM at idle: ~80 MB.

The remaining work is product, not framework. Pick the front-end stack you like, wire a real database with tauri-plugin-sql or a managed SQLite via Rust, and use the built-in updater so your users always have the latest release. Tauri 2.x removes the historic reasons not to ship desktop software – size, speed, and security – and leaves you with the same question every product team has: what should we build next?

Nadia Dubois

Nadia Dubois

AI & Innovation Editor

Nadia Dubois is the AI & Innovation Editor at Tech Insider, where she tracks the rapid evolution of artificial intelligence, from foundation models to real-world enterprise deployment. She previously covered AI and startups for La Tribune and contributed to MIT Technology Review's European coverage. Nadia specializes in generative AI, AI regulation, and the intersection of technology and European industrial policy. She holds a dual degree in Computational Linguistics and Journalism from Sciences Po Paris.

View all articles