Skip to content

Fix: Tauri 2 Not Working — Capabilities, Plugin Permissions, Mobile Build, and v1 Migration

FixDevs ·

Quick Answer

How to fix Tauri 2 errors — invoke command not allowed by capabilities, plugin permission missing, tauri.conf.json schema, mobile init/build failures, updater migration, and v1 allowlist conversion.

The Error

You upgrade to Tauri 2 and a working invoke call from the frontend fails:

import { invoke } from "@tauri-apps/api/core";

await invoke("greet", { name: "Alice" });
// Error: Command greet not allowed by ACL

Or you add the dialog plugin and it refuses to open:

import { open } from "@tauri-apps/plugin-dialog";

const path = await open();
// Error: dialog.open not allowed. Required: dialog:default

Or cargo tauri android init fails on a fresh project:

Error: NDK not found. Set ANDROID_NDK_HOME or install via Android Studio.

Or your v1 tauri.conf.json doesn’t validate:

Error parsing tauri.conf.json:
unknown field `allowlist`, expected one of `productName`, `version`, `identifier`, ...

Why This Happens

Tauri 2 introduced a permissions-and-capabilities model that replaces v1’s allowlist. Every command (built-in or custom) and every plugin call must be explicitly permitted, scoped to specific windows/webviews. Three sources of friction:

  • Capabilities are required. v1’s “expose everything by default in dev, lock down in prod” pattern is gone. Tauri 2 requires you to grant explicit permissions in capability files. Missing permissions → command blocked, even in dev.
  • Plugin permissions are separate from app permissions. Every plugin (dialog, fs, shell, updater, notification) ships its own permission set. Adding a plugin to Cargo.toml doesn’t grant permission to call it — you also add it to a capability.
  • Mobile is a separate build target. cargo tauri build produces desktop binaries; mobile needs cargo tauri android / cargo tauri ios. Toolchain setup (NDK, Xcode) trips up most first-time mobile builds.
  • Config schema is different. tauri.conf.json was completely restructured. allowlist is gone; bundle, app, build are flatter. Old configs don’t parse.

Fix 1: Create a Capability File

Capabilities live in src-tauri/capabilities/. Create default.json:

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability granted to the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "core:window:default",
    "core:webview:default",
    "core:event:default",
    "dialog:default",
    "fs:default",
    "shell:default"
  ]
}

Reference it in tauri.conf.json:

{
  "app": {
    "security": {
      "csp": null
    },
    "windows": [{ "label": "main", "title": "My App" }]
  }
}

The capabilities/ directory is auto-loaded — you don’t list capability files in tauri.conf.json. They apply by matching the windows field.

To grant a permission to all windows:

{
  "identifier": "all",
  "windows": ["*"],
  "permissions": [...]
}

Pro Tip: Split capabilities by feature. default.json for the main window, admin.json scoped to an admin window with broader permissions. Reduces the risk of widening one window’s surface accidentally.

Fix 2: Add Plugin Permissions

When you add a plugin to Cargo.toml:

[dependencies]
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
tauri-plugin-shell = "2"

And register it in src-tauri/src/lib.rs:

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

You also need to grant the plugin’s permissions in a capability. The plugin docs list available permission identifiers:

{
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "dialog:default",
    "dialog:allow-open",
    "dialog:allow-save",
    "fs:default",
    "fs:allow-read-file",
    "fs:allow-write-file",
    "shell:default",
    "shell:allow-open"
  ]
}

For finer-grained scopes (e.g. only read files under a specific directory):

{
  "identifier": "fs:scope-app",
  "allow": [{ "path": "$APPDATA/*" }],
  "deny": [{ "path": "$APPDATA/secrets/*" }]
}

The $APPDATA, $DOCUMENT, $HOME placeholders resolve to platform-appropriate paths at runtime.

Common Mistake: Adding tauri-plugin-fs to Cargo.toml but forgetting permissions: ["fs:default"] in the capability. The plugin loads, the API is callable, but every call returns “not allowed by ACL.”

