desktop-plugins-tauri
Tauri 2.x official plugin ecosystem, plugin APIs, permissions, and custom plugin development
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/desktop-plugins-tauri/skills/desktop-plugins-tauri
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
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 usetauri::plugin::Builderwith 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)
- Data & storage plugins: fs, store, sql, stronghold (examples/data-storage.md)
- System integration plugins: shell, notification, clipboard, dialog, os, process (examples/system.md)
- App lifecycle plugins: updater, deep-link, autostart, single-instance, window-state, global-shortcut (examples/lifecycle.md)
- Networking plugins: http, log, websocket, upload (examples/networking.md)
- Mobile-only plugins: barcode-scanner, biometric, geolocation, haptics, nfc (examples/mobile.md)
- Custom plugin development: Builder pattern, commands, config, lifecycle hooks, mobile support (examples/custom-plugins.md)
Detailed resources:
- examples/core.md - Installation pattern, permission scoping, multi-plugin registration
- examples/data-storage.md - fs, store, sql, stronghold plugin APIs
- examples/system.md - shell, notification, clipboard, dialog, os, process APIs
- examples/lifecycle.md - updater, deep-link, autostart, single-instance, window-state, global-shortcut
- examples/networking.md - http, log, websocket, upload
- examples/mobile.md - barcode-scanner, biometric, geolocation, haptics, nfc
- examples/custom-plugins.md - Custom plugin scaffolding, Builder, mobile (Swift/Kotlin)
- reference.md - Full plugin registry table, permission patterns, platform support matrix
<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-filewithout path restriction) -- grants access to entire filesystem - Unscoped HTTP permissions (
http:defaultwithout URL pattern) -- allows requests to any domain - Unscoped shell execute (
shell:allow-executewithout 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-executewhen onlyshell:allow-open(URLs/files) is needed - Missing
sql:allow-executepermission (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 addand 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 useBuilder::new().build()(store, updater, global-shortcut, log) -- check each plugin's docs - Store autoSave: When
autoSave: false, you must callstore.save()manually. WhenautoSaveis a number, it debounces saves by that many milliseconds. - SQL default permissions: Only read operations (select, load, close) are granted by default --
sql:allow-executemust be added explicitly for INSERT/UPDATE/DELETE - Stronghold platform: Desktop-only. Store data as
Uint8Array(not strings) -- useTextEncoder/TextDecoderfor 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.
Reviews (0)
No reviews yet.
No comments yet.