Native APIs in Tauri v2

How Tauri v2 exposes operating system features to your React frontend through built-in modules, plugin-based APIs, and a secure inter-process communication model.

Desktop applications need capabilities that web browsers deliberately restrict — reading and writing files, showing system notifications, interacting with the clipboard, spawning external processes, and accessing hardware information. A React app running in a normal browser tab gets none of this. A Tauri application is still a web app, but it runs inside a native webview container that ships with a Rust backend. That backend can safely reach into the operating system, and the frontend can talk to it through Tauri's native API system.

This document explains how that system is structured. You will learn what the built-in APIs cover, how the official plugin ecosystem extends them, how the inter-process communication bridge works under the hood, and what responsibilities the frontend and backend each carry.

What Native APIs Provide

A native API in Tauri is any functionality that lets your React code perform an operation the browser sandbox would deny — reading a file, opening a save dialog, writing to the clipboard, or querying the app's installation path. These operations are not exposed through standard web APIs like fetch or localStorage. They require a bridge into the operating system, and Tauri's entire architecture is built around making that bridge predictable, auditable, and secure.

From the frontend developer's perspective, a native API call looks almost identical to importing and calling a function from any JavaScript library:

import { appDataDir } from '@tauri-apps/api/path';
const dir = await appDataDir();

That one-liner did something a plain website cannot: it resolved the current application's data directory according to the operating system's conventions. The call crossed from the JavaScript runtime into a Rust process, queried the OS, and returned the result.

What Native APIs are Not:

Native APIs are not direct OS syscall wrappers. They pass through Tauri's permission model, which means even if a function exists in the Rust code, the frontend can only call it if the app's capability configuration explicitly allows it. This layer exists even for the simplest built-in APIs.

How Communication Works — The IPC Model

The frontend (React, Vite, the HTML/CSS/JS bundle) runs inside a system webview — WebKit on macOS and Linux, WebView2 on Windows. That webview process is isolated from the Rust core process. They communicate through an inter-process communication (IPC) channel that Tauri sets up automatically when the app launches.

Two IPC patterns matter for native APIs:

Commands (request–response) — The frontend calls a named command with optional arguments. The Rust backend executes the matching handler function and returns a value. This is how you read a config file, query system information, or perform any synchronous or async operation where you need an answer.

React (WebView)                  Rust Backend
     |                                |
     |-- invoke('read_config') ------>|
     |                                |-- read file from disk
     |<-- return file contents -------|

Events (push from backend to frontend) — The Rust side emits an event with a payload, and any frontend listeners registered for that event name receive it. This is useful for long-running tasks like download progress, file watcher notifications, or system tray interactions where the user might click an icon and the backend needs to notify the UI.

React (WebView)                  Rust Backend
     |                                |
     |-- listen('progress') -------->|  (registers listener)
     |                                |-- task progresses
     |<-- emit('progress', 42%) -----|

Under the hood, the @tauri-apps/api package you install from npm provides typed wrappers around these primitives. When you call appDataDir() from the path module, the library serializes your request, sends it through the IPC bridge as a command, deserializes the response, and returns a promise. You never need to write raw IPC calls — but knowing the model exists helps you understand where errors come from and why permissions matter.

Commands Must Be Registered:

Every command the frontend can call must be explicitly registered in the Rust backend's invocation handler. If you write a #[tauri::command] function but forget to add it to .invoke_handler(tauri::generate_handler![...]), the frontend will receive a "command not found" error, not a Rust compilation error. This is a common debugging pitfall.

Built-in APIs — What Ships with Tauri Core

Tauri v2 ships with a set of APIs that require no additional plugins. They are part of the core Rust crate and exposed through the @tauri-apps/api JavaScript package. These APIs cover the essential bridge between the webview and the native windowing system, plus utilities every desktop app needs.

The modules accessible from @tauri-apps/api are:

ModulePurpose
appRead app metadata — name, version, tauri version.
coreLow-level primitives like invoke, convertFileSrc, and transformCallback.
dpiPhysical and logical position/size types for window geometry.
eventListen and emit events between frontend and backend.
imageCreate image objects from raw bytes, used by menu and tray APIs.
menuBuild native application menus and context menus.
mocksMock IPC calls for testing frontend code without a live backend.
pathResolve standard system directories — app data, cache, desktop, downloads, and more.
trayConfigure and control the system tray icon and its menu.
webviewCreate and manage additional webview windows.
webviewWindowConvenience wrapper for webview-specific window operations.
windowManage the main application window — size, position, title, fullscreen, and more.

Every one of these modules calls into Rust code that is already compiled into your Tauri binary. You do not need to add any crate to Cargo.toml or register any plugin. You do, however, need to grant the appropriate permissions through Tauri's capability system — a topic covered in detail in the Permissions & Security section.

A small example that uses a built-in API without writing a single line of Rust:

// src/App.tsx
import { appDataDir } from '@tauri-apps/api/path';
import { useState, useEffect } from 'react';
function DataDirDisplay() {
  const [dir, setDir] = useState('');
  useEffect(() => {
    appDataDir()
      .then(setDir)
      .catch((err) => console.error('Failed to get app data dir:', err));
  }, []);
  return <p>App data directory: {dir || 'Loading...'}</p>;
}
export default DataDirDisplay;

The appDataDir function is not reading a JavaScript variable. It sends a command through IPC to Rust, which calls the operating system to resolve the path (something like /Users/you/Library/Application Support/com.yourapp on macOS, or C:\Users\you\AppData\Roaming\com.yourapp on Windows), and returns that string to the frontend. The entire round trip is hidden behind a clean async function.

No Custom Rust Required:

If your app's needs are fully covered by the built-in modules and official plugins, you may never need to write a custom Rust command. The existing APIs handle windowing, paths, events, menus, and more. This is a realistic path for many small utilities.

Plugin-Based APIs — The Official Ecosystem

When the built-in APIs are not enough — you need to read files, show dialogs, send notifications, or make HTTP requests — you reach for plugins. Tauri's plugin system is the standard extension mechanism. It works by adding a Rust crate on the backend and a corresponding JavaScript package on the frontend, then wiring them together with a single line of Rust registration.

The Tauri project maintains an official collection of plugins covering the most common desktop application needs:

  • tauri-plugin-fsFile system read, write, and directory operations.
  • tauri-plugin-dialog — Native file open, file save, and message dialogs.
  • tauri-plugin-clipboard-manager — Read and write text, images, and HTML to the system clipboard.
  • tauri-plugin-notification — Send desktop notifications.
  • tauri-plugin-shellOpen URLs in the default browser, execute shell commands, and spawn sidecar processes.
  • tauri-plugin-http — Make HTTP requests from the Rust backend (bypassing browser CORS restrictions).
  • tauri-plugin-updater — In-app update checking and installation.
  • tauri-plugin-process — Restart the app, exit with a specific code, or access process information. See the Process plugin.
  • tauri-plugin-os — Query OS type, version, architecture, and locale.
  • tauri-plugin-store — A simple persistent key–value store.

These plugins are not bundled into every Tauri app. You opt into them, which keeps the final binary small — an app that never touches the clipboard does not carry clipboard-handling code.

1

Add the Rust crate

From the src-tauri directory, run Cargo to add the plugin:

cargo add tauri-plugin-fs

This updates Cargo.toml with the dependency.

2

Register the plugin in your Rust entry point

Open src-tauri/src/main.rs and add the plugin to the Tauri builder chain:

// src-tauri/src/main.rs
fn main() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init()) // register the plugin
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The plugin's init() function registers all its commands with the IPC handler.

3

Install the JavaScript package

From the project root, install the companion npm package:

npm install @tauri-apps/plugin-fs
4

Configure permissions

Tauri v2 will not let the frontend call any plugin command until you grant capabilities. In a capability file (typically src-tauri/capabilities/default.json), add the permissions the plugin requires:

{
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "fs:default",
    "fs:allow-read-text-file",
    "fs:allow-write-text-file"
  ]
}

Without this step, the frontend call will fail with a permission error.

After these four steps, your React components can import and use the plugin just like a built-in module:

import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
async function saveNote(note: string) {
  await writeTextFile('notes.txt', note);
}

The plugin ecosystem follows a consistent pattern: Rust crate, JavaScript package, registration call, and capability permissions. Once you learn this flow for one plugin, you can apply it to any official or community plugin.

Missing Permission Is the Most Common Failure:

If you add a plugin, register it, install the JS package, but forget to grant the required permissions in the capability file, the IPC call will fail with a cryptic error. Always check the permissions list first when a plugin call breaks. The Permissions & Security chapter covers this in detail.

Backend and Frontend Responsibilities

The division of labor between the React frontend and the Rust backend is not arbitrary — it follows from the security model. The webview is treated as untrusted. It cannot access the file system, network sockets, or process management directly. All privileged operations must go through Rust.

Frontend (React)

  • Renders the user interface and handles user interaction.
  • Calls native APIs through the @tauri-apps/api package or plugin modules.
  • Never directly touches the operating system. Every file read, notification, or dialog is a request sent across IPC.
  • Handles loading states, errors, and data display exactly as it would with a REST API — the native API calls are async promises that either resolve with data or reject with an error.

