Tauri Plugins: Peer, Don't Pin

Publishing a Tauri plugin means letting go of the exact-pinning habits that work for application code. Here's how @tauri-apps/api should be declared, and why.

taurisoftware engineering

For application code, pinning NPM dependencies to exact versions is a sensible default: what you install is what ships.

Libraries are different. A package you publish to NPM runs inside someone else's dependency tree, and exact pins in a published package become version conflicts the consumer has to resolve. Publishing a Tauri plugin recently was a useful reminder that the rules for applications don't automatically transfer to the packages we publish — and that @tauri-apps/api in particular needs special handling.

The dependency trap

A Tauri plugin is an NPM package that extends a host Tauri app. The plugin calls into Tauri's JavaScript API via @tauri-apps/api, and so does the host app. There's one runtime, one webview, one invoke function.

If the plugin declares @tauri-apps/api as a production dependency with an exact pin and the host app uses a different version, NPM installs a second, nested copy inside the plugin's node_modules. While @tauri-apps/api is a thin wrapper over window.__TAURI_INTERNALS__ and two copies usually behave identically at runtime, duplication becomes a problem when a future version introduces a breaking change.

The bug isn't in the plugin. It's in the dependency declaration.

Why a peer dependency

The fix is to declare @tauri-apps/api as a peerDependency. That tells NPM "the host app provides this — don't install your own."

Tauri's own first-party plugins keep @tauri-apps/api in dependencies with a caret range, which lets NPM dedupe against the host app's copy. That works in the common case, but leans on the resolver to do the right thing.

Peer-declaring it is stricter: NPM warns or errors if the host hasn't installed a compatible version, surfacing conflicts at install time rather than letting them ship as silent duplicates. For third-party plugins, where we don't control how the host app is structured, that extra guarantee is worth having.

Why a wide caret range

A permissive caret range — ^2.9.0 — says "we work with 2.9.0 and anything compatible with it." The minimum reflects the oldest version we've tested; the caret accepts every minor and patch release above it. If we later discover a specific incompatibility, we raise the floor. The ceiling stays open.

The rule

So, for our Tauri plugins:

✅ Good — peer dependency with a caret range:

{
   "peerDependencies": {
      "@tauri-apps/api": "^2.9.0"
   }
}

❌ Bad — production dependency (nested duplicate risk):

{
   "dependencies": {
      "@tauri-apps/api": "2.9.1"
   }
}

❌ Bad — exact peer dependency (breaks consumers on any different version):

{
   "peerDependencies": {
      "@tauri-apps/api": "2.9.1"
   }
}

None of this is special to Tauri. Peer dependencies for host-provided runtimes are the standard pattern across NPM — React, Vue, Svelte, RxJS, and every mature plugin ecosystem work the same way. The only Tauri-specific thing here is remembering to apply the pattern to @tauri-apps/api. Any other dependencies can be managed according to your preferred pinning style.

Library code isn't application code. When a package plugs into a shared runtime, the right question isn't "how tightly can we pin this?" but "who should own this version?" For host-provided runtimes, the answer is almost always the host.


Tauri is a trademark of the Tauri Programme within The Commons Conservancy.