Auto Updates in Tauri v2

Implement automatic updates for your Tauri v2 desktop app using the updater plugin, from key generation and signing to deployment with static JSON or a dynamic server.

Desktop applications that never update become security risks and support headaches. Tauri v2 provides a built-in updater plugin that lets your app check for new versions, download signed updates, and restart itself — all without the user visiting a website. This guide covers the entire flow: generating cryptographic keys, configuring the plugin, wiring up the frontend in a React + Vite project, hosting the update manifest, and troubleshooting the most common pitfalls. Install and register the plugin as described in the Updater Plugin chapter.

How the Tauri Updater Plugin Works

The plugin operates on a simple principle: your app periodically checks a JSON manifest that describes the latest release. That manifest contains a version number, platform-specific download URLs, and a signature file. The signature is generated with a private key you control; the public key lives inside your app so it can verify that the downloaded update has not been tampered with.

Update flow summary:

The typical sequence is: check → notify user → download and install → relaunch. You can trigger this automatically on startup or in response to a manual "Check for Updates" button.

Think of it as a two-part system. The first part is the update manifest — a JSON file you host somewhere on the internet, containing the version and download links. The second part is the signing infrastructure — a public/private key pair that guarantees the integrity of the update.

Prerequisites and Dependencies

You need a working Tauri v2 project with React and Vite. The following plugins are required:

  • tauri-plugin-updater — the core auto-update logic (Rust crate)
  • @tauri-apps/plugin-updater — the JavaScript API to call from the frontend
  • tauri-plugin-dialog — used to ask the user if they want to install the update
  • tauri-plugin-process — used to restart the app after installation

Add the Rust dependencies to your src-tauri/Cargo.toml:

src-tauri/Cargo.toml
[dependencies]
tauri-plugin-updater = "2"
tauri-plugin-dialog = "2"
tauri-plugin-process = "2"

Install the frontend packages:

npm install @tauri-apps/plugin-updater @tauri-apps/plugin-dialog @tauri-apps/plugin-process

Signing Keys — The Foundation of Trust

Before your app can accept an update, it must verify that the update came from you. The updater enforces this with a public/private key pair, and it cannot be disabled.

Generate the keys with the Tauri CLI. The -w flag writes the key files to a location of your choice.

npm run tauri signer generate -- -w ~/.tauri/myapp.key

You will be prompted to enter a password. After completion, you will have two files:

  • myapp.key — the private key. Never share this. Losing it means you cannot sign updates for existing users.
  • myapp.key.pub — the public key, which you embed in the app's configuration.

Losing the private key is catastrophic:

If you lose the private key or its password, you will not be able to publish updates to users who already have the app installed. Those users would need to manually download and reinstall the application. Store the key in a password manager and keep a secure backup.

Configuration in tauri.conf.json

Open src-tauri/tauri.conf.json and add the updater configuration under plugins. You must also tell the bundler to generate updater artifacts by setting createUpdaterArtifacts to true inside bundle.

src-tauri/tauri.conf.json
{
  "bundle": {
    "createUpdaterArtifacts": true
  },
  "plugins": {
    "updater": {
      "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM4NDVDMEIyQ0ExN0IwRjkKUldUNXNCZktzc0JGT04ycGx0NEJ0SnZiSTdxS3FFVVFGaVlVN3pndkZCU2l4SWlyQnRRdEx6Z3oK",
      "endpoints": [
        "https://releases.myapp.com/{{target}}/{{arch}}/{{current_version}}"
      ],
      "windows": {
        "installMode": "passive"
      }
    }
  }
}

The pubkey field accepts the full content of the myapp.key.pub file, not a file path. Copy and paste the entire string.

The endpoints array lists one or more URLs your app will query to check for updates. Tauri automatically substitutes {{target}}, {{arch}}, and {{current_version}} with the relevant values for the running system. If the first endpoint returns a non-2XX status, Tauri tries the next one.

The windows.installMode setting controls how the installer presents itself on Windows. "passive" (the default) shows a small progress bar without requiring user interaction. "basicUi" requires the user to step through the installer. "quiet" gives no feedback at all and cannot request admin privileges, which makes it only suitable for per-user installations.

Local testing requires HTTP:

If you are testing with a local server using http:// instead of https://, add "dangerousInsecureTransportProtocol": true inside the updater object. Never leave this enabled in production builds.

Permissions

The updater plugin requires explicit permissions in your capabilities file. Open src-tauri/capabilities/default.json (or the relevant capability file) and add:

