# How Tauri's Plugin System Works and How to Create a Custom Plugin

> Discover how Tauri's plugin system works and learn to build your own custom plugin. Explore JavaScript injection, IPC commands, and custom URI schemes.

- Repository: [Tauri/tauri](https://github.com/tauri-apps/tauri)
- Tags: deep-dive
- Published: 2026-02-26

---

**Tauri's plugin system treats extensions as first-class citizens that can inject JavaScript, register IPC commands, hook into lifecycle events, and define custom URI schemes through the `Plugin<R>` trait and `Builder<R, C>` abstraction.**

The plugin architecture in the tauri-apps/tauri repository enables developers to extend both the Rust backend and JavaScript frontend without modifying core framework code. By leveraging the `Builder<R, C>` pattern and the `Plugin<R>` trait defined in [`crates/tauri/src/plugin.rs`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/plugin.rs), you can create reusable extensions that integrate deeply with the application lifecycle across desktop and mobile platforms.

## Core Architecture: Plugin Trait and Builder Pattern

Tauri's plugin system revolves around two fundamental abstractions that decouple extension logic from the core runtime.

### The `Plugin<R>` Trait

The `Plugin<R>` trait in [`crates/tauri/src/plugin.rs`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/plugin.rs) (lines 36-62) defines the runtime contract every plugin must fulfill. It specifies callbacks for initialization, window events, navigation handling, and IPC command registration. When you implement this trait, you gain access to hooks like `initialize`, `window_created`, `on_navigation`, and `extend_api`, allowing your code to execute at precise moments in the application lifecycle.

### The `Builder<R, C>` Ergonomic API

Rather than implementing `Plugin<R>` manually, most developers use the `Builder<R, C>` struct (lines 64-78 in the same file). This builder accumulates configuration through methods like `invoke_handler()`, `js_init_script()`, and `on_navigation()`, then constructs a `TauriPlugin<R, C>` that automatically implements the `Plugin<R>` trait. The builder stores callbacks as boxed closures (e.g., `Box<OnNavigation<R>>`), and the `TauriPlugin` implementation forwards framework calls to these stored closures.

### Plugin Registration Flow

When the application starts, the **plugin store** (`PluginStore<R>`) orchestrates the following sequence:

1. **Registration**: `PluginStore::register` stores the boxed plugin instance (lines 76-89).
2. **Initialization**: `PluginStore::initialize_all` invokes each plugin's `initialize` method (lines 998-1009), executing the `setup` closure supplied to the builder.
3. **Event Wiring**: The runtime connects lifecycle callbacks (window creation, navigation, etc.) to the plugin's stored handlers.

## Lifecycle Hooks and Capabilities

Tauri plugins can intercept or extend nearly every aspect of the application runtime through these specific hooks:

| Hook | Execution Timing | Common Use Case |
|------|------------------|-----------------|
| `initialize` | Immediately after `App` creation, before any window builds | Load native resources, register URI schemes |
| `window_created` | After a `Window<R>` instance constructs | Attach window-specific state or menus |
| `webview_created` | After the `Webview<R>` initializes | Inject JavaScript listeners or preload scripts |
| `on_navigation` | Before any navigation occurs | Block external URLs or validate deep links |
| `on_page_load` | After a page finishes loading | Emit initialization events to the frontend |
| `extend_api` | When an IPC `invoke` message arrives | Provide custom commands via `invoke_handler` |
| `js_init_script` | Before any page script executes | Expose global JavaScript APIs |
| `register_uri_scheme_protocol` | During `initialize` | Handle custom schemes like `myapp://` |

## How to Create a Custom Tauri Plugin

Building a plugin follows a standardized workflow that begins with CLI scaffolding and ends with configuration in [`tauri.conf.json`](https://github.com/tauri-apps/tauri/blob/main/tauri.conf.json).

### Step 1: Scaffold the Plugin Structure

Execute the CLI command to generate a new plugin crate:

```bash
cargo tauri plugin new my_plugin

```

This creates a crate under `src-tauri/plugins/my_plugin` using the template at [`crates/tauri-cli/templates/plugin/src/lib.rs`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri-cli/templates/plugin/src/lib.rs). The generated `init` function already returns a `Builder::new("my_plugin")`, establishing the plugin namespace.

### Step 2: Add IPC Commands

Define Rust functions using the `#[tauri::command]` macro and register them via the builder's `invoke_handler`:

```rust
use tauri::{
    plugin::{Builder, TauriPlugin},
    Runtime,
};

#[tauri::command]
fn greet(name: String) -> String {
    format!("Hello, {name}!")
}

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("my_plugin")
        .invoke_handler(tauri::generate_handler![greet])
        .build()
}

```

### Step 3: Inject Frontend JavaScript

Expose a global API to the frontend by providing a `js_init_script`:

```rust
Builder::new("my_plugin")
    .invoke_handler(tauri::generate_handler![greet])
    .js_init_script(r#"
        window.__MY_PLUGIN__ = {
            greet: (name) => invoke('my_plugin:greet', { name })
        };
    "#)
    .build()

```

### Step 4: Hook Into Lifecycle Events

Intercept navigation or window events using builder methods:

```rust
Builder::new("my_plugin")
    .on_navigation(|_webview, url| {
        // Allow only tauri:// scheme
        url.scheme() == "tauri"
    })
    .build()

```

### Step 5: Configure the Application

Add the plugin name to [`tauri.conf.json`](https://github.com/tauri-apps/tauri/blob/main/tauri.conf.json) under the `plugins` array, then ensure your [`main.rs`](https://github.com/tauri-apps/tauri/blob/main/main.rs) calls the plugin's `init` function:

```rust
tauri::Builder::default()
    .plugin(my_plugin::init())
    .run(tauri::generate_context!())
    .expect("error while running tauri application");

```

## Complete Implementation Examples

### Minimal Standalone Plugin

This example creates a functional "adder" plugin in a standalone crate:

```rust
// src/lib.rs
use tauri::{
    plugin::{Builder, TauriPlugin},
    Runtime,
};

#[tauri::command]
fn add(a: i64, b: i64) -> i64 {
    a + b
}

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("adder")
        .invoke_handler(tauri::generate_handler![add])
        .js_init_script(r#"
            window.__ADDER__ = {
                add: (a, b) => invoke('adder:add', { a, b })
            };
        "#)
        .build()
}

```

After adding `"adder"` to [`tauri.conf.json`](https://github.com/tauri-apps/tauri/blob/main/tauri.conf.json), the plugin is available to both Rust and JavaScript.

### Frontend JavaScript Usage

Access the plugin from any renderer process:

```javascript
// In any webview script
window.__ADDER__.add(2, 3).then(result => console.log(result)); // prints 5

```

### Rust-Side Plugin Access

The scaffolding generates an extension trait allowing direct access to plugin state from Rust:

```rust
fn handle_event(app: &tauri::AppHandle) {
    // The generated extension trait provides the accessor method
    let adder = app.adder();
    // Call methods defined on the plugin's internal state
}

```

### Custom URI Scheme Handler

Register a protocol handler for synchronous or asynchronous resource loading:

```rust
use tauri::http;

Builder::new("files")
    .register_uri_scheme_protocol("myapp", |_ctx, request| {
        let path = &request.uri().path()[1..];
        let data = std::fs::read(path).unwrap_or_default();
        http::Response::builder()
            .header("Content-Type", "application/octet-stream")
            .body(data)
            .unwrap()
    })
    .build()

```

## Mobile vs Desktop Plugin Development

While the core `Builder` and `Plugin` trait work across platforms, mobile-specific functionality resides in [`crates/tauri/src/plugin/mobile.rs`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/plugin/mobile.rs). This module handles the Android/iOS bridge through `PluginApi::run_command` and manages pending call maps for asynchronous native communication. Desktop plugins may include platform-specific code using conditional compilation (e.g., `#[cfg(target_os = "macos")]`) within the plugin crate.

For a complete reference implementation including custom scopes and event channels, examine the official sample plugin at [`examples/api/src-tauri/tauri-plugin-sample/src/lib.rs`](https://github.com/tauri-apps/tauri/blob/main/examples/api/src-tauri/tauri-plugin-sample/src/lib.rs).

## Summary

- **Core Abstractions**: The `Plugin<R>` trait defines the contract, while `Builder<R, C>` provides an ergonomic API for constructing `TauriPlugin` instances without manual trait implementation.
- **Registration**: Plugins register via `PluginStore::register` and initialize through `PluginStore::initialize_all` (lines 998-1009 in [`crates/tauri/src/plugin.rs`](https://github.com/tauri-apps/tauri/blob/main/crates/tauri/src/plugin.rs)).
- **Lifecycle Integration**: Hooks like `initialize`, `window_created`, `on_navigation`, and `extend_api` allow deep integration with the Tauri runtime.
- **CLI Workflow**: Use `cargo tauri plugin new` to scaffold, then configure `invoke_handler`, `js_init_script`, and optional URI scheme protocols.
- **Cross-Platform**: The same plugin architecture supports desktop and mobile, with mobile-specific bridge code isolated in [`mobile.rs`](https://github.com/tauri-apps/tauri/blob/main/mobile.rs).

## Frequently Asked Questions

### What is the difference between a Tauri plugin and a regular Rust module?

A Tauri plugin implements the `Plugin<R>` trait and registers with the `PluginStore`, granting it access to lifecycle hooks, automatic IPC registration, and JavaScript injection capabilities. Regular Rust modules can contain commands but lack the standardized lifecycle integration and frontend-facing APIs that the plugin system provides through `Builder` methods like `js_init_script`.

### How do I share state between plugin commands?

Store state within the plugin's struct or use the `setup` closure to initialize shared data structures before returning the builder. The scaffolding generates an extension trait (e.g., `MyPluginExt`) that allows other Rust code to access this state through `app_handle.my_plugin()`. Alternatively, use Tauri's standard state management with `app.manage()` inside the plugin's `initialize` hook.

### Can I use async/await in plugin commands?

Yes. Declare commands with `async fn` and Tauri's runtime will handle the async execution. When using `invoke_handler(tauri::generate_handler![my_async_command])`, the framework automatically awaits the future. For mobile plugins, the bridge in [`mobile.rs`](https://github.com/tauri-apps/tauri/blob/main/mobile.rs) specifically manages pending asynchronous calls between the Rust core and native mobile code.

### How do I debug a plugin during development?

Run your Tauri application with `cargo tauri dev` and use standard Rust debugging tools like `println!`, `dbg!`, or attach a debugger to the Rust process. For the JavaScript side, use the browser's DevTools to inspect the injected `js_init_script` and verify that global objects like `window.__MY_PLUGIN__` are correctly initialized. Check the `examples/api/src-tauri/tauri-plugin-sample` directory for debugging patterns involving custom scopes and event channels.