Plugin Development Best Practices
Strategies for selecting, maintaining, and securing Tauri plugins in your React application.
Plugins extend a Tauri app with capabilities that aren’t baked into the core — databases, HTTP clients, system trays, and anything else that shouldn’t live in every binary. Once you start relying on them, a handful of decisions determine whether your project stays stable and secure, or breaks in unexpected ways. This guide covers how to pick a plugin you can trust, keep its version in lockstep with Tauri, lock down permissions to only what you need, avoid the most common security pitfalls, and diagnose the errors that catch almost everyone at least once.
How to Choose the Right Plugin
Every plugin you add becomes a permanent dependency: its code runs inside your app’s process, consumes CPU cycles, and potentially reads or writes files. A poorly chosen plugin can drag in outdated dependencies, widen your attack surface, or stop working when Tauri updates. The decision matters, even if you’re only installing one of the official ones. The Choosing the Right Plugin page is the full decision framework.
When evaluating a plugin, start with these checks — they cost a minute and save hours later.
- Official vs community. Plugins maintained under the
tauri-appsGitHub organisation and published with thetauri-plugin-prefix are reviewed by the core team. They follow the same release cadence as Tauri itself and their permissions are well-documented. Community plugins can be excellent, but you carry the burden of verifying them yourself. - Tauri v2 compatibility explicitly stated. A plugin that only mentions Tauri v1 in its README or
Cargo.tomlkeywords is a risk. The migration from v1 to v2 changed the plugin API, permission model, and IPC layer. Look fortauri = "2"in the[dependencies]of the plugin’s Cargo.toml, or a clear compatibility table in its docs. - Maintenance signals. Check the last commit date, number of open issues that look like “does not work with Tauri 2.0”, and whether the maintainer responds to PRs. A plugin that hasn’t been touched in eighteen months will break eventually; the only question is when.
- Permission footprint. A plugin that demands
fs:allow-allto open a single configuration file is either poorly designed or hiding something. Legitimate plugins request the narrowest set of permissions possible. The official plugins publish their permission table — read it before you install. - License. Permissive licenses (MIT, Apache-2.0) let you ship without legal surprises. Copyleft licenses like GPL can restrict how you distribute your app. This is easy to overlook when you’re just trying to add a feature.
Abandoned Plugins Can Block Tauri Upgrades:
If a plugin pins a dependency that conflicts with the version required by the latest Tauri release, cargo update will refuse to resolve. You end up stuck on an older Tauri version or forced to fork the plugin. Check the plugin’s release history before treating it as a permanent fixture.
When in doubt, prefer an official plugin over a community one, and a community plugin with a clear maintenance record over one that appears to be a one-off experiment. If no plugin does exactly what you need, consider whether you can build the functionality as a small custom plugin inside your own project — you control the code and the maintenance timeline.
Keeping Versions in Sync
Tauri v2 has a strict rule: every plugin you use must be built against a compatible version of the core Tauri crate. A mismatch between the Tauri runtime and a plugin can produce linker errors, silently dropped commands, or crashes that only appear on one platform. See Version Compatibility.
The simplest way to install a plugin is the tauri add command. It reads your project’s current Tauri version and pulls the matching plugin release automatically.
npm run tauri add <plugin-name>
Under the hood, this updates both src-tauri/Cargo.toml (the Rust dependency) and package.json (the JavaScript bindings your React frontend imports). After running it, inspect the versions it selected.
[dependencies]
tauri = "2"
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
{
"dependencies": {
"@tauri-apps/plugin-sql": "^2.0.0"
}
}
Notice the tauri = "2" line: the "2" specifier means “any semver-compatible release in the 2.x range.” This is what you want. Pinning an exact patch like =2.0.1 prevents cargo update from pulling bugfixes; a wildcard like * can break your project when a new major version appears.
Mixing Tauri 2 App with v1 Plugins Will Not Work:
The plugin API changed fundamentally between v1 and v2. If a plugin’s Cargo.toml lists tauri = "1" as a dependency, it will not compile against your tauri = "2" app. The compiler error is usually a wall of trait-bound mismatches — don’t try to work around it; find the v2 version of the plugin or update it yourself.
When you want to upgrade everything at once, use the Tauri CLI’s update command:
npm run tauri update
This bumps all Tauri crates, plugins, and JS packages to the latest versions that satisfy your semver constraints. If a plugin has not yet released a compatible update, the command will report the conflict rather than silently breaking your build.
A lesson that surfaces repeatedly in community discussions: after upgrading Tauri, always check for plugin updates as well. The tauri update command handles this, but if you’re updating Rust dependencies manually with cargo update, you must separately update JS packages with npm update @tauri-apps/plugin-*. A Rust plugin that expects a new IPC protocol will fail silently if the frontend bindings are still on the old version.
A Clean Update Confirmation:
After running tauri update, if cargo build completes without errors and your app launches with all plugin features working, your version alignment is correct. The absence of console errors about missing commands is the signal you’re looking for.
Gating Access with Permissions
Tauri v2 replaced the old allowlist system with a capability-based permission model. Every command a plugin exposes, from reading a file to opening a URL, must be explicitly granted in your app’s capabilities configuration. This is not optional — a command that hasn’t been permitted will be blocked, and you’ll see an error in the browser console. The Permission Management page walks through granting those identifiers.
Official plugins ship with auto-generated permission files that enumerate every command. Your job is to include the identifiers your app actually uses.
Adding Permissions to a Plugin, Step by Step
The process is the same whether you’re using an official plugin or a custom one that has generated its permissions correctly.
Step 1: Identify the permission identifiers
Each plugin command has a corresponding permission string in the format plugin-name:allow-command-name. For example, the opener plugin defines opener:allow-open-path and opener:allow-open-url. Check the plugin’s documentation or its generated permissions/ folder.
Step 2: Edit the capabilities file
Open src-tauri/capabilities/default.json (or whichever capability file targets your main window). Add the required permissions to the permissions array.
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "main-capability",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"opener:allow-open-url",
"opener:allow-open-path"
]
}
If a permission accepts scoped arguments — like restricting file paths — you can supply them inline. The opener plugin, for instance, lets you restrict open-path to a specific directory using a glob pattern:
{
"identifier": "opener:allow-open-path",
"allow": [{ "path": "$APPDATA/**" }]
}
Without scoping, the command is allowed on any path, which is rarely what you want.
Step 3: Test the command from the frontend
Import the plugin’s JS binding in a React component and call the command.
import { openUrl } from "@tauri-apps/plugin-opener";
function App() {
const handleOpen = async () => {
await openUrl("https://tauri.app");
};
return <button onClick={handleOpen}>Open Tauri Website</button>;
}
If the command fires successfully, the permission is configured. If it fails, the browser console will contain a message like opener:allow-open-url not allowed.
Forgetting Permissions Is the Most Common v2 Breakage:
If your app worked in Tauri v1 and suddenly all plugin commands are rejected in v2, this is almost certainly the cause. Every command, even from official plugins, must be listed in a capability. There is no global “allow all” escape hatch — by design.
Permissions for Inline Plugins
When you define a plugin directly in your app’s Rust code (not as a separate crate), Tauri does not automatically generate a permission manifest. You must add a build script that tells the Tauri CLI about your plugin’s commands.
Add this to your build.rs in src-tauri:
fn main() {
tauri_build::build()
}
And configure tauri-build in your Cargo.toml:
[build-dependencies]
tauri-build = "2"
Then register your plugin with tauri::Builder::default().plugin(…). Tauri will pick up the permission files you place in the permissions/ directory of your plugin crate. If you’re writing an inline plugin without a separate crate, you may want to extract it into a local crate under src-tauri/plugins/ so the build system treats it as a standalone unit with its own permissions.
The Manifest Error:
Seeing Plugin did not define its manifest in the console means Tauri cannot locate permission files for your inline plugin. The fix is the build script and permission folder structure described above. Without it, your plugin’s commands will never be callable from the frontend.
Security When Adding Third-Party Code
A Tauri plugin runs in the same Rust process as your application’s backend. It has the same access to the filesystem, network, and system APIs as your own code. This is why treating a plugin like a harmless extension of the browser — the way you might install an npm package without a second thought — is a mistake. See Security Recommendations.
What to Audit Before You Install
- Source code, not just the registry page. Read the
lib.rs,commands.rs, and any mobile implementations. Ask: does this plugin open network connections I didn’t expect? Does it spawn child processes? Does it callstd::process::Commandwith user-supplied input? - Permission declarations. A plugin that declares
fs:allow-allin its default permission set is a red flag unless its entire purpose is file management. Check whether the plugin provides a more restricted permission set (many do) and use that instead. - Dependency tree. Run
cargo tree -p <plugin>to see what it pulls in. A plugin that depends on an outdated, unmaintained cryptography library may introduce vulnerabilities that have nothing to do with Tauri. - Build scripts. A plugin’s
build.rscan execute arbitrary code at compile time. If you see a build script that downloads binaries from an unknown URL, treat it as hostile until proven otherwise.
Limiting the Blast Radius
Even with a plugin you trust, follow the principle of least privilege.
- Scope permissions aggressively. Instead of
opener:allow-open-pathwith no arguments, lock it to"$APPDATA/my-app/**". That way, even if a bug in the plugin tries to read an arbitrary file, the system will block it. - Validate all input from the frontend. A command like
execute_sql(query: String)is a SQL injection vector unless you parameterise it. The fact that the call originates from your own React code does not guarantee safety — an XSS vulnerability in your web view could let an attacker craft malicious commands. - Don’t expose raw system commands. If your plugin wraps
std::process::Command, ensure the frontend can only select from a fixed set of pre-approved operations. Never pass a user-supplied string directly to a shell.
Trust But Verify:
A plugin published on crates.io or npm is not automatically safe. Anyone can publish under a plausible-sounding name. Before adopting a community plugin, scan its source, check who maintains it, and confirm it has not been flagged in the Tauri discord or issue tracker. A few minutes of review can prevent a data leak.
Secure Custom Plugin Development
When you build your own plugin, the same rules apply in reverse.
- Validate every argument you receive from the JS side with serde’s
Deserializeand, where appropriate, additional sanity checks (e.g., clamp numeric ranges, reject empty strings that shouldn’t be empty). - Prefer returning structured error types over panicking. A
Result<T, MyPluginError>that returns a descriptive message to the frontend is far safer than an uncaught panic that takes down the whole Rust process. - If your plugin stores sensitive data (tokens, passwords), use the OS keychain via
tauri-plugin-keyringrather than writing them to disk in plaintext.
Errors You’ll Run Into and How to Fix Them
Certain errors appear so regularly in new Tauri v2 projects that they’re worth memorising. Each one has a specific, non-magical fix. The dedicated Common Plugin Errors page expands on these.
“Plugin did not define its manifest”
Cause: The plugin (often an inline one) hasn’t generated its permission files, or the Tauri build script wasn’t configured to pick them up.
Fix: Add tauri_build::build() to your build.rs and ensure your plugin crate (even if it’s a subfolder of src-tauri) contains a permissions/ directory with at least a default.toml declaring its commands. If you’re embedding the plugin directly without a separate crate, restructure it into a local crate under src-tauri/plugins/ — this isolates its permission manifest and makes the build system aware of it.
“command plugin-name:allow-command not allowed”
Cause: The capability configuration for your window doesn’t include the required permission identifier.
Fix: Open src-tauri/capabilities/default.json and add the exact permission string shown in the error to the permissions array. If the command requires scoped arguments, you must also provide an allow block with the appropriate constraints. Restart the app — the change takes effect immediately in development.
“Unresolved import tauri_plugin_xxx” or “cannot find crate tauri_plugin_xxx”
Cause: The Rust crate for the plugin isn’t listed in Cargo.toml, or its feature flags are mismatched.
Fix: Run cargo add tauri-plugin-xxx from the src-tauri directory, or add it manually. If the crate requires features (like sqlite for the SQL plugin), enable them:
tauri-plugin-sql = { version = "2", features = ["sqlite"] }
“TypeError: plugin.command is not a function” or “Cannot read properties of undefined”
Cause: The JavaScript bindings are either not installed or not initialised. Common triggers: missing npm install @tauri-apps/plugin-xxx, or forgetting to call .plugin(tauri_plugin_xxx::init()) in your Rust setup.
Fix: Verify both the npm package is in package.json and the Rust plugin is registered in lib.rs.
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_sql::init()) // this line is mandatory
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
“Could not load plugin xxx” at startup
Cause: Tauri tried to initialise the plugin but its setup logic returned an error. This can happen if the plugin expects a configuration object in tauri.conf.json that you haven’t provided, or if it accesses a resource that isn’t available.
Fix: Check the plugin’s documentation for required configuration keys under plugins in tauri.conf.json. For example, the SQL plugin needs a database path:
{
"plugins": {
"sql": {
"preload": {
"db": "sqlite:app.db"
}
}
}
}
Zero Plugin Errors Means Everything Is Wired Correctly:
When your app builds, launches, and all plugin calls resolve without console errors, you’ve passed the permission, version, and registration gauntlet. This is the baseline for a healthy Tauri v2 project with plugins.
Summary
Plugin best practices sit at the intersection of dependency management, security engineering, and the Tauri permission model. If you walk away with one rule, make it this: treat every plugin as code you are running, not as a black-box service you are consuming. That means auditing its permissions, pinning its version to a compatible Tauri release, and granting it the narrowest possible access to your app’s internals.
Choosing the Right Plugin
Learn how to evaluate, select, and integrate plugins in your Tauri v2 application, balancing community support, compatibility, and custom development needs.
Version Compatibility
How to manage plugin and dependency versions to ensure your Tauri plugin works across different environments and Tauri releases.
Permission Management
How to configure and understand plugin permissions in Tauri v2 applications including built-in and custom plugins
Security Recommendations
Essential security practices for developing and using Tauri v2 plugins including capability-based permissions, CSP, input validation, and secure IPC
Common Plugin Errors
How to diagnose and fix the most frequent issues when integrating Tauri v2 plugins in a React and Vite application