Get the FREE Ultimate OpenClaw Setup Guide →

Tauri Plugin Development

npx machina-cli add skill UtakataKyosui/C2Lab/tauri-plugin-dev --openclaw
Files (1)
SKILL.md
8.1 KB

Tauri 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

  1. Scaffold: cargo tauri plugin new <name>
  2. Define models: Add types to models.rs
  3. Implement commands: Add handlers to commands.rs
  4. Register commands: Update lib.rs invoke_handler
  5. Add permissions: Create entries in permissions/
  6. Write TypeScript: Implement bindings in guest-js/index.ts
  7. Test: Write unit tests, then integration tests
  8. Integrate: Add to test Tauri app in examples/

Mobile Plugin Workflow

When targeting iOS or Android, extend the standard workflow with these steps:

  1. Split implementation: Create src/desktop.rs (AppHandle-based) and src/mobile.rs (PluginHandle wrapper)
  2. Update lib.rs: Add #[cfg(desktop)]/#[cfg(mobile)] module declarations and branch in .setup()
  3. Update Cargo.toml: Add mobile = [] feature and [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] section
  4. Add native code: Create android/ (Kotlin @TauriPlugin class) and/or ios/ (Swift Plugin subclass)
  5. Initialize project: Run tauri android init / tauri ios init to generate platform directories
  6. Develop: Use tauri android dev / tauri ios dev for 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 setup
  • references/rust-patterns.md - State management, events, mobile support, advanced patterns
  • references/typescript-bindings.md - Full TypeScript API, type generation, npm packaging
  • references/testing-debugging.md - Testing strategies, logging, common errors, troubleshooting
  • references/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

  1. 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>)
  2. Step 2: Implement core Rust in lib.rs (Builder pattern) and commands.rs for async handlers; update Cargo.toml and build.rs as needed
  3. 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

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers