Tauri 2.x Plugin Ecosystem

SkillAI & models

Tauri 2.x official plugin ecosystem, plugin APIs, permissions, and custom plugin development

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Tauri 2.x Plugin Ecosystem skill

What this skill tells your AI

The instructions your AI receives, as published by agents-inc/skills in src/skills/desktop-plugins-tauri/SKILL.md and read by ahel’s review.

Quick Guide: Tauri plugins follow a dual-install pattern: Cargo crate (Rust backend) + npm package (JS frontend). Every plugin must be registered with .plugin() in Rust AND have permissions granted in a capability file. Missing any step causes runtime errors, not compile errors. Custom plugins use tauri::plugin::Builder with optional mobile support (Swift/Kotlin). There are 30+ official plugins covering fs, http, dialog, store, notification, shell, updater, sql, log, stronghold, deep-link, global-shortcut, and more.

Current version: Tauri 2.x (stable). All plugins require Rust 1.77.2+.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST complete ALL four installation steps for every plugin: 1) cargo add crate, 2) npm install bindings, 3) .plugin() registration in Rust, 4) permissions in capability file -- missing any step causes runtime errors)

(You MUST scope plugin permissions in capability files -- never grant unscoped fs:allow-read-text-file or http:default without URL restrictions)

(You MUST use @tauri-apps/plugin-* npm packages for JS bindings -- not @tauri-apps/api/* which is the core API)

(You MUST use #[cfg(desktop)] guard when registering desktop-only plugins -- mobile builds will fail otherwise)

(You MUST use tauri::plugin::Builder with an init() convention when creating custom plugins -- not raw command registration)

</critical_requirements>


Auto-detection: tauri-plugin, @tauri-apps/plugin, tauri_plugin, plugin registration, .plugin(), tauri-plugin-fs, tauri-plugin-http, tauri-plugin-store, tauri-plugin-dialog, tauri-plugin-notification, tauri-plugin-shell, tauri-plugin-updater, tauri-plugin-log, tauri-plugin-sql, tauri-plugin-stronghold, tauri-plugin-deep-link, tauri-plugin-global-shortcut, tauri-plugin-autostart, tauri-plugin-clipboard-manager, tauri-plugin-window-state, tauri-plugin-single-instance, tauri-plugin-barcode-scanner, tauri-plugin-biometric, tauri-plugin-os, tauri-plugin-process, custom plugin, plugin development, npx tauri plugin new

When to use:

  • Installing and configuring official Tauri plugins
  • Using plugin JavaScript APIs from the frontend
  • Scoping plugin permissions in capability files
  • Creating custom plugins with Rust backend + optional JS API
  • Adding mobile support (Swift/Kotlin) to custom plugins
  • Choosing between plugins for a specific use case (store vs stronghold, fs vs dialog)

When NOT to use:

  • Tauri core framework patterns (commands, invoke, state, events, tray, windows -- use the framework skill)
  • Frontend framework patterns (component architecture, state management -- use respective framework skills)
  • General Rust programming not related to Tauri plugin APIs
  • Build tool or bundler configuration (separate tooling concern)

Key patterns covered:

Detailed resources:


Philosophy

Tauri plugins extend the core framework with native capabilities through a dual-architecture design: a Rust backend crate providing the implementation, and an npm package providing typed JavaScript bindings. This separation enforces security -- every plugin operation must be explicitly permitted in a capability file.

Plugin architecture principles:

  • Security by default: Plugins do nothing until permissions are granted. Permissions are scoped per-window and can restrict operations to specific paths, URLs, or commands.
  • Dual install: Rust crate handles native operations; npm package provides the typed JS API. Both are required.
  • Platform awareness: Some plugins are desktop-only (shell, autostart, global-shortcut), some are mobile-only (barcode-scanner, biometric, haptics), and many work on both.
  • Convention over configuration: All official plugins follow the same four-step install pattern. Custom plugins use tauri::plugin::Builder with an init() export.

When to use plugins vs custom commands:

  • Need file system, HTTP, notifications, or other OS features? Use the official plugin.
  • Need custom business logic that runs in Rust? Write a Tauri command (framework skill).
  • Need a reusable native capability shared across projects? Write a custom plugin.

When NOT to use a plugin:

  • The JS Web API already covers the need (e.g., navigator.clipboard for simple text copy in some contexts)
  • A custom Tauri command is simpler for a one-off operation
  • The plugin is mobile-only but your app is desktop-only (or vice versa)

Core Patterns

Pattern 1: Four-Step Plugin Installation

Every official plugin requires exactly four steps. Missing any step causes runtime errors, not compile errors.

# Step 1: Add Rust crate
cargo add tauri-plugin-store

# Step 2: Add JS bindings
npm add @tauri-apps/plugin-store

# Step 3: Register plugin in Rust (src-tauri/src/lib.rs)
# .plugin(tauri_plugin_store::Builder::new().build())

# Step 4: Add permissions to capability file (src-tauri/capabilities/main.json)
# "store:default"

Why all four steps: Cargo crate = backend implementation, npm package = typed JS bindings, .plugin() = runtime activation, capability permission = frontend authorization. Any missing piece causes a runtime error with an unhelpful message.

Shortcut: cargo tauri add <plugin> handles steps 1 and 3 automatically. You still need npm install (step 2) and permissions (step 4).

See examples/core.md for multi-plugin registration and permission scoping.


Pattern 2: Permission Scoping

Plugins operate under least-privilege. Scope permissions to specific paths, URLs, or commands.

{
  "permissions": [
    "core:default",
    {
      "identifier": "fs:allow-read-text-file",
      "allow": [{ "path": "$APPDATA/**" }]
    },
    {
      "identifier": "http:default",
      "allow": [{ "url": "https://api.example.com/**" }]
    }
  ]
}

Why scoping matters: Unscoped fs:allow-read-text-file grants access to ANY file on the system. Unscoped http:default allows requests to ANY domain. Always restrict to the minimum required scope.

See examples/core.md for shell command scoping and window-specific permissions.


Pattern 3: Desktop-Only Plugin Guard

Desktop-only plugins (shell, autostart, global-shortcut, single-instance, window-state, positioner) must be wrapped in #[cfg(desktop)] to prevent mobile build failures.

tauri::Builder::default()
    .setup(|app| {
        #[cfg(desktop)]
        {
            app.handle().plugin(tauri_plugin_autostart::init(
                tauri_plugin_autostart::MacosLauncher::LaunchAgent,
                None,
            ));
            app.handle().plugin(tauri_plugin_global_shortcut::Builder::new().build());
        }
        Ok(())
    })

Key point: Without #[cfg(desktop)], the Rust compiler will fail on mobile targets because these crates do not support iOS/Android.


Pattern 4: Store vs Stronghold vs SQL

Three storage plugins serve different needs:

PluginUse CaseEncryptionQueryPlatform
StoreApp preferences, settingsNoKey-value onlyAll
StrongholdSecrets, API keys, tokensYes (Argon2)Key-value onlyDesktop
SQLStructured data, relationsNo (app-level)Full SQL (SQLite/MySQL/Postgres)All

Decision: User preferences and simple config? Store. Sensitive credentials? Stronghold. Structured relational data? SQL.

See examples/data-storage.md for complete API examples for each.


Pattern 5: Updater with Signed Releases

The updater plugin requires cryptographic signatures -- this cannot be disabled. Updates check an endpoint, verify the signature, download, and install.

import { check } from "@tauri-apps/plugin-updater";
import { relaunch } from "@tauri-apps/plugin-process";

const update = await check();
if (update) {
  await update.downloadAndInstall((event) => {
    // event.event: "Started" | "Progress" | "Finished"
  });
  await relaunch();
}

Key point: Generate signing keys with cargo tauri signer generate. Set TAURI_SIGNING_PRIVATE_KEY during builds. The public key goes in tauri.conf.json. Losing the private key means you cannot ship updates to existing users.

See examples/lifecycle.md for endpoint JSON format and Rust API.


Pattern 6: Custom Plugin Development

Custom plugins use tauri::plugin::Builder with the init() convention.

use tauri::plugin::{Builder, TauriPlugin};
use tauri::Runtime;

#[tauri::command]
fn my_command() -> String {
    "Hello from plugin".into()
}

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("my-plugin")
        .invoke_handler(tauri::generate_handler![my_command])
        .setup(|app, _api| {
            // Initialize state, start background tasks
            Ok(())
        })
        .build()
}

Key points: Plugin commands are invoked as plugin:my-plugin|my_command from JS. Scaffold a full plugin project with npx @tauri-apps/cli plugin new <name>. The template includes desktop.rs, mobile.rs, permissions, and JS bindings.

See examples/custom-plugins.md for lifecycle hooks, configuration, and mobile support.


<decision_framework>

Decision Framework

Plugin Selection

What native capability do you need?
|
+-- File system read/write?
|   +-- tauri-plugin-fs (scoped to specific directories)
|
+-- File/folder picker dialog?
|   +-- tauri-plugin-dialog (open, save, message, ask)
|
+-- HTTP requests bypassing CORS?
|   +-- tauri-plugin-http (scope to specific domains)
|
+-- Persistent key-value storage?
|   +-- Sensitive data (tokens, keys)? -> tauri-plugin-stronghold
|   +-- App preferences/settings? -> tauri-plugin-store
|
+-- Relational/structured data?
|   +-- tauri-plugin-sql (SQLite, MySQL, PostgreSQL)
|
+-- System notifications?
|   +-- tauri-plugin-notification (check permissions first on macOS/mobile)
|
+-- Run external processes?
|   +-- tauri-plugin-shell (desktop only, scope allowed commands)
|
+-- Auto-update?
|   +-- tauri-plugin-updater (requires signed releases)
|
+-- Structured logging?
|   +-- tauri-plugin-log (targets: stdout, file, webview)
|
+-- Custom URL scheme handling?
|   +-- tauri-plugin-deep-link (configure per-platform)
|
+-- System-wide keyboard shortcuts?
|   +-- tauri-plugin-global-shortcut (desktop only)
|
+-- Launch on system startup?
|   +-- tauri-plugin-autostart (desktop only)
|
+-- Single app instance?
|   +-- tauri-plugin-single-instance (desktop only)
|
+-- Remember window position/size?
|   +-- tauri-plugin-window-state (desktop only)
|
+-- Clipboard access?
|   +-- tauri-plugin-clipboard-manager
|
+-- Mobile camera/scanner?
|   +-- tauri-plugin-barcode-scanner (mobile only)
|
+-- Biometric auth?
|   +-- tauri-plugin-biometric (mobile only)
|
+-- OS/platform info?
|   +-- tauri-plugin-os
|
+-- App restart/exit?
    +-- tauri-plugin-process

Custom Plugin vs Custom Command

Is this a reusable capability shared across projects?
+-- YES -> Custom plugin (npx @tauri-apps/cli plugin new)
+-- NO  -> Is it complex enough to need its own permission model?
    +-- YES -> Custom plugin
    +-- NO  -> Regular Tauri command (simpler, framework skill)

</decision_framework>


<red_flags>

RED FLAGS

High Priority Issues:

  • Missing any of the four installation steps (cargo, npm, .plugin(), permissions) -- causes runtime error with unhelpful message
  • Unscoped filesystem permissions (fs:allow-read-text-file without path restriction) -- grants access to entire filesystem
  • Unscoped HTTP permissions (http:default without URL pattern) -- allows requests to any domain
  • Unscoped shell execute (shell:allow-execute without command allowlist) -- allows running arbitrary commands
  • Using @tauri-apps/api/* imports for plugin functionality -- plugins use @tauri-apps/plugin-* packages
  • Registering desktop-only plugins without #[cfg(desktop)] -- breaks mobile builds
  • Losing the updater signing private key -- makes shipping updates to existing users impossible

Medium Priority Issues:

  • Using Store plugin for sensitive data (API keys, tokens) -- Store is NOT encrypted, use Stronghold
  • Not checking isPermissionGranted() before sending notifications on macOS/mobile
  • Granting shell:allow-execute when only shell:allow-open (URLs/files) is needed
  • Missing sql:allow-execute permission (default only includes read operations)
  • Forgetting to call stronghold.save() after modifications (changes are lost)

Common Mistakes:

  • Installing the cargo crate but forgetting the npm package (or vice versa)
  • Using cargo tauri add and assuming all four steps are done (npm install and permissions still needed)
  • Not scoping HTTP plugin URLs -- allows the app to make requests to arbitrary servers
  • Using the updater plugin on mobile (it is desktop-only)
  • Expecting Store data to persist across app reinstalls (store location depends on app identifier)

Gotchas & Edge Cases:

  • Plugin init variants: Some plugins use .init() (fs, dialog, shell, notification), others use Builder::new().build() (store, updater, global-shortcut, log) -- check each plugin's docs
  • Store autoSave: When autoSave: false, you must call store.save() manually. When autoSave is a number, it debounces saves by that many milliseconds.
  • SQL default permissions: Only read operations (select, load, close) are granted by default -- sql:allow-execute must be added explicitly for INSERT/UPDATE/DELETE
  • Stronghold platform: Desktop-only. Store data as Uint8Array (not strings) -- use TextEncoder/TextDecoder for string conversion
  • Deep link desktop: On desktop, deep links arrive as command-line arguments. Combine with single-instance plugin to handle links when the app is already running.
  • Global shortcut conflicts: Registering a shortcut already bound system-wide (e.g., Ctrl+C) silently fails or overrides the system binding depending on the OS
  • Window-state plugin: Automatically restores window position/size on startup with zero JS code needed -- just register the plugin
  • Plugin registration order: Does not matter. Each .plugin() call is independent.

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST complete ALL four installation steps for every plugin: 1) cargo add crate, 2) npm install bindings, 3) .plugin() registration in Rust, 4) permissions in capability file -- missing any step causes runtime errors)

(You MUST scope plugin permissions in capability files -- never grant unscoped fs:allow-read-text-file or http:default without URL restrictions)

(You MUST use @tauri-apps/plugin-* npm packages for JS bindings -- not @tauri-apps/api/* which is the core API)

(You MUST use #[cfg(desktop)] guard when registering desktop-only plugins -- mobile builds will fail otherwise)

(You MUST use tauri::plugin::Builder with an init() convention when creating custom plugins -- not raw command registration)

Failure to follow these rules will cause silent runtime errors, security vulnerabilities from unscoped permissions, or broken mobile builds.

</critical_reminders>

Signals

GitHub stars
24
Forks
8
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
desktop-plugins-tauri
Source
github.com/agents-inc/skills