Fix 3: Custom Commands Need Permissions Too

A #[tauri::command] function isn’t automatically callable from the frontend — you must register it as a permission in your app’s manifest:

#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

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

For the command to be callable from the frontend, add it to the capability’s permissions. The identifier is <plugin-or-app>:allow-<command-name>. For app-level commands, the convention is to reference them via the app’s permission set generated under src-tauri/permissions/:

// src-tauri/permissions/default.toml
"$schema" = "schemas/schema.json"

[default]
description = "Allow app commands"
permissions = ["allow-greet"]

Then in the capability:

"permissions": [
  "core:default",
  "<your-app-identifier>:allow-greet"
]

The exact identifier depends on your app’s bundle identifier. Run cargo tauri build once to generate the schemas and inspect src-tauri/gen/schemas/.

Pro Tip: Start with broad core:default while developing, then narrow as you go. Don’t try to write a tight ACL on day one — you’ll fight the system instead of building features.

Fix 4: Migrate tauri.conf.json Schema

v1 → v2 config structure changed significantly:

// v1 (does NOT work in v2):
{
  "tauri": {
    "allowlist": {
      "all": false,
      "dialog": { "open": true }
    },
    "windows": [{ "title": "..." }],
    "bundle": { ... }
  }
}

// v2:
{
  "$schema": "../node_modules/@tauri-apps/cli/schema.json",
  "productName": "My App",
  "version": "0.1.0",
  "identifier": "com.example.myapp",
  "build": {
    "beforeDevCommand": "npm run dev",
    "beforeBuildCommand": "npm run build",
    "devUrl": "http://localhost:5173",
    "frontendDist": "../dist"
  },
  "app": {
    "windows": [{ "title": "My App", "width": 1200, "height": 800 }],
    "security": { "csp": null }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": ["icons/icon.icns", "icons/icon.ico"]
  }
}

