Tauri Plugins
Tauri 2.0 plugin system including official plugins for filesystem, shell, dialog, notification, HTTP, clipboard, updater, and deep-link, plus community plugins and writing custom plugins.
You are an expert in the Tauri 2.0 plugin ecosystem, including official plugins, community plugins, and building custom plugins for desktop and mobile applications.
## Key Points
- **Bypassing plugins with raw system calls**: Using `std::process::Command` directly instead of the shell plugin skips permission checks and platform abstraction.
- **Installing plugins without configuring capabilities**: Plugins do nothing without the corresponding permission grants in your capability files.
- **Granting wildcard permissions**: Using `"allow-*"` on plugins like `fs` defeats the security model. Grant only the specific permissions your app needs.
- **Reimplementing what official plugins provide**: Custom HTTP commands, file dialogs, or notification systems are worse than the maintained plugins.
## Quick Example
```bash
# Add the Rust dependency
cargo add tauri-plugin-fs
# Add the JavaScript bindings
npm install @tauri-apps/plugin-fs
```
```typescript
import { writeText, readText } from "@tauri-apps/plugin-clipboard-manager";
await writeText("Copied to clipboard!");
const text = await readText();
```skilldb get tauri-skills/Tauri PluginsFull skill: 415 linesTauri Plugins — Extending Tauri Applications
You are an expert in the Tauri 2.0 plugin ecosystem, including official plugins, community plugins, and building custom plugins for desktop and mobile applications.
Core Philosophy
Tauri plugins are the sanctioned way to extend your app's capabilities beyond the core. Each plugin encapsulates a specific system capability -- file access, HTTP requests, notifications -- behind a permission boundary. The plugin system exists because Tauri deliberately ships with a minimal core. You opt into exactly the capabilities you need, which keeps your binary small and your attack surface narrow.
Prefer official plugins over reimplementing functionality in custom commands. The official plugins handle platform differences, implement the permission model correctly, and receive security updates. When you need something beyond what official plugins offer, check the community ecosystem before writing your own. Custom plugins make sense for app-specific system integrations or when wrapping a Rust crate for frontend access.
Anti-Patterns
- Bypassing plugins with raw system calls: Using
std::process::Commanddirectly instead of the shell plugin skips permission checks and platform abstraction. - Installing plugins without configuring capabilities: Plugins do nothing without the corresponding permission grants in your capability files.
- Granting wildcard permissions: Using
"allow-*"on plugins likefsdefeats the security model. Grant only the specific permissions your app needs. - Reimplementing what official plugins provide: Custom HTTP commands, file dialogs, or notification systems are worse than the maintained plugins.
Installing Plugins
Every plugin has a Rust crate and a JavaScript package:
# Add the Rust dependency
cargo add tauri-plugin-fs
# Add the JavaScript bindings
npm install @tauri-apps/plugin-fs
Register in your Rust setup:
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Capabilities and Permissions
Each plugin requires explicit permissions in src-tauri/capabilities/default.json:
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Default capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-read",
"fs:allow-write",
"dialog:allow-open",
"dialog:allow-save",
"shell:allow-open",
"notification:default"
]
}
Scoped Permissions
{
"permissions": [
{
"identifier": "fs:allow-read",
"allow": [
{ "path": "$APPDATA/**" },
{ "path": "$DOCUMENT/**" }
]
},
{
"identifier": "fs:allow-write",
"allow": [
{ "path": "$APPDATA/**" }
]
}
]
}
Official Plugins
Filesystem (tauri-plugin-fs)
import { readTextFile, writeTextFile, readDir, exists, mkdir } from "@tauri-apps/plugin-fs";
import { appDataDir, join } from "@tauri-apps/api/path";
// Read a file
const dataDir = await appDataDir();
const configPath = await join(dataDir, "config.json");
const content = await readTextFile(configPath);
// Write a file
await writeTextFile(configPath, JSON.stringify({ theme: "dark" }));
// List directory contents
const entries = await readDir(dataDir);
for (const entry of entries) {
console.log(entry.name, entry.isDirectory);
}
// Create directories
const logsDir = await join(dataDir, "logs");
if (!(await exists(logsDir))) {
await mkdir(logsDir, { recursive: true });
}
Dialog (tauri-plugin-dialog)
import { open, save, message, ask, confirm } from "@tauri-apps/plugin-dialog";
// Open file picker
const selected = await open({
multiple: false,
filters: [
{ name: "Images", extensions: ["png", "jpg", "gif"] },
{ name: "All Files", extensions: ["*"] },
],
});
if (selected) {
console.log("Selected:", selected);
}
// Save file dialog
const savePath = await save({
defaultPath: "document.txt",
filters: [{ name: "Text", extensions: ["txt"] }],
});
// Message dialog
await message("Operation complete!", { title: "Success", kind: "info" });
// Confirmation dialog
const confirmed = await confirm("Delete this item?", {
title: "Confirm Deletion",
kind: "warning",
});
Shell (tauri-plugin-shell)
import { Command } from "@tauri-apps/plugin-shell";
// Execute a sidecar or allowed command
const output = await Command.create("echo", ["hello"]).execute();
console.log(output.stdout);
// Streaming output
const cmd = Command.create("long-running-process");
cmd.on("close", (data) => console.log("Exited with code:", data.code));
cmd.stdout.on("data", (line) => console.log("stdout:", line));
cmd.stderr.on("data", (line) => console.error("stderr:", line));
const child = await cmd.spawn();
// Open URL in default browser
import { open as shellOpen } from "@tauri-apps/plugin-shell";
await shellOpen("https://tauri.app");
Shell commands must be allowlisted in tauri.conf.json:
{
"plugins": {
"shell": {
"open": true,
"scope": [
{
"name": "echo",
"cmd": "echo",
"args": true
}
]
}
}
}
HTTP (tauri-plugin-http)
import { fetch } from "@tauri-apps/plugin-http";
const response = await fetch("https://api.example.com/data", {
method: "GET",
headers: {
Authorization: "Bearer token123",
},
});
const data = await response.json();
// POST with body
const postResponse = await fetch("https://api.example.com/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "New Item" }),
});
Notification (tauri-plugin-notification)
import {
isPermissionGranted,
requestPermission,
sendNotification,
} from "@tauri-apps/plugin-notification";
let permission = await isPermissionGranted();
if (!permission) {
const result = await requestPermission();
permission = result === "granted";
}
if (permission) {
sendNotification({
title: "Download Complete",
body: "Your file has been downloaded successfully.",
});
}
Clipboard (tauri-plugin-clipboard-manager)
import { writeText, readText } from "@tauri-apps/plugin-clipboard-manager";
await writeText("Copied to clipboard!");
const text = await readText();
Updater (tauri-plugin-updater)
import { check } from "@tauri-apps/plugin-updater";
const update = await check();
if (update) {
console.log(`Update available: ${update.version}`);
await update.downloadAndInstall();
// Optionally restart
import { relaunch } from "@tauri-apps/plugin-process";
await relaunch();
}
Deep Link (tauri-plugin-deep-link)
// src-tauri/src/lib.rs
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_deep_link::init())
.setup(|app| {
#[cfg(any(target_os = "linux", all(debug_assertions, windows)))]
{
use tauri_plugin_deep_link::DeepLinkExt;
app.deep_link().register_all()?;
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { onOpenUrl } from "@tauri-apps/plugin-deep-link";
await onOpenUrl((urls) => {
console.log("Deep link opened:", urls);
// Handle myapp://path/to/resource
});
Writing a Custom Plugin
Plugin Structure
tauri-plugin-myfeature/
├── Cargo.toml
├── src/
│ ├── lib.rs # Plugin entry point
│ ├── commands.rs # Tauri commands
│ ├── error.rs # Error types
│ └── models.rs # Data types
├── guest-js/ # TypeScript bindings
│ └── index.ts
├── permissions/ # Permission definitions
│ ├── default.toml
│ └── autogenerated/
├── package.json
└── build.rs
Rust Implementation
// src/lib.rs
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Manager, Runtime};
mod commands;
mod error;
pub use error::Error;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("myfeature")
.invoke_handler(tauri::generate_handler![
commands::do_something,
commands::get_status,
])
.setup(|app, api| {
// Initialize plugin state
app.manage(MyFeatureState::default());
Ok(())
})
.build()
}
// src/commands.rs
use tauri::{command, State};
#[derive(Default)]
pub struct MyFeatureState {
pub initialized: std::sync::Mutex<bool>,
}
#[command]
pub async fn do_something(state: State<'_, MyFeatureState>) -> Result<String, crate::Error> {
let mut init = state.initialized.lock().unwrap();
*init = true;
Ok("Feature activated".into())
}
#[command]
pub fn get_status(state: State<'_, MyFeatureState>) -> bool {
*state.initialized.lock().unwrap()
}
// src/error.rs
use serde::Serialize;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Feature not initialized")]
NotInitialized,
#[error("Internal error: {0}")]
Internal(String),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
TypeScript Bindings
// guest-js/index.ts
import { invoke } from "@tauri-apps/api/core";
export async function doSomething(): Promise<string> {
return invoke<string>("plugin:myfeature|do_something");
}
export async function getStatus(): Promise<boolean> {
return invoke<boolean>("plugin:myfeature|get_status");
}
Permission Definitions
# permissions/default.toml
[default]
description = "Default permissions for myfeature plugin"
permissions = ["allow-do-something", "allow-get-status"]
Using the Custom Plugin
// In the consuming app
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_myfeature::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
Install this skill directly: skilldb add tauri-skills
Related Skills
Tauri Commands
Rust commands with the invoke pattern, argument passing, return types, async commands, error handling, state management, and type safety between Rust and TypeScript in Tauri 2.0.
Tauri Distribution
Distributing Tauri applications including installers for MSI, DMG, AppImage, and deb, auto-update with the built-in updater, code signing for Windows and macOS, CI/CD builds, and cross-compilation.
Tauri Frontend
Frontend integration with Tauri 2.0 including React, Vue, Svelte, and Solid frameworks, Vite configuration, asset handling, window management, multiple windows, and webview communication.
Tauri Fundamentals
Tauri 2.0 architecture, Rust backend with webview frontend, project setup with Cargo and npm, development workflow, and build targets for Windows, macOS, Linux, iOS, and Android.
Tauri Mobile
Tauri 2.0 mobile development for iOS and Android, including platform-specific code, mobile plugins, testing on simulators and devices, and app store distribution.
Tauri Patterns
Common Tauri 2.0 patterns: system tray apps, menu bar apps, file handling, SQLite database integration, IPC communication patterns, background tasks, and single-instance enforcement.