src-tauri/capabilities/default.json
{
  "permissions": [
    "updater:default",
    "updater:allow-check",
    "updater:allow-download-and-install",
    "dialog:default",
    "dialog:allow-ask",
    "dialog:allow-message",
    "process:allow-restart"
  ]
}

Without these permissions, the frontend calls will fail silently. A missing updater:allow-check means check() returns nothing, and a missing updater:allow-download-and-install will throw a permission error at runtime.

Backend Setup in Rust

Register the plugins in your src-tauri/src/lib.rs:

src-tauri/src/lib.rs
use tauri_plugin_dialog;
use tauri_plugin_process;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_updater::Builder::new().build())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_process::init())
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

The updater plugin is added inside the builder chain. The dialog plugin gives the frontend the ask() and message() functions; the process plugin provides relaunch().

Frontend Implementation with React

Create a reusable update function that checks for updates, shows a dialog, downloads the update, and restarts the app. The following example runs the check automatically when the app mounts and provides a manual button.

src/App.tsx
import { useEffect, useState } from "react";
import { getVersion } from "@tauri-apps/api/app";
import { check } from "@tauri-apps/plugin-updater";
import { ask, message } from "@tauri-apps/plugin-dialog";
import { relaunch } from "@tauri-apps/plugin-process";
function App() {
  const [currentVersion, setCurrentVersion] = useState("");
  const [updateStatus, setUpdateStatus] = useState("");
  useEffect(() => {
    getVersion()
      .then(setCurrentVersion)
      .catch(() => setCurrentVersion("unknown"));
  }, []);
  async function checkForUpdates(userInitiated = false) {
    setUpdateStatus("Checking for updates...");
    try {
      const update = await check();
      if (!update) {
        setUpdateStatus("Failed to check for updates. Try again later.");
        return;
      }
      if (update.available) {
        const answer = await ask(
          `Version ${update.version} is available.\n\n${update.body ?? ""}`,
          {
            title: "Update Available",
            kind: "info",
            okLabel: "Install",
            cancelLabel: "Later",
          }
        );
        if (answer) {
          setUpdateStatus("Downloading and installing...");
          await update.downloadAndInstall();
          setUpdateStatus("Installation complete. Restarting...");
          await relaunch();
        }
      } else if (userInitiated) {
        await message("You are on the latest version.", {
          title: "No Updates",
          kind: "info",
        });
        setUpdateStatus("");
      } else {
        setUpdateStatus("");
      }
    } catch (error) {
      setUpdateStatus(`Update error: ${String(error)}`);
    }
  }
  return (
    <main>
      <h1>My Tauri App</h1>
      <p>Current version: {currentVersion || "loading..."}</p>
      <button onClick={() => checkForUpdates(true)}>
        Check for Updates
      </button>
      <p>{updateStatus}</p>
    </main>
  );
}
export default App;

The check() call queries the endpoint configured in tauri.conf.json. If the server responds with a JSON manifest that contains a higher version, the returned object has available: true, along with version, body (release notes), and a downloadAndInstall() method.

downloadAndInstall() accepts optional callbacks for progress reporting. The example above uses the simplest form, but you can track download progress if you want a custom progress bar. The function blocks until the install finishes. Then relaunch() kills the current process and starts the new version.

When the user manually triggers the check and no update is found, a message dialog confirms they are up to date. On automatic startup checks, the function stays quiet to avoid nagging.

This pattern works for most apps:

The combination of an automatic check on startup plus a manual button is the most common pattern. It ensures users are aware of updates without being interrupted at inconvenient times.

Hosting the Update Manifest and Artifacts

The updater needs two things to be reachable via the endpoint: the update manifest JSON and the actual installer files.

The manifest follows this structure:

latest.json
{
  "version": "1.0.1",
  "notes": "Fixed crash on startup and improved dark mode.",
  "pub_date": "2026-07-09T12:00:00Z",
  "platforms": {
    "windows-x86_64": {
      "signature": "dW50cnVzdGVk... (content of .sig file)",
      "url": "https://github.com/user/repo/releases/download/v1.0.1/app-x64-setup.nsis.zip"
    },
    "darwin-aarch64": {
      "signature": "...",
      "url": "https://github.com/user/repo/releases/download/v1.0.1/app-aarch64.app.tar.gz"
    },
    "linux-x86_64": {
      "signature": "...",
      "url": "https://github.com/user/repo/releases/download/v1.0.1/app-amd64.AppImage.tar.gz"
    }
  }
}

The version field is compared against the running app's version, which is read from tauri.conf.json (the version field). Either 1.0.0 or v1.0.0 is accepted. The notes field appears in the dialog as update.body. The signature is the entire content of the .sig file that Tauri generated during the build.