Key changes:

  • Top-level tauri.* is gone. Sections moved to top-level: app, build, bundle.
  • allowlist removed — replaced by capabilities/*.json files (see Fix 1).
  • devPathdevUrl; distDirfrontendDist.
  • identifier (bundle ID like com.example.myapp) is now required at the top level.

The official migrate tool handles most of this. Install the Tauri 2 CLI, then run migrate from your project root:

# Via Cargo
cargo install tauri-cli --version "^2.0.0" --locked

# Or via npm (recommended for JS-heavy projects)
npm install -D @tauri-apps/cli@latest

cargo tauri migrate
# or: npx @tauri-apps/cli migrate

Run on a clean git branch, review the diff, fix anything migrate couldn’t infer.

Fix 5: Mobile Setup (Android)

cargo tauri android init requires the Android SDK and NDK:

# Set environment variables (add to ~/.zshrc or ~/.bashrc):
export ANDROID_HOME=$HOME/Library/Android/sdk        # macOS
export ANDROID_HOME=$HOME/Android/Sdk                 # Linux
export ANDROID_HOME="$env:LOCALAPPDATA/Android/Sdk"  # Windows PowerShell
export NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125       # Use installed NDK version

# Install Rust targets:
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android

# Init:
cargo tauri android init
cargo tauri android dev

Install Android Studio first — it manages SDK/NDK versions through SDK Manager. The exact NDK version path changes with each release.

For iOS (macOS only):

xcode-select --install
rustup target add aarch64-apple-ios x86_64-apple-ios aarch64-apple-ios-sim

cargo tauri ios init
cargo tauri ios dev

Common Mistake: Trying mobile builds on Windows for iOS. iOS builds require Xcode and macOS. Use a Mac mini, GitHub Actions macOS runners, or a cloud-based macOS service.

Fix 6: Updater Plugin Migration

The updater is a plugin in v2:

# Cargo.toml
[dependencies]
tauri-plugin-updater = "2"
// lib.rs
use tauri_plugin_updater::UpdaterExt;

pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_updater::Builder::new().build())
        .setup(|app| {
            let handle = app.handle().clone();
            tauri::async_runtime::spawn(async move {
                if let Ok(Some(update)) = handle.updater().unwrap().check().await {
                    let _ = update.download_and_install(|_, _| {}, || {}).await;
                }
            });
            Ok(())
        })
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
// tauri.conf.json
{
  "plugins": {
    "updater": {
      "endpoints": ["https://releases.example.com/{{target}}/{{arch}}/{{current_version}}"],
      "pubkey": "..."
    }
  }
}

Permissions in the capability:

"permissions": [
  "updater:default",
  "updater:allow-check",
  "updater:allow-download-and-install"
]

The signature key flow is unchanged — generate with tauri signer generate and sign releases with the private key.

Fix 7: CSP and Webview Security

Tauri 2 enforces stricter CSP. The default is null (no enforcement) for dev convenience, but production builds should set one:

{
  "app": {
    "security": {
      "csp": "default-src 'self' tauri:; script-src 'self' tauri:; style-src 'self' 'unsafe-inline'"
    }
  }
}

tauri: is the scheme for Tauri’s IPC bridge. Without tauri: in script-src/default-src, the frontend can’t call commands.

For frontends that need unsafe-inline styles (most frameworks), include 'unsafe-inline' in style-src. For Vite dev mode, also add ws:// for HMR:

"csp": "default-src 'self' tauri: ws://localhost:1420; script-src 'self' tauri:; style-src 'self' 'unsafe-inline'"

Fix 8: IPC and Type-Safe Bindings

In v2, invoke lives in @tauri-apps/api/core:

import { invoke } from "@tauri-apps/api/core";

const result = await invoke<string>("greet", { name: "Alice" });

For type safety across many commands, use tauri-specta to generate TypeScript bindings from your Rust commands:

[dependencies]
specta = "2.0.0-rc"
tauri-specta = { version = "2.0.0-rc", features = ["typescript"] }
use specta::Type;
use tauri_specta::{collect_commands, Builder};

#[derive(Type, serde::Serialize)]
struct Greeting { message: String }

#[tauri::command]
#[specta::specta]
fn greet(name: String) -> Greeting {
    Greeting { message: format!("Hello, {}!", name) }
}

pub fn run() {
    let builder = Builder::<tauri::Wry>::new()
        .commands(collect_commands![greet]);
    
    #[cfg(debug_assertions)]
    builder.export(specta_typescript::Typescript::default(), "../src/bindings.ts").unwrap();

    tauri::Builder::default()
        .invoke_handler(builder.invoke_handler())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

src/bindings.ts is regenerated on each debug build. Import typed commands:

import { commands } from "./bindings";
const result = await commands.greet("Alice");  // Fully typed

Still Not Working?

A few less-obvious failures:

  • tauri dev runs but window is blank. Frontend dev server didn’t start, or devUrl doesn’t match. Check beforeDevCommand runs your dev server first.
  • CSP blocks IPC. Add 'self' tauri: to default-src/script-src. Without it, the frontend can’t invoke any command.
  • fs:allow-read-file works but reads fail. You also need a scope. Add a fs:scope-app permission that lists allowed paths.
  • Bundle is huge. Build with --release and strip debug symbols: add strip = true to [profile.release] in Cargo.toml.
  • Android: gradle build failed. Open the project in Android Studio once to let it fix Gradle plugin versions. Then go back to cargo tauri android build.
  • macOS: notarization fails after cargo tauri build. Set signingIdentity and configure bundle.macOS.providerShortName. Without notarization, users see “App is damaged” errors.
  • Cross-compiling from Linux to Windows: missing system libs. Use the official Tauri GitHub Actions workflow templates. Cross-compile is fragile; use OS-native runners.
  • Frontend works in browser but blank in Tauri. Hard-coded localhost:5173 URLs in your code don’t work — Tauri serves from tauri://localhost. Use relative paths.

For related desktop and cross-platform issues, see Tauri not working, Electron not working, Rust trait not implemented, and Vite failed to resolve import.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles