Application Updates
How to manage versioning, plan your update strategy, and prepare releases in Tauri v2 so users always run the latest version of your desktop application
Desktop applications do not refresh themselves when you open a browser tab. If you fix a bug or add a feature, users keep running the old binary until they manually download and install the new one. Tauri v2 ships a built-in updater plugin that removes this friction—it checks a remote endpoint, downloads a signed update package, and installs it inside your existing app install.
This section covers everything you need to design, set up, and test an update system: version management, choosing an update strategy, and preparing future releases with the tauri-plugin-updater. Automatic install-on-launch is in Auto Updates.
Version Management
Every update decision hinges on version numbers. The updater compares the version of the installed app with the version advertised by your release server. If the advertised version is higher, an update is available.
Tauri v2 reads the version from your tauri.conf.json file. However, your app’s identity is spread across three files, and they must agree:
| File | Key | Purpose |
|---|---|---|
src-tauri/tauri.conf.json | version | Used by the updater for version comparison |
src-tauri/Cargo.toml | package.version | Rust crate version; shown in some platform dialogs |
package.json | version | Frontend project version; informational |
If tauri.conf.json says "1.0.0" but Cargo.toml says "0.9.0", no build error will stop you—but the updater will advertise itself as version 1.0.0 while the compiled binary metadata claims otherwise. Users get inconsistent information, and platform-specific installers may clash when upgrading.
Version Mismatch Causes Silent Failures:
If the version in tauri.conf.json does not match the version in the update manifest (latest.json), the updater may either skip a valid update or prompt for a downgrade. Always bump all three version fields together before building a release.
A practical approach is to use a script or a workspace tool that syncs all versions from a single source. If you use semantic versioning (major.minor.patch), decide whether the updater should treat pre-release tags (-beta.1) as newer or older than the stable release. Tauri compares versions using the semver crate, so 1.0.0 is newer than 1.0.0-beta.1 by default.
Update Strategy
Not every app updates the same way. Before wiring the updater plugin, decide how aggressive the update flow should be.
User-Prompted Updates
This is the safest default. When the app starts (or when the user clicks a "Check for Updates" button), the frontend calls the updater’s check() function. If a newer version exists, a dialog asks the user to install it now or later.
// src/updater.ts
import { check } from "@tauri-apps/plugin-updater";
import { ask } from "@tauri-apps/plugin-dialog";
export async function checkForAppUpdates() {
const update = await check();
if (update?.available) {
const yes = await ask(
`Update to ${update.version} is available.\n\nRelease notes:\n${update.body}`,
{ title: "Update Available", kind: "info", okLabel: "Update", cancelLabel: "Later" }
);
if (yes) {
await update.downloadAndInstall();
// Restart the app (via process plugin) or ask user to relaunch
}
}
}
In this flow, update.downloadAndInstall() downloads the binary, verifies its signature, and stages it for replacement. The app must restart to complete the swap; you can use @tauri-apps/plugin-process to invoke a relaunch.
Silent Background Updates
Some apps want to download the update without interrupting the user, then apply it on the next restart. Tauri’s updater supports this, but you must be careful about network usage and user trust.
Set "dialog": false in the updater configuration to suppress the built-in update dialog. Then call check() periodically and, if an update is available, call downloadAndInstall() in the background. The app will still require a restart to run the new version.
Silent Updates and User Consent:
Forcing a download without telling the user can feel intrusive, especially on metered connections. If you ship a silent updater, make it opt-in and clearly document the behavior. A notification after the download finishes ("Update ready. Restart to apply.") is a good middle ground.
Phased Rollouts and Canary Releases
For applications with a large user base, releasing to everyone at once is risky. You can split your update endpoint to serve different manifest files based on a user’s cohort or a beta flag stored locally. The endpoint URL in tauri.conf.json can include dynamic segments, and the updater plugin replaces placeholders like {{current_version}} and {{target}}. For phased rollouts, you might:
- Point a subset of users to a canary endpoint that returns a newer version early.
- Use a server-side rule that checks a unique device ID (sent as a query parameter you append in your
check()wrapper) and decides whether to offer the update.
This keeps the core updater simple while giving you rollout control on the server side.
Preparing Future Releases
Before you can push an update, the app must be prepared to verify it. Tauri uses public-key cryptography: you generate a private key to sign update packages, and the public key is baked into the app. Every release you publish must be signed with that private key, and the app refuses to install anything that fails verification.
Generating the Keypair
The updater plugin CLI generates an encrypted keypair. Run the command below and store the private key and its password securely—losing either means you cannot sign updates for that app.
Step 1: Generate the keypair
Open a terminal in your Tauri project and run:
npm run tauri signer generate -- -w ~/.tauri/myapp.key
You will be prompted for a password. This encrypts the private key on disk. Two files are created: myapp.key (private) and myapp.key.pub (public).
Step 2: Embed the public key
Open the .pub file and copy its entire content. Paste it into the plugins.updater.pubkey field of tauri.conf.json:
{
"plugins": {
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDk2QjZCN0..."
}
}
}
This key is public—it can be safely committed to your repository.
Step 3: Store the private key as an environment variable
For local builds, export the key and its password in your shell:
export TAURI_SIGNING_PRIVATE_KEY="$(cat ~/.tauri/myapp.key)"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="your-password"
For CI/CD, add both as encrypted secrets. Never commit the private key to your repository.
The Private Key is Irreplaceable:
If you lose the private key or its password, you cannot sign update packages for existing installations. Users would have to manually download and reinstall the app to resume receiving updates. Back it up using a password manager or a hardware security module.
Update Endpoints
The updater plugin needs at least one URL that returns a JSON manifest describing the latest version. Tauri calls this endpoint during check() and compares the version field with the app’s own version.
{
"version": "1.2.0",
"notes": "Fixed crash on startup and improved dark mode support.",
"pub_date": "2026-07-09T14:30:00Z",
"platforms": {
"darwin-aarch64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
"url": "https://github.com/user/repo/releases/download/v1.2.0/app-aarch64.dmg"
},
"windows-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
"url": "https://github.com/user/repo/releases/download/v1.2.0/app-x64-setup.exe"
},
"linux-x86_64": {
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6...",
"url": "https://github.com/user/repo/releases/download/v1.2.0/app-amd64.AppImage"
}
}
}
Each platform entry contains a signature (the base64 signature of the install file) and a url pointing to the signed binary. The updater downloads the file, verifies the signature against the embedded public key, and then installs it.
Where does this JSON live? Two common approaches:
Use the createUpdaterArtifacts option in Tauri's bundler. When enabled, tauri build generates a latest.json file and signature files alongside your installers. You then upload these to a GitHub Release. The endpoint URL becomes a raw file link:
{
"plugins": {
"updater": {
"endpoints": [
"https://github.com/USER/REPO/releases/latest/download/latest.json"
]
}
}
}
This approach works with public repositories. Private repos need an access token appended to the URL.
Integrating the Updater Plugin
With the keypair generated and the endpoint configured, wire the plugin into your Rust backend and your React frontend.
Rust Setup
Add the plugin dependencies to Cargo.toml:
[dependencies]
tauri-plugin-updater = "2"
tauri-plugin-dialog = "2"
tauri-plugin-process = "2"
Initialize them in your app builder. The updater plugin must be added inside a setup closure to ensure it registers correctly on desktop platforms:
use tauri_plugin_updater;
use tauri_plugin_dialog;
use tauri_plugin_process;
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_process::init())
.setup(|app| {
#[cfg(desktop)]
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
The #[cfg(desktop)] attribute is important—the updater is only relevant on desktop. This compiles cleanly for mobile targets if you later add them.
Capabilities
The updater plugin needs explicit permissions to function. Open your capabilities file (usually src-tauri/capabilities/main.json) and add:
{
"identifier": "main",
"description": "permissions for desktop app",
"local": true,
"windows": ["main"],
"permissions": [
"dialog:default",
"dialog:allow-ask",
"dialog:allow-message",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"process:allow-restart"
]
}
If you skip these, the frontend calls will fail with permission errors.
Frontend Check
The @tauri-apps/plugin-updater npm package exposes a check() function that contacts your endpoint, compares versions, and returns an update object if one is available. Here is a complete React integration that checks on app launch and on a manual button click:
import { useEffect } from "react";
import { check } from "@tauri-apps/plugin-updater";
import { ask, message } from "@tauri-apps/plugin-dialog";
import { relaunch } from "@tauri-apps/plugin-process";
function App() {
useEffect(() => {
checkForUpdates(false);
}, []);
async function checkForUpdates(isManual: boolean) {
const update = await check();
if (update === null) {
if (isManual) {
await message("Failed to check for updates. Check your network connection and try again.", {
title: "Error",
kind: "error",
});
}
return;
}
if (update.available) {
const yes = await ask(
`Update to version ${update.version} is available.\n\nRelease notes:\n${update.body}`,
{
title: "Update Available",
kind: "info",
okLabel: "Update Now",
cancelLabel: "Later",
}
);
if (yes) {
await update.downloadAndInstall();
await relaunch();
}
} else if (isManual) {
await message("You are on the latest version.", {
title: "No Update Available",
kind: "info",
});
}
}
return (
<div>
<h1>My Tauri App</h1>
<button onClick={() => checkForUpdates(true)}>Check for Updates</button>
</div>
);
}
export default App;
When check() returns null, the endpoint could not be reached. This is distinct from "no update available"—distinguishing the two gives the user clearer feedback.
Everything Is Wired Correctly:
To verify your setup without publishing a real release, bump the version in your latest.json above the current app version and run the app locally. If the update dialog appears, the endpoint, keys, and plugin are all connected correctly.
Common Misconceptions
- The updater works over IPC like other plugins. It does not. The updater contacts your endpoint directly from the Rust side using an HTTP client. The frontend calls
check()which triggers the internal fetch, version comparison, and signature verification. If the endpoint is behind authentication, configure the plugin with appropriate headers. - The app will update while it is running. The download and signature verification happen while the app is open, but the actual binary replacement occurs when the app exits. Tauri's updater uses a small helper process to swap the old binary with the new one during shutdown. This is why you must restart or relaunch after
downloadAndInstall(). - You can skip key generation during development. Without a keypair, the updater plugin cannot sign packages, and any endpoint with a
signaturefield will fail verification. You can develop without the updater, but testing the full flow requires a valid keypair and a manifest with matching signatures. - Linux AppImage updates are the same as macOS and Windows. AppImage requires special handling because of glibc version dependencies. Tauri v2 requires
webkit2gtk-4.1, which is available on Ubuntu 22.04 and newer, but older distributions may not support it. If you target older Linux versions, consider Flatpak as an alternative distribution channel that sandboxes dependencies.
Summary
Application updates in Tauri v2 are not an afterthought—they are a core plugin that ties your release process directly to the user’s desktop. The three pillars you control are version management (consistent version bumps across all manifest files), update strategy (how aggressively you prompt users), and release preparation (keypair generation, manifest hosting, and signature embedding).
The updater plugin gives you a secure, signed pipeline that resists tampering. When combined with a CI/CD workflow that builds, signs, and publishes releases automatically, the whole system becomes a one-time setup.