There are two common hosting approaches:

  • Static JSON file: Upload latest.json and the binary packages to a GitHub Release, an S3 bucket, or a GitHub Gist. The endpoint URL is the raw file URL. Tauri will compare versions and download from the URLs inside the manifest.
  • Dynamic server: The endpoint URL is your own API server. You can inspect {{current_version}}, decide whether to offer an update, and return a JSON response that matches the format above. This is useful for staged rollouts or license-based updates.

CrabNebula Cloud offers a dedicated update service that automatically generates signed manifests and hosts them on a CDN. If you use their service, the endpoint URL is provided by their dashboard.

Step-by-Step Setup Workflow

1

Step 1: Install Dependencies

Add the Rust crates to Cargo.toml and install the npm packages as shown earlier.

npm install @tauri-apps/plugin-updater @tauri-apps/plugin-dialog @tauri-apps/plugin-process
2

Step 2: Generate the Signing Key Pair

Run the tauri signer generate command. Save the private key securely and note the public key content.

npm run tauri signer generate -- -w ~/.tauri/myapp.key
3

Step 3: Configure tauri.conf.json

Set createUpdaterArtifacts to true in bundle, add the updater plugin section with your public key, endpoints, and install mode.

4

Step 4: Register Plugins in Rust

Add .plugin(tauri_plugin_updater::Builder::new().build()) and the dialog and process plugins in lib.rs.

5

Step 5: Update Permissions

Add the required updater, dialog, and process permissions to your capability file.

6

Step 6: Write the Frontend Update Logic

Create the checkForUpdates function and integrate it into your React app. Call it on mount and optionally from a button.

7

Step 7: Build with Signing Environment Variables

Set the environment variables and run the build.

# macOS/Linux
export TAURI_SIGNING_PRIVATE_KEY="path/to/myapp.key"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="your_password"
npm run tauri build
# Windows PowerShell
$env:TAURI_SIGNING_PRIVATE_KEY="path/to/myapp.key"
$env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="your_password"
npm run tauri build

After building, you will find .sig files alongside the installers.

8

Step 8: Prepare and Upload the Manifest

Create latest.json using the signatures and download URLs for the files you just built. Upload everything to your chosen hosting platform and verify the endpoint URL is accessible.

9

Step 9: Test the Complete Flow

Install the older version of your app, bump the version in tauri.conf.json, build the new version, update the manifest, and launch the old app to trigger the update.

Common Pitfalls and Troubleshooting

The updater never detects a new version

The most common cause is a mismatch between the version in the manifest and the version embedded in the running app. Tauri reads the version from the version field in tauri.conf.json, not from Cargo.toml or package.json. Make sure all three are kept in sync, but understand that the updater only uses tauri.conf.json for the comparison.

Another frequent cause is a missing .sig file or an incorrect signature value in latest.json. Double-check that the signature string is the exact content of the file, with no extra whitespace or line breaks altered.

The update downloads but the app stays on the old version

On Windows, if the user installed the app to a custom directory (not the default Program Files path), the MSI upgrade may fail to replace the executable. This is a known issue (Tauri issue #14828). The underlying cause is often a changed product name between versions, which generates a new upgrade code and prevents the MSI from recognizing the existing installation.

To avoid this, keep the productName field in tauri.conf.json identical across versions. If you must change it, you need to manually match the upgrade code to the old product name’s GUID.

Product name changes break Windows MSI updates:

If you rename your app after the first release, existing users with MSI installations will experience an update loop. The new MSI is treated as a separate product and does not overwrite the old one. Define your product name once and stick with it.

Signature verification fails

The public key in tauri.conf.json must be the same one that corresponds to the private key used to sign the build. If you generated a new key pair and did not update the config, verification will fail. Also, the private key password must be correct — a wrong password leads to a corrupted signature.

The update endpoint is unreachable

In production, Tauri enforces TLS (HTTPS). If you are using a local http:// endpoint, you must set dangerousInsecureTransportProtocol: true in the updater config. Without it, the request will be blocked. For production, always use HTTPS.

Summary

Tauri v2’s updater plugin gives you a cryptographically secure, built-in mechanism to deliver new versions to your users. The key pieces are the signing key pair, the JSON manifest, and the frontend integration. Once set up, the flow is automatic: your app checks the manifest, downloads the signed update, and relaunches with the new version.

The most consequential decision you will make is keeping your private key safe and your product name stable. Lose the private key, and you cannot update existing users. Change the product name on Windows, and you break the MSI upgrade path.