Claude Skill

desktop-plugins-tauri

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_desktop-plugins-tauri_skills_desktop-plugins-tauri-3a51ef5.zip · 27 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/desktop-plugins-tauri/skills/desktop-plugins-tauri
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Tauri 2.x Plugin Ecosystem

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:




<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>

Files (skills)
  • examples
    • core.md 6 KB
      # Tauri Plugins - Core Patterns
      
      > Installation pattern, permission scoping, multi-plugin registration. See [SKILL.md](../SKILL.md) for decision frameworks and red flags. See [reference.md](../reference.md) for the full plugin registry.
      
      ---
      
      ## Four-Step Installation Pattern
      
      Every official plugin follows the same four steps. Missing any step causes runtime errors (not compile errors).
      
      ```sh
      # Step 1: Add the Rust crate
      cargo add tauri-plugin-fs
      
      # Step 2: Add the JS bindings
      npm add @tauri-apps/plugin-fs
      
      # Step 3: Register the plugin in Rust (src-tauri/src/lib.rs)
      ```
      
      ```rust
      // src-tauri/src/lib.rs
      #[cfg_attr(mobile, tauri::mobile_entry_point)]
      pub fn run() {
          tauri::Builder::default()
              .plugin(tauri_plugin_fs::init())  // Step 3
              .run(tauri::generate_context!())
              .expect("error while running tauri application");
      }
      ```
      
      ```json
      // Step 4: Add permissions to capability file (src-tauri/capabilities/main.json)
      {
        "$schema": "../gen/schemas/desktop-schema.json",
        "identifier": "main-capability",
        "description": "Capability for the main window",
        "windows": ["main"],
        "permissions": ["core:default", "fs:default"]
      }
      ```
      
      **Why all four steps:** The Rust crate provides backend functionality, the npm package provides typed JS bindings, `.plugin()` activates the plugin at runtime, and the capability permission authorizes the frontend to invoke plugin commands.
      
      **Shortcut:** `cargo tauri add <plugin-name>` handles steps 1 and 3 (Cargo crate + Rust registration). You still need npm install (step 2) and capability permissions (step 4).
      
      ---
      
      ## Permission Scoping
      
      ### Scoped Filesystem Access
      
      ```json
      {
        "permissions": [
          {
            "identifier": "fs:allow-read-text-file",
            "allow": [{ "path": "$APPDATA/**" }]
          },
          {
            "identifier": "fs:allow-write-text-file",
            "allow": [{ "path": "$APPDATA/**" }]
          }
        ]
      }
      ```
      
      **Why good:** Restricts read/write to the app data directory only. Without scoping, `fs:allow-read-text-file` grants access to the entire filesystem.
      
      ### Scoped HTTP Access
      
      ```json
      {
        "permissions": [
          {
            "identifier": "http:default",
            "allow": [{ "url": "https://api.example.com/**" }]
          }
        ]
      }
      ```
      
      **Why good:** Restricts HTTP requests to a specific domain. Without scoping, the app could make requests to any server.
      
      ### Scoped Shell Execution
      
      ```json
      {
        "permissions": [
          "shell:allow-open",
          {
            "identifier": "shell:allow-execute",
            "allow": [
              {
                "name": "run-git-status",
                "cmd": "git",
                "args": ["status"],
                "sidecar": false
              }
            ]
          }
        ]
      }
      ```
      
      **Why good:** Only allows running `git status` specifically. Never grant unscoped `shell:allow-execute` -- it allows running any command on the system.
      
      ---
      
      ## Multi-Plugin Registration
      
      ```rust
      #[cfg_attr(mobile, tauri::mobile_entry_point)]
      pub fn run() {
          tauri::Builder::default()
              // Cross-platform plugins
              .plugin(tauri_plugin_fs::init())
              .plugin(tauri_plugin_http::init())
              .plugin(tauri_plugin_store::Builder::new().build())
              .plugin(tauri_plugin_notification::init())
              .plugin(tauri_plugin_process::init())
              .plugin(tauri_plugin_os::init())
              .plugin(tauri_plugin_log::Builder::new().build())
              .setup(|app| {
                  // Desktop-only plugins must be guarded
                  #[cfg(desktop)]
                  {
                      app.handle().plugin(tauri_plugin_shell::init())?;
                      app.handle().plugin(
                          tauri_plugin_autostart::init(
                              tauri_plugin_autostart::MacosLauncher::LaunchAgent,
                              None,
                          )
                      );
                      app.handle().plugin(
                          tauri_plugin_global_shortcut::Builder::new().build()
                      )?;
                      app.handle().plugin(
                          tauri_plugin_updater::Builder::new().build()
                      )?;
                      app.handle().plugin(
                          tauri_plugin_window_state::Builder::default().build()
                      )?;
                  }
                  Ok(())
              })
              .invoke_handler(tauri::generate_handler![/* your commands */])
              .run(tauri::generate_context!())
              .expect("error while running tauri application");
      }
      ```
      
      **Key points:**
      
      - Plugin registration order does not matter
      - Cross-platform plugins register directly on the Builder
      - Desktop-only plugins register inside `setup()` with `#[cfg(desktop)]` guard
      - Use `app.handle().plugin()` inside `setup()` (not the builder chain)
      
      ---
      
      ## Capability File with Multiple Plugins
      
      ```json
      {
        "$schema": "../gen/schemas/desktop-schema.json",
        "identifier": "main-capability",
        "description": "Main window permissions",
        "windows": ["main"],
        "permissions": [
          "core:default",
          "fs:default",
          "store:default",
          "notification:default",
          "process:default",
          "os:default",
          "log:default",
          "shell:allow-open",
          "clipboard-manager:default",
          "updater:default",
          "autostart:allow-enable",
          "autostart:allow-disable",
          "autostart:allow-is-enabled",
          "global-shortcut:allow-register",
          "global-shortcut:allow-unregister",
          "global-shortcut:allow-is-registered",
          "window-state:default",
          {
            "identifier": "fs:allow-read-text-file",
            "allow": [{ "path": "$APPDATA/**" }]
          },
          {
            "identifier": "fs:allow-write-text-file",
            "allow": [{ "path": "$APPDATA/**" }]
          },
          {
            "identifier": "http:default",
            "allow": [{ "url": "https://api.example.com/**" }]
          }
        ]
      }
      ```
      
      **Key points:**
      
      - Use `:default` permission sets for safe defaults
      - Add granular `allow-*` permissions for specific operations
      - Scope filesystem and HTTP permissions to specific paths/URLs
      - Permissions are per-window -- secondary windows need their own capability or must be listed in `windows`
      
      ---
      
      See [data-storage.md](data-storage.md) for fs, store, sql, stronghold APIs. See [system.md](system.md) for shell, notification, clipboard, dialog APIs. See [lifecycle.md](lifecycle.md) for updater, deep-link, autostart, global-shortcut APIs.
      
    • custom-plugins.md 10.5 KB
      # Tauri Plugins - Custom Plugin Development
      
      > Creating custom plugins with the Builder pattern, commands, configuration, lifecycle hooks, and mobile support (Swift/Kotlin). See [core.md](core.md) for official plugin patterns. See [reference.md](../reference.md) for the full plugin registry.
      
      ---
      
      ## Scaffolding a New Plugin
      
      ```sh
      # Scaffold a plugin project (creates tauri-plugin-<name>/ directory)
      npx @tauri-apps/cli plugin new my-feature
      
      # With mobile support
      npx @tauri-apps/cli plugin new my-feature --android --ios
      
      # Without JS bindings (Rust-only plugin)
      npx @tauri-apps/cli plugin new my-feature --no-api
      ```
      
      ### Generated Project Structure
      
      ```
      tauri-plugin-my-feature/
        src/
          lib.rs           # Plugin entry point (init function, re-exports)
          commands.rs      # Command definitions
          desktop.rs       # Desktop-specific implementation
          mobile.rs        # Mobile-specific implementation (if --android/--ios)
          error.rs         # Error types
          models.rs        # Shared data structures
        permissions/       # Permission definitions (TOML)
        guest-js/          # TypeScript API bindings
          index.ts
        android/           # Kotlin code (if --android)
        ios/               # Swift code (if --ios)
        build.rs           # Auto-generates permission files
        Cargo.toml
        package.json
      ```
      
      ---
      
      ## Basic Plugin with Commands
      
      ### Plugin Entry Point (src/lib.rs)
      
      ```rust
      use tauri::plugin::{Builder, TauriPlugin};
      use tauri::Runtime;
      
      mod commands;
      
      pub fn init<R: Runtime>() -> TauriPlugin<R> {
          Builder::new("my-feature")
              .invoke_handler(tauri::generate_handler![
                  commands::get_status,
                  commands::process_data,
              ])
              .setup(|app, _api| {
                  // Initialize plugin state
                  app.manage(PluginState::default());
                  Ok(())
              })
              .build()
      }
      ```
      
      ### Command Definitions (src/commands.rs)
      
      ```rust
      use tauri::{command, AppHandle, Runtime, State};
      use crate::PluginState;
      
      #[command]
      pub async fn get_status<R: Runtime>(
          _app: AppHandle<R>,
          state: State<'_, PluginState>,
      ) -> Result<String, String> {
          let status = state.status.lock().unwrap();
          Ok(status.clone())
      }
      
      #[command]
      pub async fn process_data<R: Runtime>(
          _app: AppHandle<R>,
          input: String,
      ) -> Result<String, String> {
          // Process the input
          Ok(format!("Processed: {input}"))
      }
      ```
      
      ### JavaScript Bindings (guest-js/index.ts)
      
      ```typescript
      import { invoke } from "@tauri-apps/api/core";
      
      export async function getStatus(): Promise<string> {
        return invoke<string>("plugin:my-feature|get_status");
      }
      
      export async function processData(input: string): Promise<string> {
        return invoke<string>("plugin:my-feature|process_data", { input });
      }
      ```
      
      **Key points:**
      
      - Plugin commands are invoked as `plugin:<plugin-name>|<command_name>` from JS
      - The `<R: Runtime>` generic allows the plugin to work in both desktop and mobile contexts
      - Export an `init()` function following the Tauri plugin convention
      - The plugin name in `Builder::new("my-feature")` must match the `plugin:my-feature|` prefix in JS invocations
      
      ---
      
      ## Plugin with Configuration
      
      ### Typed Config (src/lib.rs)
      
      ```rust
      use serde::Deserialize;
      use tauri::plugin::{Builder, TauriPlugin};
      use tauri::Runtime;
      
      const DEFAULT_TIMEOUT_MS: usize = 30_000;
      const DEFAULT_MAX_RETRIES: usize = 3;
      
      #[derive(Deserialize)]
      pub struct Config {
          #[serde(default = "default_timeout")]
          timeout: usize,
          #[serde(default = "default_max_retries")]
          max_retries: usize,
      }
      
      fn default_timeout() -> usize { DEFAULT_TIMEOUT_MS }
      fn default_max_retries() -> usize { DEFAULT_MAX_RETRIES }
      
      pub fn init<R: Runtime>() -> TauriPlugin<R, Config> {
          Builder::<R, Config>::new("my-feature")
              .setup(|app, api| {
                  let timeout = api.config().timeout;
                  let max_retries = api.config().max_retries;
                  app.manage(PluginConfig { timeout, max_retries });
                  Ok(())
              })
              .build()
      }
      ```
      
      ### Host App Configuration (tauri.conf.json)
      
      ```json
      {
        "plugins": {
          "my-feature": {
            "timeout": 5000,
            "maxRetries": 5
          }
        }
      }
      ```
      
      **Key points:**
      
      - Config is deserialized from the host app's `tauri.conf.json` at runtime
      - Use `#[serde(default)]` for optional fields with default values
      - The config key in JSON must match the plugin name
      
      ---
      
      ## Plugin Lifecycle Hooks
      
      ```rust
      use tauri::plugin::Builder;
      use tauri::{RunEvent, Runtime};
      
      pub fn init<R: Runtime>() -> TauriPlugin<R> {
          Builder::new("my-feature")
              .setup(|app, _api| {
                  // Called when plugin is initialized
                  // Register state, start background tasks
                  Ok(())
              })
              .on_navigation(|window, url| {
                  // Called when webview navigates
                  // Return false to cancel navigation
                  println!("Window {} navigating to {}", window.label(), url);
                  url.scheme() != "forbidden"
              })
              .on_webview_ready(|window| {
                  // Called when a new webview is created
                  // Execute initialization scripts
                  window.listen("content-loaded", |_event| {
                      println!("Webview content loaded");
                  });
              })
              .on_event(|_app, event| {
                  // Called on event loop events
                  match event {
                      RunEvent::ExitRequested { api, .. } => {
                          // Prevent exit if needed
                          api.prevent_exit();
                      }
                      RunEvent::Exit => {
                          // Final cleanup
                      }
                      _ => {}
                  }
              })
              .on_drop(|_app| {
                  // Called when plugin is destroyed
                  // Cleanup resources
              })
              .build()
      }
      ```
      
      **Key points:**
      
      - `setup`: Runs once when the plugin initializes -- register state and start background tasks here
      - `on_navigation`: Runs before each webview navigation -- return `false` to block
      - `on_webview_ready`: Runs when a new webview is created
      - `on_event`: Receives all event loop events (exit, window events, menu events)
      - `on_drop`: Runs when the plugin is destroyed -- cleanup resources
      
      ---
      
      ## Plugin Permissions
      
      ### Defining Permissions (permissions/default.toml)
      
      ```toml
      [[permission]]
      identifier = "allow-get-status"
      description = "Allows reading the plugin status"
      commands.allow = ["get_status"]
      
      [[permission]]
      identifier = "allow-process-data"
      description = "Allows processing data through the plugin"
      commands.allow = ["process_data"]
      
      [[set]]
      identifier = "default"
      description = "Default permissions for my-feature plugin"
      permissions = ["allow-get-status"]
      ```
      
      ### Auto-Generating Permissions (build.rs)
      
      ```rust
      const COMMANDS: &[&str] = &["get_status", "process_data"];
      
      fn main() {
          tauri_plugin::Builder::new(COMMANDS).build();
      }
      ```
      
      This generates `allow-get-status`, `deny-get-status`, `allow-process-data`, `deny-process-data` permissions automatically.
      
      ### Using in Host App
      
      ```json
      {
        "permissions": ["my-feature:default", "my-feature:allow-process-data"]
      }
      ```
      
      ---
      
      ## Mobile Plugin Support
      
      ### Rust: Desktop vs Mobile Split
      
      ```rust
      // src/desktop.rs
      use tauri::{AppHandle, Runtime};
      
      pub struct MyFeature<R: Runtime>(AppHandle<R>);
      
      impl<R: Runtime> MyFeature<R> {
          pub fn get_battery_level(&self) -> crate::Result<f64> {
              // Desktop implementation (may use system APIs or return default)
              Ok(1.0) // Desktop always reports "full"
          }
      }
      
      // src/mobile.rs
      use serde::de::DeserializeOwned;
      use tauri::{plugin::PluginHandle, Runtime};
      
      pub struct MyFeature<R: Runtime>(PluginHandle<R>);
      
      impl<R: Runtime> MyFeature<R> {
          pub fn get_battery_level(&self) -> crate::Result<f64> {
              // Calls native mobile code via PluginHandle
              self.0
                  .run_mobile_plugin::<BatteryResponse>("getBatteryLevel", ())
                  .map(|r| r.level)
                  .map_err(Into::into)
          }
      }
      ```
      
      ### Android Plugin (Kotlin)
      
      ```kotlin
      package com.plugin.myfeature
      
      import android.app.Activity
      import app.tauri.annotation.Command
      import app.tauri.annotation.InvokeArg
      import app.tauri.annotation.TauriPlugin
      import app.tauri.plugin.Invoke
      import app.tauri.plugin.JSObject
      import app.tauri.plugin.Plugin
      
      @TauriPlugin
      class MyFeaturePlugin(private val activity: Activity) : Plugin(activity) {
          @Command
          fun getBatteryLevel(invoke: Invoke) {
              val batteryManager = activity.getSystemService(
                  android.content.Context.BATTERY_SERVICE
              ) as android.os.BatteryManager
      
              val level = batteryManager.getIntProperty(
                  android.os.BatteryManager.BATTERY_PROPERTY_CAPACITY
              )
      
              val result = JSObject()
              result.put("level", level / 100.0)
              invoke.resolve(result)
          }
      }
      ```
      
      **Key points for Android:**
      
      - `@TauriPlugin` annotation marks the plugin class
      - `@Command` marks methods callable from Rust/JS
      - Commands run on the main thread -- use coroutines for blocking I/O
      - `invoke.resolve(JSObject)` sends data back to Rust
      - `invoke.reject(errorMessage)` sends an error
      
      ### iOS Plugin (Swift)
      
      ```swift
      import SwiftRs
      import Tauri
      import UIKit
      import WebKit
      
      class MyFeaturePlugin: Plugin {
          @objc public func getBatteryLevel(_ invoke: Invoke) throws {
              UIDevice.current.isBatteryMonitoringEnabled = true
              let level = UIDevice.current.batteryLevel
      
              invoke.resolve(["level": Double(level)])
          }
      }
      ```
      
      **Key points for iOS:**
      
      - Plugin class extends `Plugin`
      - `@objc` attribute + `Invoke` parameter = callable from Rust
      - `invoke.resolve([String: Any])` sends data back
      - `invoke.reject(errorMessage)` sends an error
      
      ### Plugin Events from Mobile
      
      ```kotlin
      // Android: emit event to frontend
      trigger("battery-change", JSObject().apply {
          put("level", newLevel)
      })
      ```
      
      ```swift
      // iOS: emit event to frontend
      trigger("battery-change", data: ["level": newLevel])
      ```
      
      ```typescript
      // Frontend: listen for plugin events
      import { addPluginListener } from "@tauri-apps/api/core";
      
      const unlisten = await addPluginListener(
        "my-feature",
        "battery-change",
        (event: { level: number }) => {
          console.log(`Battery: ${event.level * 100}%`);
        },
      );
      ```
      
      ---
      
      ## Publishing a Custom Plugin
      
      ```sh
      # Build TypeScript bindings
      cd guest-js && npm run build
      
      # Publish to npm (JS bindings)
      npm publish
      
      # Publish to crates.io (Rust crate)
      cargo publish
      ```
      
      **Naming conventions:**
      
      - Cargo crate: `tauri-plugin-<name>`
      - NPM package: `@scope/plugin-<name>` (scope recommended) or `tauri-plugin-<name>`
      - Plugin identifier (in Builder): `<name>` (without the `tauri-plugin-` prefix)
      
      ---
      
      See [core.md](core.md) for the four-step installation pattern that consumers use. See [reference.md](../reference.md) for the full plugin registry.
      
    • data-storage.md 8.6 KB
      # Tauri Plugins - Data & Storage
      
      > File system, store, SQL, and Stronghold plugin APIs. See [core.md](core.md) for installation and permission patterns. See [reference.md](../reference.md) for the full plugin registry.
      
      ---
      
      ## File System Plugin
      
      ### Reading and Writing Files
      
      ```typescript
      import {
        readTextFile,
        writeTextFile,
        readDir,
        mkdir,
        exists,
        remove,
        BaseDirectory,
      } from "@tauri-apps/plugin-fs";
      
      // Read from app data directory
      const content = await readTextFile("config.json", {
        baseDir: BaseDirectory.AppData,
      });
      
      // Write to app data directory
      await writeTextFile("config.json", JSON.stringify(data, null, 2), {
        baseDir: BaseDirectory.AppData,
      });
      
      // Check if file exists
      const fileExists = await exists("config.json", {
        baseDir: BaseDirectory.AppData,
      });
      
      // Create directory
      await mkdir("exports", { baseDir: BaseDirectory.AppData, recursive: true });
      
      // List directory contents
      const entries = await readDir("exports", {
        baseDir: BaseDirectory.AppData,
      });
      
      // Remove file
      await remove("old-config.json", { baseDir: BaseDirectory.AppData });
      ```
      
      **Key points:**
      
      - Always use `BaseDirectory` enum for portable paths across platforms
      - Always scope filesystem permissions to specific directories in the capability file
      - Binary files use `readFile` / `writeFile` (returns/accepts `Uint8Array`)
      
      ### Capability Permissions
      
      ```json
      {
        "permissions": [
          "fs:default",
          {
            "identifier": "fs:allow-read-text-file",
            "allow": [{ "path": "$APPDATA/**" }]
          },
          {
            "identifier": "fs:allow-write-text-file",
            "allow": [{ "path": "$APPDATA/**" }]
          },
          {
            "identifier": "fs:allow-exists",
            "allow": [{ "path": "$APPDATA/**" }]
          },
          {
            "identifier": "fs:allow-mkdir",
            "allow": [{ "path": "$APPDATA/**" }]
          },
          {
            "identifier": "fs:allow-read-dir",
            "allow": [{ "path": "$APPDATA/**" }]
          }
        ]
      }
      ```
      
      ---
      
      ## Store Plugin (Persistent Key-Value)
      
      ### JavaScript API
      
      ```typescript
      import { Store } from "@tauri-apps/plugin-store";
      
      const STORE_FILE = "settings.json";
      
      // Load or create a store (persisted as JSON in app data)
      const store = await Store.load(STORE_FILE, { autoSave: true });
      
      // Set values (supports any serializable type)
      await store.set("theme", "dark");
      await store.set("windowSize", { width: 1024, height: 768 });
      await store.set("recentFiles", ["/path/one.txt", "/path/two.txt"]);
      
      // Get values (typed)
      const theme = await store.get<string>("theme");
      const size = await store.get<{ width: number; height: number }>("windowSize");
      
      // Check existence
      const hasTheme = await store.has("theme");
      
      // Delete a key
      await store.delete("theme");
      
      // Iterate
      const keys = await store.keys();
      const values = await store.values();
      const entries = await store.entries<string>();
      
      // Clear all data
      await store.clear();
      
      // Manual save (only needed if autoSave is false)
      await store.save();
      ```
      
      ### LazyStore (Loads on First Access)
      
      ```typescript
      import { LazyStore } from "@tauri-apps/plugin-store";
      
      // LazyStore defers loading until the first get/set call
      const store = new LazyStore("settings.json");
      ```
      
      ### Rust API
      
      ```rust
      use tauri::Manager;
      use serde_json::json;
      
      fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
          let store = app.store("store.json")?;
          store.set("some-key", json!({ "value": 5 }));
          let value = store.get("some-key");
          Ok(())
      }
      ```
      
      **Key points:**
      
      - Store persists as a JSON file in the app data directory
      - `autoSave: true` saves after every `.set()`. `autoSave: false` requires manual `.save()`. A number value debounces saves by that many milliseconds.
      - Store data is NOT encrypted -- use Stronghold for sensitive data
      - Permissions: `"store:default"` grants all operations
      
      ---
      
      ## SQL Plugin (SQLite / MySQL / PostgreSQL)
      
      ### Installation with Feature Flag
      
      ```sh
      # Choose your database engine
      cargo add tauri-plugin-sql --features sqlite
      # Or: --features mysql
      # Or: --features postgres
      ```
      
      ### Rust Setup with Migrations
      
      ```rust
      use tauri_plugin_sql::{Builder, Migration, MigrationKind};
      
      let migrations = vec![
          Migration {
              version: 1,
              description: "create_users_table",
              sql: "CREATE TABLE IF NOT EXISTS users (
                  id INTEGER PRIMARY KEY AUTOINCREMENT,
                  name TEXT NOT NULL,
                  email TEXT UNIQUE NOT NULL,
                  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
              );",
              kind: MigrationKind::Up,
          },
          Migration {
              version: 2,
              description: "create_todos_table",
              sql: "CREATE TABLE IF NOT EXISTS todos (
                  id INTEGER PRIMARY KEY AUTOINCREMENT,
                  user_id INTEGER NOT NULL,
                  title TEXT NOT NULL,
                  completed BOOLEAN DEFAULT FALSE,
                  FOREIGN KEY (user_id) REFERENCES users(id)
              );",
              kind: MigrationKind::Up,
          },
      ];
      
      tauri::Builder::default()
          .plugin(
              Builder::default()
                  .add_migrations("sqlite:app.db", migrations)
                  .build(),
          )
      ```
      
      ### JavaScript API
      
      ```typescript
      import Database from "@tauri-apps/plugin-sql";
      
      // Connect (SQLite creates the file automatically)
      const db = await Database.load("sqlite:app.db");
      
      // INSERT (SQLite uses $1, $2 placeholders)
      await db.execute("INSERT INTO users (name, email) VALUES ($1, $2)", [
        "Alice",
        "alice@example.com",
      ]);
      
      // SELECT
      const users = await db.select<
        Array<{ id: number; name: string; email: string }>
      >("SELECT * FROM users WHERE name = $1", ["Alice"]);
      
      // UPDATE
      await db.execute("UPDATE users SET name = $1 WHERE id = $2", [
        "Alice Smith",
        1,
      ]);
      
      // DELETE
      await db.execute("DELETE FROM users WHERE id = $1", [1]);
      
      // Close connection
      await db.close();
      ```
      
      **Key points:**
      
      - Default permissions only include read operations -- add `"sql:allow-execute"` for INSERT/UPDATE/DELETE
      - Migrations run automatically when the database is loaded (either via `preload` config or `Database.load()`)
      - Migrations execute in a transaction -- failures cause complete rollback
      - SQLite and PostgreSQL use `$1, $2, $3` placeholders; MySQL uses `?, ?, ?`
      
      ### Permissions
      
      ```json
      {
        "permissions": ["sql:default", "sql:allow-execute"]
      }
      ```
      
      ---
      
      ## Stronghold Plugin (Encrypted Storage)
      
      ### Rust Setup (with Argon2 Hashing)
      
      ```rust
      pub fn run() {
          tauri::Builder::default()
              .plugin(tauri_plugin_stronghold::Builder::new(|password| {
                  use argon2::{hash_raw, Config, Variant, Version};
      
                  let config = Config {
                      lanes: 4,
                      mem_cost: 10_000,
                      time_cost: 10,
                      variant: Variant::Argon2id,
                      version: Version::Version13,
                      ..Default::default()
                  };
      
                  let salt = "your-salt".as_bytes();
                  let key = hash_raw(password.as_ref(), salt, &config)
                      .expect("failed to hash password");
                  key.to_vec()
              })
              .build())
              .run(tauri::generate_context!())
              .expect("error while running tauri application");
      }
      ```
      
      **Note:** Add `rust-argon2 = "2"` to your `src-tauri/Cargo.toml` dependencies.
      
      ### JavaScript API
      
      ```typescript
      import { Client, Stronghold } from "@tauri-apps/plugin-stronghold";
      import { appDataDir } from "@tauri-apps/api/path";
      
      // Initialize Stronghold
      const vaultPath = `${await appDataDir()}/vault.hold`;
      const stronghold = await Stronghold.load(vaultPath, "vault-password");
      
      // Create or load a client
      let client: Client;
      try {
        client = await stronghold.loadClient("my-client");
      } catch {
        client = await stronghold.createClient("my-client");
      }
      
      // Get the store
      const store = client.getStore();
      
      // Insert a secret (data must be Uint8Array)
      const encoder = new TextEncoder();
      await store.insert("api-key", Array.from(encoder.encode("sk-secret-value")));
      
      // Retrieve a secret
      const data = await store.get("api-key");
      const secret = new TextDecoder().decode(new Uint8Array(data));
      
      // Remove a record
      await store.remove("api-key");
      
      // IMPORTANT: Save changes to disk
      await stronghold.save();
      
      // Unload when done
      await stronghold.unload(vaultPath);
      ```
      
      **Key points:**
      
      - Desktop-only plugin (Windows, macOS, Linux)
      - Data is stored as `Uint8Array`, not strings -- use `TextEncoder`/`TextDecoder` for conversion
      - `stronghold.save()` must be called to persist changes to disk
      - `Builder::new()` takes a closure that hashes the vault password -- use a secure algorithm like Argon2
      - Add `rust-argon2` crate for the password hashing implementation
      - Permissions: `"stronghold:default"` grants all store operations
      
      ---
      
      See [system.md](system.md) for shell, notification, clipboard, dialog plugin APIs. See [lifecycle.md](lifecycle.md) for updater, autostart, deep-link APIs.
      
    • lifecycle.md 8.4 KB
      # Tauri Plugins - App Lifecycle
      
      > Updater, deep-link, autostart, single-instance, window-state, and global-shortcut plugin APIs. See [core.md](core.md) for installation and permission patterns. See [reference.md](../reference.md) for endpoint formats and platform support.
      
      ---
      
      ## Updater Plugin (Desktop Only)
      
      ### Rust Setup
      
      ```rust
      #[cfg_attr(mobile, tauri::mobile_entry_point)]
      pub fn run() {
          tauri::Builder::default()
              .setup(|app| {
                  #[cfg(desktop)]
                  app.handle().plugin(tauri_plugin_updater::Builder::new().build());
                  Ok(())
              })
              .run(tauri::generate_context!())
              .expect("error while running tauri application");
      }
      ```
      
      ### Configuration (tauri.conf.json)
      
      ```json
      {
        "bundle": {
          "createUpdaterArtifacts": true
        },
        "plugins": {
          "updater": {
            "pubkey": "CONTENT_FROM_PUBLIC_KEY_FILE",
            "endpoints": [
              "https://releases.example.com/{{target}}/{{arch}}/{{current_version}}"
            ]
          }
        }
      }
      ```
      
      **Endpoint variables:** `{{current_version}}`, `{{target}}` (linux/windows/darwin), `{{arch}}` (x86_64/aarch64).
      
      ### JavaScript API
      
      ```typescript
      import { check } from "@tauri-apps/plugin-updater";
      import { relaunch } from "@tauri-apps/plugin-process";
      
      const update = await check();
      if (update) {
        console.log(`Update to v${update.version} available: ${update.body}`);
      
        await update.downloadAndInstall((event) => {
          switch (event.event) {
            case "Started":
              console.log(`Downloading ${event.data.contentLength} bytes`);
              break;
            case "Progress":
              console.log(`Downloaded chunk: ${event.data.chunkLength} bytes`);
              break;
            case "Finished":
              console.log("Download complete");
              break;
          }
        });
      
        await relaunch();
      }
      ```
      
      ### Signing Keys
      
      ```sh
      # Generate signing keypair (do this once, keep private key safe)
      cargo tauri signer generate -w ~/.tauri/myapp.key
      
      # Set environment variables during builds
      export TAURI_SIGNING_PRIVATE_KEY="path/to/private.key"  # or key content
      export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="optional-password"
      ```
      
      **Key points:**
      
      - Signature validation is mandatory and cannot be disabled
      - Losing the private key means you cannot ship updates to existing users
      - The public key goes in `tauri.conf.json`, the private key stays in CI/CD secrets
      - Windows `installMode` options: `"passive"` (default, minimal UI), `"basicUi"` (interactive), `"quiet"` (no feedback)
      - Permissions: `"updater:default"` grants check, download, and install
      
      ---
      
      ## Deep Link Plugin
      
      ### Configuration (tauri.conf.json)
      
      ```json
      {
        "plugins": {
          "deep-link": {
            "mobile": [{ "scheme": ["myapp"], "appLink": false }],
            "desktop": {
              "schemes": ["myapp"]
            }
          }
        }
      }
      ```
      
      ### JavaScript API
      
      ```typescript
      import { getCurrent, onOpenUrl } from "@tauri-apps/plugin-deep-link";
      
      // Check if app was started via deep link
      const startUrls = await getCurrent();
      if (startUrls) {
        handleDeepLink(startUrls);
      }
      
      // Listen for deep links while the app is running
      await onOpenUrl((urls) => {
        console.log("Deep link received:", urls);
        handleDeepLink(urls);
      });
      
      function handleDeepLink(urls: string[]) {
        for (const url of urls) {
          const parsed = new URL(url);
          // Route based on path: myapp://settings/theme
          console.log(`Scheme: ${parsed.protocol}, Path: ${parsed.pathname}`);
        }
      }
      ```
      
      ### Rust Runtime Registration (Desktop)
      
      ```rust
      use tauri_plugin_deep_link::DeepLinkExt;
      
      app.deep_link().register_all()?;  // Register all configured schemes
      // Or register a specific scheme:
      app.deep_link().register("myapp")?;
      ```
      
      **Key points:**
      
      - On desktop, deep links arrive as command-line arguments -- combine with single-instance plugin for handling when the app is already running
      - On mobile, `appLink: true` requires server-side verification files (`.well-known/assetlinks.json` for Android, `.well-known/apple-app-site-association` for iOS)
      - `appLink: false` uses custom URI schemes (`myapp://`) without server verification
      - Permissions: `"deep-link:default"` grants get-current and event listening
      
      ---
      
      ## Autostart Plugin (Desktop Only)
      
      ### Rust Setup
      
      ```rust
      tauri::Builder::default()
          .setup(|app| {
              #[cfg(desktop)]
              app.handle().plugin(tauri_plugin_autostart::init(
                  tauri_plugin_autostart::MacosLauncher::LaunchAgent,
                  Some(vec!["--minimized"]),  // Optional args passed on startup
              ));
              Ok(())
          })
      ```
      
      ### JavaScript API
      
      ```typescript
      import { enable, disable, isEnabled } from "@tauri-apps/plugin-autostart";
      
      // Enable launch on login
      await enable();
      
      // Check status
      const enabled = await isEnabled();
      console.log(`Autostart: ${enabled ? "enabled" : "disabled"}`);
      
      // Disable launch on login
      await disable();
      ```
      
      **Key points:**
      
      - Desktop-only -- guard with `#[cfg(desktop)]`
      - `MacosLauncher::LaunchAgent` is the recommended macOS method
      - Optional args are passed to the app when it auto-starts (useful for `--minimized` flag)
      - Permissions: `"autostart:allow-enable"`, `"autostart:allow-disable"`, `"autostart:allow-is-enabled"`
      
      ---
      
      ## Single Instance Plugin (Desktop Only)
      
      ### Rust Setup
      
      ```rust
      tauri::Builder::default()
          .plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
              // This callback runs in the EXISTING instance when a second instance is launched
              println!("Second instance launched with args: {:?}", argv);
              println!("Working directory: {}", cwd);
      
              // Focus the existing window
              if let Some(window) = app.get_webview_window("main") {
                  let _ = window.set_focus();
              }
      
              // Handle deep links from argv
              for arg in &argv {
                  if arg.starts_with("myapp://") {
                      // Handle deep link
                  }
              }
          }))
      ```
      
      **Key points:**
      
      - Desktop-only
      - When a second instance starts, the callback fires in the FIRST instance and the second instance exits
      - The `argv` parameter contains command-line arguments from the second instance (useful for deep links on desktop)
      - Combine with the deep-link plugin for complete deep link handling on desktop
      - No JS API needed -- behavior is entirely configured in Rust
      
      ---
      
      ## Window State Plugin (Desktop Only)
      
      ### Rust Setup
      
      ```rust
      tauri::Builder::default()
          .setup(|app| {
              #[cfg(desktop)]
              app.handle().plugin(
                  tauri_plugin_window_state::Builder::default().build()
              )?;
              Ok(())
          })
      ```
      
      **That is it.** The plugin automatically:
      
      - Saves window position, size, and maximized state on close
      - Restores saved state on next launch
      - No JavaScript API needed
      
      **Key points:**
      
      - Zero-config persistence of window geometry
      - Data persists in the app's data directory
      - Works automatically for all windows
      - Permissions: `"window-state:default"`
      
      ---
      
      ## Global Shortcut Plugin (Desktop Only)
      
      ### JavaScript API
      
      ```typescript
      import {
        register,
        unregister,
        unregisterAll,
        isRegistered,
      } from "@tauri-apps/plugin-global-shortcut";
      
      // Register a global shortcut
      await register("CommandOrControl+Shift+C", () => {
        console.log("Global shortcut triggered");
      });
      
      // Check if registered
      const registered = await isRegistered("CommandOrControl+Shift+C");
      
      // Unregister specific shortcut
      await unregister("CommandOrControl+Shift+C");
      
      // Unregister all shortcuts
      await unregisterAll();
      ```
      
      ### Rust API (with Pressed/Released Events)
      
      ```rust
      use tauri_plugin_global_shortcut::{
          Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState,
      };
      
      let shortcut = Shortcut::new(Some(Modifiers::CONTROL), Code::KeyN);
      
      app.handle().plugin(
          tauri_plugin_global_shortcut::Builder::new()
              .with_handler(move |_app, shortcut, event| {
                  match event.state() {
                      ShortcutState::Pressed => println!("Shortcut pressed"),
                      ShortcutState::Released => println!("Shortcut released"),
                  }
              })
              .build(),
      )?;
      
      app.global_shortcut().register(shortcut)?;
      ```
      
      **Key points:**
      
      - Desktop-only -- guard with `#[cfg(desktop)]`
      - `CommandOrControl` maps to `Cmd` on macOS and `Ctrl` on Windows/Linux
      - Registering a shortcut already bound system-wide may silently fail or override the system binding (OS-dependent)
      - Always unregister shortcuts when no longer needed to avoid conflicts
      - Permissions: `"global-shortcut:allow-register"`, `"global-shortcut:allow-unregister"`, `"global-shortcut:allow-is-registered"`
      
      ---
      
      See [system.md](system.md) for shell, notification, clipboard, dialog APIs. See [networking.md](networking.md) for http and log APIs.
      
    • mobile.md 4.6 KB
      # Tauri Plugins - Mobile-Only
      
      > Barcode scanner, biometric, geolocation, haptics, and NFC plugin APIs. See [core.md](core.md) for installation and permission patterns. See [reference.md](../reference.md) for platform support.
      
      ---
      
      ## Barcode Scanner Plugin (Mobile Only)
      
      ```typescript
      import { scan, Format } from "@tauri-apps/plugin-barcode-scanner";
      
      // Scan a barcode using the device camera
      const result = await scan({
        formats: [Format.QR_CODE, Format.EAN_13],
        windowed: false, // Full-screen scanner
      });
      
      console.log(`Scanned: ${result.content} (format: ${result.format})`);
      ```
      
      **Key points:**
      
      - iOS and Android only -- not available on desktop
      - Requires camera permission on both platforms
      - `windowed: true` shows the scanner in a small overlay; `false` uses full-screen
      - Supported formats: QR Code, EAN-8, EAN-13, Code 39, Code 128, and more
      - Permissions: `"barcode-scanner:default"` or `"barcode-scanner:allow-scan"`
      
      ---
      
      ## Biometric Plugin (Mobile Only)
      
      ```typescript
      import { authenticate } from "@tauri-apps/plugin-biometric";
      
      // Authenticate the user with biometric prompt
      try {
        await authenticate("Confirm your identity to proceed", {
          title: "Biometric Authentication",
          subtitle: "Verify to access settings",
          confirmationRequired: true,
          allowDeviceCredential: true, // Fall back to PIN/password
        });
        console.log("Authentication successful");
      } catch (error) {
        console.error("Authentication failed:", error);
      }
      ```
      
      **Key points:**
      
      - iOS (Face ID, Touch ID) and Android (fingerprint, face) only
      - `allowDeviceCredential: true` allows PIN/password as fallback
      - `confirmationRequired` adds explicit confirmation after biometric (Android only)
      - The function throws on failure -- wrap in try/catch
      - No `BiometryType` export -- the plugin only exports `authenticate` and permission helpers
      - Permissions: `"biometric:default"` or `"biometric:allow-authenticate"`
      
      ---
      
      ## Geolocation Plugin (Mobile Only)
      
      ```typescript
      import {
        getCurrentPosition,
        watchPosition,
      } from "@tauri-apps/plugin-geolocation";
      
      // Get current position
      const position = await getCurrentPosition();
      console.log(
        `Lat: ${position.coords.latitude}, Lng: ${position.coords.longitude}`,
      );
      console.log(`Accuracy: ${position.coords.accuracy}m`);
      
      // Watch position changes
      const watchId = await watchPosition(
        { enableHighAccuracy: true },
        (position) => {
          if (position) {
            console.log(
              `Moved to: ${position.coords.latitude}, ${position.coords.longitude}`,
            );
          }
        },
      );
      ```
      
      **Key points:**
      
      - iOS and Android only
      - Requires location permission on both platforms (the plugin handles permission requests)
      - `enableHighAccuracy: true` uses GPS (higher battery usage)
      - Cancel watching with `clearWatch(watchId)`
      - Permissions: `"geolocation:default"`
      
      ---
      
      ## Haptics Plugin (Mobile Only)
      
      ```typescript
      import {
        impactFeedback,
        notificationFeedback,
        selectionFeedback,
        vibrate,
      } from "@tauri-apps/plugin-haptics";
      
      // Impact feedback (tap sensation) -- accepts "light", "medium", "heavy"
      await impactFeedback("medium");
      
      // Notification feedback (success/warning/error vibration) -- accepts "success", "warning", "error"
      await notificationFeedback("success");
      
      // Selection feedback (light tap for UI selection)
      await selectionFeedback();
      
      // Custom vibration pattern (Android only -- iOS ignores duration)
      const VIBRATION_DURATION_MS = 200;
      await vibrate(VIBRATION_DURATION_MS);
      ```
      
      **Key points:**
      
      - iOS and Android only
      - `impactFeedback` accepts string values: `"light"`, `"medium"`, `"heavy"`
      - `notificationFeedback` accepts string values: `"success"`, `"warning"`, `"error"`
      - iOS uses the Taptic Engine -- `vibrate()` duration is ignored on iOS
      - Android uses the vibrator motor
      - Permissions: `"haptics:default"`
      
      ---
      
      ## NFC Plugin (Mobile Only)
      
      ```typescript
      import { scan, write, textRecord, uriRecord } from "@tauri-apps/plugin-nfc";
      
      // Scan for NFC tags
      const tag = await scan({
        type: "tag",
        keepSessionAlive: false,
      });
      console.log("Tag data:", tag);
      
      // Write to an NFC tag
      await write([textRecord("Hello from Tauri"), uriRecord("https://example.com")]);
      ```
      
      **Key points:**
      
      - iOS and Android only
      - Requires NFC hardware and permission
      - `scan()` takes an options object with `type` and `keepSessionAlive` fields
      - `write()` accepts an array of records created with `textRecord()`, `uriRecord()`, etc.
      - iOS requires a user-initiated scan action (cannot scan in background)
      - Android supports background NFC scanning
      - Permissions: `"nfc:default"` or `"nfc:allow-scan"`
      
      ---
      
      See [core.md](core.md) for the four-step installation pattern. See [custom-plugins.md](custom-plugins.md) for building mobile-native plugin functionality.
      
    • networking.md 4.5 KB
      # Tauri Plugins - Networking
      
      > HTTP client, logging, WebSocket, and upload plugin APIs. See [core.md](core.md) for installation and permission patterns. See [reference.md](../reference.md) for log targets and platform support.
      
      ---
      
      ## HTTP Plugin
      
      ### JavaScript API
      
      ```typescript
      import { fetch } from "@tauri-apps/plugin-http";
      
      // GET request
      const response = await fetch("https://api.example.com/data", {
        method: "GET",
        headers: { Authorization: "Bearer token" },
      });
      const data = await response.json();
      
      // POST request with JSON body
      const createResponse = await fetch("https://api.example.com/items", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ name: "New Item" }),
      });
      ```
      
      ### Scoped Permissions
      
      ```json
      {
        "permissions": [
          {
            "identifier": "http:default",
            "allow": [
              { "url": "https://api.example.com/**" },
              { "url": "https://cdn.example.com/**" }
            ]
          }
        ]
      }
      ```
      
      **Key points:**
      
      - HTTP plugin requests go through the Rust backend, bypassing CORS restrictions
      - Always scope URL patterns in capabilities -- unscoped `http:default` allows requests to any domain
      - The API mirrors the browser `fetch()` API
      - Works on all platforms (desktop and mobile)
      
      ---
      
      ## Log Plugin
      
      ### Rust Configuration
      
      ```rust
      use tauri_plugin_log::{Target, TargetKind};
      
      const MAX_LOG_FILE_SIZE: u128 = 50_000; // bytes
      
      tauri::Builder::default()
          .plugin(
              tauri_plugin_log::Builder::new()
                  .targets([
                      Target::new(TargetKind::Stdout),
                      Target::new(TargetKind::Webview),
                      Target::new(TargetKind::LogDir {
                          file_name: Some("app".to_string()),
                      }),
                  ])
                  .max_file_size(MAX_LOG_FILE_SIZE)
                  .rotation_strategy(tauri_plugin_log::RotationStrategy::KeepAll)
                  .level(log::LevelFilter::Info)
                  .level_for("hyper", log::LevelFilter::Warn) // Silence noisy dependencies
                  .timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
                  .build(),
          )
      ```
      
      ### JavaScript API
      
      ```typescript
      import {
        trace,
        debug,
        info,
        warn,
        error,
        attachConsole,
      } from "@tauri-apps/plugin-log";
      
      // Attach to browser console (forwards console.log etc. to Tauri log)
      const detach = await attachConsole();
      
      // Log at different levels
      trace("Detailed trace information");
      debug("Debug diagnostic info");
      info("Normal operation info");
      warn("Something unexpected happened");
      error("Something failed");
      
      // Detach console forwarding when no longer needed
      detach();
      ```
      
      ### Forwarding Console Output
      
      ```typescript
      import { trace, error } from "@tauri-apps/plugin-log";
      
      function forwardConsole(
        fnName: "log" | "warn" | "error",
        logger: (message: string) => Promise<void>,
      ) {
        const original = console[fnName];
        console[fnName] = (message: string) => {
          original(message);
          logger(message);
        };
      }
      
      forwardConsole("log", trace);
      forwardConsole("error", error);
      ```
      
      **Key points:**
      
      - Default targets: stdout + app log directory
      - Use `.clear_targets()` before `.targets()` to override defaults
      - Log directory: Linux `~/.local/share/{bundleId}/logs`, macOS `~/Library/Logs/{bundleId}`, Windows `AppData\Local\{bundleId}\logs`
      - `.level_for()` filters logs from specific Rust modules (useful for silencing noisy dependencies like `hyper`)
      - Permissions: `"log:default"`
      
      ---
      
      ## WebSocket Plugin
      
      ```typescript
      import WebSocket from "@tauri-apps/plugin-websocket";
      
      // Connect to a WebSocket server
      const ws = await WebSocket.connect("wss://echo.websocket.org");
      
      // Listen for messages
      ws.addListener((message) => {
        console.log("Received:", message);
      });
      
      // Send message
      ws.send("Hello, WebSocket!");
      
      // Disconnect
      ws.disconnect();
      ```
      
      **Key points:**
      
      - WebSocket connections go through the Rust backend (bypasses browser restrictions)
      - Works on all platforms
      - Permissions: `"websocket:default"`
      
      ---
      
      ## Upload Plugin
      
      ```typescript
      import { upload } from "@tauri-apps/plugin-upload";
      
      await upload(
        "https://api.example.com/upload",
        "/path/to/file.zip",
        (progress, total) => {
          console.log(`Uploaded ${progress} of ${total} bytes`);
        },
        { Authorization: "Bearer token" },
      );
      ```
      
      **Key points:**
      
      - Streams the file from disk (does not load entire file into memory)
      - Progress callback provides byte-level progress
      - Works on all platforms
      - Permissions: `"upload:default"`
      
      ---
      
      See [data-storage.md](data-storage.md) for fs, store, sql, stronghold APIs. See [lifecycle.md](lifecycle.md) for updater and deep-link APIs.
      
    • system.md 5.6 KB
      # Tauri Plugins - System Integration
      
      > Shell, notification, clipboard, dialog, OS info, and process plugin APIs. See [core.md](core.md) for installation and permission patterns. See [reference.md](../reference.md) for the full plugin registry.
      
      ---
      
      ## Shell Plugin (Desktop Only)
      
      ### Opening URLs and Files
      
      ```typescript
      import { open } from "@tauri-apps/plugin-shell";
      
      // Open URL in default browser
      await open("https://tauri.app");
      
      // Open file with default application
      await open("/path/to/document.pdf");
      ```
      
      **Permission:** `"shell:allow-open"` (included in `shell:default`).
      
      ### Executing Commands (Dangerous -- Scope Carefully)
      
      Shell execution must be scoped to specific commands and arguments in the capability file.
      
      ```json
      {
        "permissions": [
          "shell:allow-open",
          {
            "identifier": "shell:allow-execute",
            "allow": [
              {
                "name": "run-git-status",
                "cmd": "git",
                "args": ["status"],
                "sidecar": false
              },
              {
                "name": "run-ls",
                "cmd": "ls",
                "args": ["-la", { "validator": "\\S+" }],
                "sidecar": false
              }
            ]
          }
        ]
      }
      ```
      
      **Key points:**
      
      - Desktop-only -- wrap registration in `#[cfg(desktop)]`
      - `shell:allow-open` is safe (opens URLs/files in default app)
      - `shell:allow-execute` is dangerous -- always scope to specific commands
      - Use `{ "validator": "\\S+" }` for arguments that need runtime validation
      - Never grant unscoped `shell:allow-execute`
      
      ---
      
      ## Notification Plugin
      
      ### Sending Notifications
      
      ```typescript
      import {
        isPermissionGranted,
        requestPermission,
        sendNotification,
      } from "@tauri-apps/plugin-notification";
      
      // Check and request notification permission (required on macOS and mobile)
      let granted = await isPermissionGranted();
      if (!granted) {
        const permission = await requestPermission();
        granted = permission === "granted";
      }
      
      if (granted) {
        sendNotification({
          title: "Download Complete",
          body: "Your file has been downloaded successfully.",
        });
      }
      ```
      
      **Key points:**
      
      - On macOS and mobile, notifications require explicit user permission -- always check first
      - On Windows and Linux, permissions are typically granted by default
      - The notification API is fire-and-forget -- no callback when the user clicks
      - Permissions: `"notification:default"` grants send and permission check
      
      ---
      
      ## Clipboard Plugin
      
      ### Reading and Writing Clipboard
      
      ```typescript
      import { readText, writeText } from "@tauri-apps/plugin-clipboard-manager";
      
      // Write text to clipboard
      await writeText("Copied text content");
      
      // Read text from clipboard
      const text = await readText();
      ```
      
      **Key points:**
      
      - Works on desktop and mobile
      - Permissions: `"clipboard-manager:default"` grants read and write
      - The plugin name in capabilities uses `clipboard-manager` (not `clipboard`)
      
      ---
      
      ## Dialog Plugin
      
      ### File Picker, Save Dialog, Message Boxes
      
      ```typescript
      import { open, save, message, ask, confirm } from "@tauri-apps/plugin-dialog";
      
      // File picker (returns path or null if cancelled)
      const filePath = await open({
        multiple: false,
        filters: [{ name: "Documents", extensions: ["txt", "md", "json"] }],
      });
      
      // Multiple file picker
      const filePaths = await open({
        multiple: true,
        directory: false,
      });
      
      // Directory picker
      const dirPath = await open({
        directory: true,
      });
      
      // Save dialog
      const savePath = await save({
        defaultPath: "export.json",
        filters: [{ name: "JSON", extensions: ["json"] }],
      });
      
      // Message dialog
      await message("Operation complete", { title: "Success", kind: "info" });
      
      // Confirm dialog (returns boolean)
      const confirmed = await ask("Are you sure you want to delete this?", {
        title: "Confirm Delete",
        kind: "warning",
      });
      
      // Yes/No/Cancel confirm
      const result = await confirm("Save changes before closing?", {
        title: "Unsaved Changes",
        kind: "warning",
        cancelLabel: "Cancel",
        okLabel: "Save",
      });
      ```
      
      **Key points:**
      
      - Works on desktop and mobile (file selection). Folder picker is desktop-only.
      - On mobile, file dialog returns `file://` URIs (iOS) or content URIs (Android) instead of filesystem paths
      - All picker dialogs return `null` when the user cancels -- always handle the null case
      - `kind` options: `"info"`, `"warning"`, `"error"`
      - Permissions: `"dialog:default"` grants all dialog operations
      
      ---
      
      ## OS Plugin
      
      ### Getting OS Information
      
      ```typescript
      import {
        platform,
        arch,
        type,
        version,
        locale,
        hostname,
      } from "@tauri-apps/plugin-os";
      
      const osInfo = {
        platform: platform(), // "linux", "macos", "windows", "ios", "android"
        arch: arch(), // "x86_64", "aarch64", "armv7", etc.
        osType: type(), // "linux", "darwin", "windows_nt"
        osVersion: version(), // "14.0" (macOS), "10.0.22621" (Windows), etc.
        locale: await locale(), // "en-US", "fr-FR", etc.
        hostname: await hostname(), // Computer name (desktop only)
      };
      ```
      
      **Key points:**
      
      - Works on all platforms
      - `platform()`, `arch()`, `type()`, `version()` are synchronous
      - `locale()` and `hostname()` are async
      - Permissions: `"os:default"` grants all operations
      
      ---
      
      ## Process Plugin
      
      ### App Lifecycle Control
      
      ```typescript
      import { exit, relaunch } from "@tauri-apps/plugin-process";
      
      // Exit the application
      await exit(0); // Exit code 0 = success
      
      // Restart the application
      await relaunch();
      ```
      
      **Key points:**
      
      - `exit()` terminates the process immediately -- ensure data is saved first
      - `relaunch()` starts a new instance and exits the current one (used after updates)
      - Works on all platforms
      - Permissions: `"process:default"` grants exit and relaunch
      
      ---
      
      See [lifecycle.md](lifecycle.md) for updater, deep-link, autostart, global-shortcut APIs. See [data-storage.md](data-storage.md) for fs, store, sql, stronghold APIs.
      
  • reference.md 11.6 KB
    # Tauri Plugins Quick Reference
    
    > Quick-lookup tables, plugin registry, permission patterns, and platform support. See [SKILL.md](SKILL.md) for decision frameworks and red flags. See [examples/core.md](examples/core.md) for installation patterns.
    
    ---
    
    ## Official Plugin Registry
    
    | Plugin          | Cargo Crate                      | NPM Package                            | Platform                         | Init Pattern                    |
    | --------------- | -------------------------------- | -------------------------------------- | -------------------------------- | ------------------------------- |
    | File System     | `tauri-plugin-fs`                | `@tauri-apps/plugin-fs`                | All                              | `.init()`                       |
    | Dialog          | `tauri-plugin-dialog`            | `@tauri-apps/plugin-dialog`            | All (folder picker desktop-only) | `.init()`                       |
    | HTTP Client     | `tauri-plugin-http`              | `@tauri-apps/plugin-http`              | All                              | `.init()`                       |
    | Store           | `tauri-plugin-store`             | `@tauri-apps/plugin-store`             | All                              | `Builder::new().build()`        |
    | Notification    | `tauri-plugin-notification`      | `@tauri-apps/plugin-notification`      | All                              | `.init()`                       |
    | Shell           | `tauri-plugin-shell`             | `@tauri-apps/plugin-shell`             | Desktop                          | `.init()`                       |
    | Clipboard       | `tauri-plugin-clipboard-manager` | `@tauri-apps/plugin-clipboard-manager` | All                              | `.init()`                       |
    | Updater         | `tauri-plugin-updater`           | `@tauri-apps/plugin-updater`           | Desktop                          | `Builder::new().build()`        |
    | Deep Link       | `tauri-plugin-deep-link`         | `@tauri-apps/plugin-deep-link`         | All                              | `.init()`                       |
    | Autostart       | `tauri-plugin-autostart`         | `@tauri-apps/plugin-autostart`         | Desktop                          | `init(MacosLauncher, args)`     |
    | Global Shortcut | `tauri-plugin-global-shortcut`   | `@tauri-apps/plugin-global-shortcut`   | Desktop                          | `Builder::new().build()`        |
    | Barcode Scanner | `tauri-plugin-barcode-scanner`   | `@tauri-apps/plugin-barcode-scanner`   | Mobile                           | `.init()`                       |
    | Biometric       | `tauri-plugin-biometric`         | `@tauri-apps/plugin-biometric`         | Mobile                           | `.init()`                       |
    | Log             | `tauri-plugin-log`               | `@tauri-apps/plugin-log`               | All                              | `Builder::new().build()`        |
    | Process         | `tauri-plugin-process`           | `@tauri-apps/plugin-process`           | All                              | `.init()`                       |
    | OS              | `tauri-plugin-os`                | `@tauri-apps/plugin-os`                | All                              | `.init()`                       |
    | Window State    | `tauri-plugin-window-state`      | `@tauri-apps/plugin-window-state`      | Desktop                          | `Builder::default().build()`    |
    | SQL             | `tauri-plugin-sql`               | `@tauri-apps/plugin-sql`               | All                              | `Builder::default().build()`    |
    | Stronghold      | `tauri-plugin-stronghold`        | `@tauri-apps/plugin-stronghold`        | Desktop                          | `Builder::new(hash_fn).build()` |
    | Single Instance | `tauri-plugin-single-instance`   | `@tauri-apps/plugin-single-instance`   | Desktop                          | `init(callback)`                |
    | Geolocation     | `tauri-plugin-geolocation`       | `@tauri-apps/plugin-geolocation`       | Mobile                           | `.init()`                       |
    | Haptics         | `tauri-plugin-haptics`           | `@tauri-apps/plugin-haptics`           | Mobile                           | `.init()`                       |
    | NFC             | `tauri-plugin-nfc`               | `@tauri-apps/plugin-nfc`               | Mobile                           | `.init()`                       |
    | Opener          | `tauri-plugin-opener`            | `@tauri-apps/plugin-opener`            | Desktop                          | `.init()`                       |
    | Positioner      | `tauri-plugin-positioner`        | `@tauri-apps/plugin-positioner`        | Desktop                          | `.init()`                       |
    | CLI             | `tauri-plugin-cli`               | `@tauri-apps/plugin-cli`               | Desktop                          | `.init()`                       |
    | Localhost       | `tauri-plugin-localhost`         | `@tauri-apps/plugin-localhost`         | Desktop                          | `Builder::new(port).build()`    |
    | Persisted Scope | `tauri-plugin-persisted-scope`   | `@tauri-apps/plugin-persisted-scope`   | All                              | `.init()`                       |
    | Upload          | `tauri-plugin-upload`            | `@tauri-apps/plugin-upload`            | All                              | `.init()`                       |
    | WebSocket       | `tauri-plugin-websocket`         | `@tauri-apps/plugin-websocket`         | All                              | `.init()`                       |
    
    ---
    
    ## Permission Pattern Reference
    
    | Pattern                    | Meaning                       | Example                                                                                                       |
    | -------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------- |
    | `<plugin>:default`         | Safe defaults for the plugin  | `"store:default"`                                                                                             |
    | `<plugin>:allow-<command>` | Allow a specific command      | `"fs:allow-read-text-file"`                                                                                   |
    | `<plugin>:deny-<command>`  | Deny a specific command       | `"shell:deny-execute"`                                                                                        |
    | Scoped permission (object) | Allow with restrictions       | `{ "identifier": "fs:allow-read-text-file", "allow": [{ "path": "$APPDATA/**" }] }`                           |
    | HTTP URL scope             | Restrict HTTP to domain       | `{ "identifier": "http:default", "allow": [{ "url": "https://api.example.com/**" }] }`                        |
    | Shell command scope        | Restrict to specific commands | `{ "identifier": "shell:allow-execute", "allow": [{ "name": "run-git", "cmd": "git", "args": ["status"] }] }` |
    
    ---
    
    ## Platform Support Matrix
    
    | Feature         | Windows | macOS | Linux | iOS                    | Android                |
    | --------------- | ------- | ----- | ----- | ---------------------- | ---------------------- |
    | File System     | Yes     | Yes   | Yes   | Yes                    | Yes                    |
    | Dialog          | Yes     | Yes   | Yes   | Yes (no folder picker) | Yes (no folder picker) |
    | HTTP            | Yes     | Yes   | Yes   | Yes                    | Yes                    |
    | Store           | Yes     | Yes   | Yes   | Yes                    | Yes                    |
    | Notification    | Yes     | Yes   | Yes   | Yes                    | Yes                    |
    | Shell           | Yes     | Yes   | Yes   | No                     | No                     |
    | Clipboard       | Yes     | Yes   | Yes   | Yes                    | Yes                    |
    | Updater         | Yes     | Yes   | Yes   | No                     | No                     |
    | Deep Link       | Yes     | Yes   | Yes   | Yes                    | Yes                    |
    | Autostart       | Yes     | Yes   | Yes   | No                     | No                     |
    | Global Shortcut | Yes     | Yes   | Yes   | No                     | No                     |
    | Barcode Scanner | No      | No    | No    | Yes                    | Yes                    |
    | Biometric       | No      | No    | No    | Yes                    | Yes                    |
    | Log             | Yes     | Yes   | Yes   | Yes                    | Yes                    |
    | SQL             | Yes     | Yes   | Yes   | Yes                    | Yes                    |
    | Stronghold      | Yes     | Yes   | Yes   | No                     | No                     |
    | Single Instance | Yes     | Yes   | Yes   | No                     | No                     |
    | Window State    | Yes     | Yes   | Yes   | No                     | No                     |
    | Geolocation     | No      | No    | No    | Yes                    | Yes                    |
    | Haptics         | No      | No    | No    | Yes                    | Yes                    |
    | NFC             | No      | No    | No    | Yes                    | Yes                    |
    
    ---
    
    ## SQL Plugin Feature Flags
    
    | Database   | Cargo Feature | Connection String              |
    | ---------- | ------------- | ------------------------------ |
    | SQLite     | `sqlite`      | `sqlite:mydatabase.db`         |
    | MySQL      | `mysql`       | `mysql://user:pass@host/db`    |
    | PostgreSQL | `postgres`    | `postgres://user:pass@host/db` |
    
    **Query placeholder syntax:**
    
    - SQLite: `$1, $2, $3`
    - MySQL: `?, ?, ?`
    - PostgreSQL: `$1, $2, $3`
    
    ---
    
    ## Updater Endpoint JSON Formats
    
    **Static JSON (multi-platform):**
    
    ```json
    {
      "version": "1.0.1",
      "notes": "Release notes",
      "pub_date": "2025-01-15T12:00:00Z",
      "platforms": {
        "linux-x86_64": { "signature": "...", "url": "https://..." },
        "windows-x86_64": { "signature": "...", "url": "https://..." },
        "darwin-x86_64": { "signature": "...", "url": "https://..." },
        "darwin-aarch64": { "signature": "...", "url": "https://..." }
      }
    }
    ```
    
    **Dynamic server response:**
    
    - HTTP 204: No update available
    - HTTP 200: `{ "version": "1.0.1", "url": "...", "signature": "...", "notes": "..." }`
    
    ---
    
    ## Tauri Path Variables (for Permission Scopes)
    
    | Variable        | Resolves To              |
    | --------------- | ------------------------ |
    | `$APPDATA`      | App data directory       |
    | `$APPLOCALDATA` | App local data directory |
    | `$APPCONFIG`    | App config directory     |
    | `$APPCACHE`     | App cache directory      |
    | `$APPLOG`       | App log directory        |
    | `$HOME`         | User home directory      |
    | `$RESOURCE`     | Bundled app resources    |
    | `$TEMP`         | System temp directory    |
    | `$DESKTOP`      | User desktop             |
    | `$DOCUMENT`     | User documents           |
    | `$DOWNLOAD`     | User downloads           |
    
    ---
    
    ## Log Plugin Targets
    
    | Target Kind | Description               | Location         |
    | ----------- | ------------------------- | ---------------- |
    | `Stdout`    | Terminal standard output  | Console          |
    | `Stderr`    | Terminal standard error   | Console          |
    | `Webview`   | Browser devtools console  | Webview          |
    | `LogDir`    | Application log directory | OS-specific path |
    | `Folder`    | Custom directory          | Specified path   |
    
    **Log directory defaults:**
    
    - Linux: `~/.local/share/{bundleId}/logs`
    - macOS: `~/Library/Logs/{bundleId}`
    - Windows: `AppData\Local\{bundleId}\logs`
    
    ---
    
    ## See Also
    
    - [Tauri v2 Plugin Documentation](https://v2.tauri.app/plugin/)
    - [Tauri Plugins Repository](https://github.com/tauri-apps/plugins-workspace)
    - [Plugin Development Guide](https://v2.tauri.app/develop/plugins/)
    - [Mobile Plugin Development](https://v2.tauri.app/develop/plugins/develop-mobile/)
    
  • SKILL.md 17.3 KB
    ---
    name: desktop-plugins-tauri
    description: Tauri 2.x official plugin ecosystem, plugin APIs, permissions, and custom plugin development
    ---
    
    # Tauri 2.x Plugin Ecosystem
    
    > **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:**
    
    - Four-step plugin installation pattern ([examples/core.md](examples/core.md))
    - Data & storage plugins: fs, store, sql, stronghold ([examples/data-storage.md](examples/data-storage.md))
    - System integration plugins: shell, notification, clipboard, dialog, os, process ([examples/system.md](examples/system.md))
    - App lifecycle plugins: updater, deep-link, autostart, single-instance, window-state, global-shortcut ([examples/lifecycle.md](examples/lifecycle.md))
    - Networking plugins: http, log, websocket, upload ([examples/networking.md](examples/networking.md))
    - Mobile-only plugins: barcode-scanner, biometric, geolocation, haptics, nfc ([examples/mobile.md](examples/mobile.md))
    - Custom plugin development: Builder pattern, commands, config, lifecycle hooks, mobile support ([examples/custom-plugins.md](examples/custom-plugins.md))
    
    **Detailed resources:**
    
    - [examples/core.md](examples/core.md) - Installation pattern, permission scoping, multi-plugin registration
    - [examples/data-storage.md](examples/data-storage.md) - fs, store, sql, stronghold plugin APIs
    - [examples/system.md](examples/system.md) - shell, notification, clipboard, dialog, os, process APIs
    - [examples/lifecycle.md](examples/lifecycle.md) - updater, deep-link, autostart, single-instance, window-state, global-shortcut
    - [examples/networking.md](examples/networking.md) - http, log, websocket, upload
    - [examples/mobile.md](examples/mobile.md) - barcode-scanner, biometric, geolocation, haptics, nfc
    - [examples/custom-plugins.md](examples/custom-plugins.md) - Custom plugin scaffolding, Builder, mobile (Swift/Kotlin)
    - [reference.md](reference.md) - Full plugin registry table, permission patterns, platform support matrix
    
    ---
    
    <philosophy>
    
    ## 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)
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Four-Step Plugin Installation
    
    Every official plugin requires exactly four steps. Missing any step causes runtime errors, not compile errors.
    
    ```sh
    # 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](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.
    
    ```json
    {
      "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](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.
    
    ```rust
    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:
    
    | Plugin     | Use Case                   | Encryption     | Query                            | Platform |
    | ---------- | -------------------------- | -------------- | -------------------------------- | -------- |
    | Store      | App preferences, settings  | No             | Key-value only                   | All      |
    | Stronghold | Secrets, API keys, tokens  | Yes (Argon2)   | Key-value only                   | Desktop  |
    | SQL        | Structured data, relations | No (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](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.
    
    ```typescript
    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](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.
    
    ```rust
    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](examples/custom-plugins.md) for lifecycle hooks, configuration, and mobile support.
    
    </patterns>
    
    ---
    
    <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>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related