Tauri Plugin Development
npx machina-cli add skill UtakataKyosui/C2Lab/tauri-plugin-dev --openclawTauri v2 Plugin Development
Overview
Tauri plugins extend Tauri applications with native capabilities. A plugin provides:
- Rust backend: Core logic, system API access, and command handlers
- TypeScript bindings: JavaScript API for frontend consumption
- Build configuration: Cargo.toml and build.rs for compilation
Plugins follow the naming convention tauri-plugin-<name>, with the corresponding crate name tauri-plugin-<name> and guest bindings at @tauri-apps/plugin-<name>.
Creating a New Plugin
Using the Official CLI (Recommended)
# Tauri CLI v2 required
cargo install tauri-cli
cargo tauri plugin new <plugin-name>
This generates the full project structure. Alternatively, use npx @tauri-apps/cli plugin new <plugin-name>.
The CLI creates:
tauri-plugin-<name>/
├── Cargo.toml # Rust crate configuration
├── build.rs # Build script (generates permissions)
├── src/
│ ├── lib.rs # Plugin implementation (entry point)
│ ├── commands.rs # Command handlers
│ ├── models.rs # Data types (optional)
│ └── error.rs # Error types
├── permissions/
│ └── default.toml # Default permission set
├── guest-js/
│ ├── package.json
│ ├── index.ts # TypeScript API
│ └── tsconfig.json
└── examples/ # Example Tauri apps
See references/plugin-structure.md for detailed file-by-file breakdown.
Core Rust Implementation
Plugin Entry Point (lib.rs)
Every Tauri v2 plugin uses the Builder pattern:
use tauri::plugin::{Builder, TauriPlugin};
use tauri::{Manager, Runtime};
pub use models::*;
pub use error::{Error, Result};
mod commands;
mod error;
mod models;
pub fn init<R: Runtime>() -> TauriPlugin<R> {
Builder::new("<plugin-name>")
.invoke_handler(tauri::generate_handler![
commands::do_something,
])
.setup(|app, api| {
// Initialize plugin state
app.manage(MyState::default());
Ok(())
})
.build()
}
Command Handlers (commands.rs)
Commands are async Rust functions exposed to the frontend:
use tauri::{command, AppHandle, Runtime, State};
use crate::{MyState, Result};
#[command]
pub(crate) async fn do_something<R: Runtime>(
app: AppHandle<R>,
state: State<'_, MyState>,
payload: String,
) -> Result<String> {
// Implementation
Ok(format!("processed: {payload}"))
}
Error Handling (error.rs)
use serde::{ser::Serializer, Serialize};
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("{0}")]
Custom(String),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where S: Serializer {
serializer.serialize_str(self.to_string().as_ref())
}
}
See references/rust-patterns.md for state management, events, and advanced patterns.
TypeScript Bindings
Basic invoke() Pattern (guest-js/index.ts)
import { invoke } from '@tauri-apps/api/core';
export async function doSomething(payload: string): Promise<string> {
return await invoke<string>('plugin:<plugin-name>|do_something', {
payload,
});
}
Important: Tauri v2 uses plugin:<name>|<command> format for plugin commands.
Event Listeners
import { listen } from '@tauri-apps/api/event';
export async function onPluginEvent(
handler: (data: MyEventData) => void,
): Promise<() => void> {
return await listen<MyEventData>('plugin:<plugin-name>//my-event', handler);
}
See references/typescript-bindings.md for full API reference, type generation, and build setup.
Cargo.toml Configuration
[package]
name = "tauri-plugin-<name>"
version = "0.1.0"
edition = "2021"
[dependencies]
tauri = { version = "2", default-features = false }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "2"
[build-dependencies]
tauri-build = { version = "2", default-features = false, features = ["codegen"] }
Registering in an App
// In the Tauri app's src-tauri/src/lib.rs
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_<name>::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
# In the app's src-tauri/Cargo.toml
[dependencies]
tauri-plugin-<name> = { path = "../tauri-plugin-<name>" }
Permissions System (Tauri v2)
Tauri v2 requires explicit permission declarations. Create permissions/default.toml:
[default]
description = "Default permissions for plugin-<name>"
permissions = ["allow-do-something"]
The build.rs auto-generates permission types from command names:
const COMMANDS: &[&str] = &["do_something"];
fn main() {
tauri_build::mobile_entry_point_exists();
tauri_build::try_build(
tauri_build::Attributes::new()
.plugin(
tauri_build::PluginBuilder::new()
.commands(COMMANDS)
.build(),
),
)
.expect("failed to run tauri-build");
}
Testing
Rust Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_core_logic() {
// Test plugin logic without Tauri runtime
let result = process_data("input");
assert_eq!(result, "expected");
}
}
Integration Testing
Use the tauri::test module for testing with a mock app handle:
#[cfg(test)]
mod tests {
use tauri::test::{mock_app, MockRuntime};
#[test]
fn test_plugin_init() {
let app = mock_app();
// Test plugin integration
}
}
See references/testing-debugging.md for full testing strategies and debugging techniques.
Common Workflow
- Scaffold:
cargo tauri plugin new <name> - Define models: Add types to
models.rs - Implement commands: Add handlers to
commands.rs - Register commands: Update
lib.rsinvoke_handler - Add permissions: Create entries in
permissions/ - Write TypeScript: Implement bindings in
guest-js/index.ts - Test: Write unit tests, then integration tests
- Integrate: Add to test Tauri app in
examples/
Mobile Plugin Workflow
When targeting iOS or Android, extend the standard workflow with these steps:
- Split implementation: Create
src/desktop.rs(AppHandle-based) andsrc/mobile.rs(PluginHandle wrapper) - Update lib.rs: Add
#[cfg(desktop)]/#[cfg(mobile)]module declarations and branch in.setup() - Update Cargo.toml: Add
mobile = []feature and[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies]section - Add native code: Create
android/(Kotlin@TauriPluginclass) and/orios/(SwiftPluginsubclass) - Initialize project: Run
tauri android init/tauri ios initto generate platform directories - Develop: Use
tauri android dev/tauri ios devfor live development
See references/mobile-support.md for complete Kotlin/Swift examples, permission setup, and error troubleshooting.
Additional Resources
Reference Files
references/plugin-structure.md- Detailed file structure and Cargo workspace setupreferences/rust-patterns.md- State management, events, mobile support, advanced patternsreferences/typescript-bindings.md- Full TypeScript API, type generation, npm packagingreferences/testing-debugging.md- Testing strategies, logging, common errors, troubleshootingreferences/mobile-support.md- Complete iOS/Android implementation: Kotlin, Swift, permissions, errors
Source
git clone https://github.com/UtakataKyosui/C2Lab/blob/main/plugins/tauri-plugin-dev/skills/tauri-plugin-dev/SKILL.mdView on GitHub Overview
Tauri plugins extend apps with native capabilities by providing a Rust backend, TypeScript bindings, and a build configuration. Plugins follow the tauri-plugin-<name> naming convention and expose a guest API at @tauri-apps/plugin-<name>.
How This Skill Works
Plugins are built with a Builder pattern in lib.rs, exposing async command handlers in commands.rs and optional data types in models.rs. TypeScript bindings live in guest-js/index.ts to expose a frontend API, and the plugin is scaffolded using the Official CLI (cargo tauri plugin new or npx @tauri-apps/cli plugin new). The frontend communicates via the plugin:<name>|<command> format to invoke Rust backend logic.
When to Use It
- When starting a new plugin project to add native capabilities to a Tauri v2 app
- When implementing a command handler in Rust for backend logic
- When exposing a TypeScript API for frontend consumption via guest-js
- When configuring build and permissions with Cargo.toml, build.rs, and default permissions
- When debugging or testing the full Tauri v2 plugin lifecycle from scaffold to deployment
Quick Start
- Step 1: Install the Official CLI and scaffold a new plugin: cargo install tauri-cli; cargo tauri plugin new <plugin-name> (or npx @tauri-apps/cli plugin new <plugin-name>)
- Step 2: Implement core Rust in lib.rs (Builder pattern) and commands.rs for async handlers; update Cargo.toml and build.rs as needed
- Step 3: Create TypeScript bindings in guest-js/index.ts and test frontend calls using invoke('plugin:<plugin-name>|<command>', { ... })
Best Practices
- Follow the tauri-plugin-<name> naming convention and keep the crate name aligned
- Use the Builder pattern in lib.rs and provide a clear setup step for plugin state
- Implement command handlers as async functions and document their inputs/outputs
- Define a robust Error type with Serialize for frontend-friendly error reporting
- Keep TypeScript bindings in guest-js/index.ts in sync with Rust commands and use the plugin:<name>|<command> invocation format
Example Use Cases
- A plugin that exposes a do_something command in Rust and a doSomething(payload) TS binding
- A plugin with a default permissions file at permissions/default.toml and a build script in build.rs
- A plugin with a structured Models type and an error enum to model failures
- A plugin scaffold that includes src/, permissions/, guest-js/, and examples/ directories
- An app that uses guest-js/index.ts to call plugin commands from the frontend via invoke