Backend (Rust)

  • Registers command handlers and plugin initializers.
  • Receives IPC requests, validates them against the capability permissions, and executes the privileged operation.
  • Interacts with the operating system: reading files, writing to disk, spawning processes, creating windows.
  • Emits events to the frontend for long-running or asynchronous notifications.
  • Enforces the security boundary — the Rust code decides what is allowed, not the frontend.

A concrete example makes this clearer. Imagine an app that needs to save user preferences. The React component calls writeTextFile from the fs plugin. The JavaScript library serializes the command and sends it over IPC. The Rust plugin handler checks if the current capability configuration permits writing to the requested path (based on the scope rules you defined). If permitted, it performs the write and returns success. If denied, it returns a permission error — and no file is touched.

This separation means that even if an attacker manages to inject arbitrary JavaScript into the webview (through a compromised dependency or an XSS vulnerability), they still cannot perform operations the capability file does not allow. The Rust core is the gatekeeper.

A Custom Command from Scratch

Sometimes no existing API or plugin does exactly what you need — for example, a specialized calculation, a custom hardware interaction, or a business rule that must live in Rust for performance reasons. Tauri makes it straightforward to define your own commands.

The Rust side defines the function and registers it:

// src-tauri/src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
#[tauri::command]
fn calculate_fibonacci(n: u64) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            let mut a = 0;
            let mut b = 1;
            for _ in 2..=n {
                let temp = a + b;
                a = b;
                b = temp;
            }
            b
        }
    }
}
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![calculate_fibonacci])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The frontend calls it the same way it would call any built-in command. The Connecting Backend to Frontend chapter covers this invoke pattern in more detail.

// src/App.tsx
import { invoke } from '@tauri-apps/api/core';
import { useState } from 'react';
function FibonacciCalculator() {
  const [input, setInput] = useState('10');
  const [result, setResult] = useState<number | null>(null);
  const [error, setError] = useState<string | null>(null);
  const calculate = async () => {
    try {
      setError(null);
      const n = parseInt(input, 10);
      const fib = await invoke<number>('calculate_fibonacci', { n });
      setResult(fib);
    } catch (err) {
      setError(String(err));
    }
  };
  return (
    <div>
      <input value={input} onChange={(e) => setInput(e.target.value)} />
      <button onClick={calculate}>Calculate Fibonacci</button>
      {result !== null && <p>Result: {result}</p>}
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}
    </div>
  );
}
export default FibonacciCalculator;

This custom command does not require a plugin, but it still goes through the same IPC bridge and the same permission checks. The invoke function sends a serialized request with the command name and arguments, waits for the Rust handler to finish, and returns the deserialized result. The frontend has no idea whether the function came from a plugin or from custom code — the call pattern is identical.

Command Names Are Case-Sensitive Strings:

The first argument to invoke must exactly match the Rust function name converted to snake_case (the default). If you name your Rust function calculateFibonacci in camelCase, Tauri will still register it as calculate_fibonacci because of Rust's naming convention normalization. Always use snake_case names for commands to avoid confusion.

If You See the Result, IPC Is Working:

A working Fibonacci calculation confirms several things at once: the command handler is registered, the frontend can invoke it, the arguments are serialized and deserialized correctly, and the response reaches the React component. This simple test is useful when setting up a new Tauri project.

Choosing Between Built-in, Plugin, and Custom APIs

When you need to add a new native capability to your app, the decision flow is simple:

  • Check if a built-in module already covers it (window, path, event, app, etc.).
  • If not, check the official plugin list (fs, dialog, clipboard, notification, shell, http, etc.).
  • If neither fits, write a custom Rust command.

Most real applications use a mix: built-in APIs for window management and paths, official plugins for file system and dialogs, and a handful of custom commands for business logic that belongs close to the OS or benefits from Rust's performance.

Summary

Native APIs are the mechanism that turns a web app inside a webview into a real desktop application. They are not magic — they are structured IPC calls from JavaScript to Rust, governed by a permission model that puts the backend in control.

You now know that Tauri ships with a set of built-in APIs covering windowing, events, paths, and basic app metadata — usable immediately, no plugins required. You know that the official plugin ecosystem extends this with file system access, dialogs, notifications, clipboard operations, and more, all installable through a consistent four-step pattern: add the crate, register the plugin, install the JS package, configure permissions. And you know how to write your own Rust commands when no existing API fits.

Everything in Tauri's native API surface runs through the same IPC bridge. Whether you call appDataDir() from the built-in path module, writeTextFile() from the fs plugin, or a custom calculate_fibonacci command, the flow is identical: frontend invokes, backend executes, result returns. The security boundary never moves.