A small demo app taking a 1.3.0 → 1.4.0 update as a 714 KB delta instead of a 15.9 MB full download. The ratio depends on what changed between releases.
Tauri's updater re-downloads the whole installer on every release. This plugin downloads only what changed. Same manifest, same signing key, and when a delta can't be used it falls back to a normal full download.
- Windows x86_64 NSIS and macOS
.app.tar.gz - Benchmark with ~6 MiB of new media: 7.0 MB instead of 106 MB on Windows, 6.9 MB instead of 78 MB on macOS (research F42)
- Signature re-verified, downgrades refused, untrusted cache re-checked
- Two
.plugin(...)lines in your app plus a release step → Quickstart
Differential updates for Tauri v2. The plugin keeps Tauri's official update
check and installer, but may reconstruct the exact published artifact from a
smaller patch before handing it to tauri-plugin-updater.
This is pre-release software. The supported v0.2 paths are macOS
.app.tar.gz and Windows x86_64 NSIS -setup.exe. Linux, Windows MSI,
and Windows ARM64 client support are not claimed, and the final security audit
and release gate have not happened yet.
- A real macOS app completed
1.0.0 → 1.0.1via Full, relaunched with its verified artifact ACTIVE, then completed1.0.1 → 1.0.2via TarDelta through the real Tauri installer. - The harness asserts the update source and exact installed binary hashes, so a Full fallback cannot masquerade as a delta success.
- A real Windows x86_64 NSIS installation completed
1.0.0 -> 1.0.1via Full, promoted only after relaunch, then completed1.0.1 -> 1.0.2via DirectDelta. A corrupt cached installer degraded to Full and still installed the exact expected executable. - With the installer hook, a real Windows NSIS installation with an empty cache took its first update as DirectDelta from the installer kept at install time, and the updater's own install refreshed that copy (research F46).
- The original solid-LZMA NSIS experiment produced patches that were 98.2520% and 98.2523% of Full. The Windows updater artifact now disables NSIS compression so unchanged bytes remain reusable, and the release tool refuses to publish a direct patch unless it is strictly below 30% of Full.
- On controlled example-app pairs, a direct compressed-artifact patch was 95.5–96.1% of a Full download while a tar-layer patch was 16.0–16.1% (release-candidate build, 2026-08-14; earlier controlled runs measured 15.0–15.4% for the tar layer). These are measurements of those builds, whose only difference is a version string. A real application's ratio depends entirely on what changed between releases and will differ.
- Final artifact signature verification, authenticated release identity, bounded reconstruction, cache re-verification, and release-time patch round-trips are enforced and tested.
Those ratios come from releases that differ by little more than a version string. On a benchmark app carrying ~88 MiB of bundled assets, with a feature-sized change that replaces and adds about 6 MiB of incompressible media and edits some data, the download was 6.6% of Full on Windows (7.0 of 106 MB) and 8.8% on macOS (6.9 of 78 MB); a client two releases behind paid about the same (research F42). That is one controlled payload, not a promise: a real application's ratio depends on how much of it changes.
On Windows a first update can be a delta too, when the app ships the installer hook below, and a Full download is served from a zstd copy of about LZMA size (research F45, F46).
GitHub-hosted HTTPS Full→TarDelta, Apple Developer ID/notarized, and Windows Authenticode-signed end-to-end tests remain credential-bound validation gaps. See Releasing and the evidence ledger in research/FINDINGS.md.
- Rust 1.88 or newer.
- A Tauri v2 application already using updater artifacts: a macOS
.app.tar.gzand/or a Windows x86_64 NSIS-setup.exe. tauri-plugin-updater2.10.1 or 2.11.0. The delta plugin deliberately supports>=2.10.1, <2.12.0because its security assumptions were verified by reading those two upstream implementations. 2.12.0 is not supported yet; see Decisions #39.- An HTTPS location for
manifest.json, the full artifacts, and patches. - A Tauri updater signing key. Keep the private key out of source control.
Add both plugins from crates.io:
cargo add tauri-plugin-updater@=2.10.1
cargo add tauri-plugin-updater-deltafn main() {
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_updater_delta::Builder::new().build())
.run(tauri::generate_context!())
.expect("error while running the Tauri application");
}Builder::new() is the normal configuration. It derives the app identifier,
updater public key, platform, architecture, cache namespace, cache location,
transaction location, transport policy, and local safety ceilings.
The existing Tauri updater configuration remains authoritative:
{
"bundle": {
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"endpoints": [
"https://releases.example.com/{{target}}/{{arch}}/manifest.json"
],
"pubkey": "YOUR_BASE64_TAURI_MINISIGN_PUBLIC_KEY"
}
}
}For Windows NSIS updater artifacts, use a platform configuration such as
tauri.windows.conf.json to disable solid installer compression:
{
"bundle": {
"windows": {
"nsis": {
"compression": "none"
}
}
}
}This trades a larger Full updater artifact for much smaller later deltas. Pass
--compressed-full-out and --compressed-full-url to delta-release as well:
plugin clients that need the whole installer then download a zstd copy and
rebuild the exact installer from it, so they do not pay for the larger Full
artifact. Stock Tauri clients keep using the uncompressed URL.
To make the first Windows update a delta too, ship the installer hook from
examples/desktop-app/windows/delta-seed.nsh
("installerHooks" in the same nsis block). It keeps a copy of the installer
in the install directory; the plugin uses it as the patch base only if it
matches the base a patch declares (Decisions #41). The
first update from an older solid-LZMA release may use Full; that successful
update seeds the delta-friendly installer used as the next release's base.
The public key must be present in tauri.conf.json; the delta plugin reads the
same configured value. There is no delta-specific manifest URL and no second
manifest request.
use tauri_plugin_updater_delta::{DeltaUpdaterExt, Outcome};
async fn check_for_updates(app: tauri::AppHandle) -> Result<String, String> {
let Some(update) = app
.delta_updater()
.check()
.await
.map_err(|error| error.to_string())?
else {
return Ok("up-to-date".to_owned());
};
let outcome = update
.install()
.await
.map_err(|error| error.to_string())?;
for diagnostic in outcome.diagnostics() {
// Installation succeeded. This warning means a future update may need
// Full again because the cache was unavailable or could not be written.
log::warn!("{diagnostic}");
}
Ok(match outcome {
Outcome::InstalledFromFullDownload { .. } => "installed-full",
Outcome::InstalledFromDirectDelta { .. } => "installed-direct-delta",
Outcome::InstalledFromTarDelta { .. } => "installed-tar-delta",
Outcome::UpToDate { .. } => "up-to-date",
_ => "installed",
}
.to_owned())
}For a frontend button, expose that Rust function as a normal #[tauri::command]
and invoke it from the frontend. v0.2 is intentionally Rust-first; it does not
add a second JS/TS updater SDK or expose identity, cache, and verification
internals to frontend code. The complete version is
examples/desktop-app/src/update.rs.
check_with and check_and_install_with accept a callback for truthful coarse
phases: Checking, Downloading, Reconstructing, Verifying, Installing, and
Finished. Downloads also report DownloadProgress { downloaded, total } byte
counts, where total is the server's advertised length when it sent one. The
other phases report no percentage, because the underlying work cannot report
one accurately. An Err is the failure signal.
A successful Outcome reports what the update cost: downloaded_bytes(),
full_artifact_size() and bytes_saved() (zero for a Full download).
Install the release tool:
cargo install --locked tauri-updater-delta-releaseUse the same private key Tauri uses:
export TAURI_SIGNING_PRIVATE_KEY="$(cat /secure/path/my-app.key)"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="your-real-password"The password must be a real one. A key generated with
tauri signer generate --password ""cannot be read by this tool at all: Tauri's key generation encrypts even with an empty password, and theminisigncrate used here does not. The failure is reported clearly, but it is easier to avoid than to diagnose — generate the key with a password you actually keep.
For the first published updater release, omit predecessor flags. This produces a
valid signed Full-only manifest.json:
delta-release \
--app-id com.example.myapp \
--platform darwin-aarch64 \
--target-version 1.0.1 \
--new-installer dist/MyApp.app.tar.gz \
--installer-url https://releases.example.com/v1.0.1/MyApp.app.tar.gz \
--signature-out dist/MyApp.app.tar.gz.sig \
--manifest dist/manifest.jsonFor a later release, repeat the predecessor flags to add direct-to-current patches from every previous version you want to retain. On macOS, add one tar-layer output pair for each predecessor in the same order:
delta-release \
--app-id com.example.myapp \
--platform darwin-aarch64 \
--target-version 1.0.3 \
--new-installer dist/MyApp.app.tar.gz \
--installer-url https://releases.example.com/v1.0.3/MyApp.app.tar.gz \
--from-version 1.0.2 \
--previous-installer dist/MyApp-1.0.2.app.tar.gz \
--patch-url https://releases.example.com/v1.0.3/1.0.2-to-1.0.3.zst \
--patch-out dist/1.0.2-to-1.0.3.zst \
--tar-patch-url https://releases.example.com/v1.0.3/1.0.2-to-1.0.3.tar.zst \
--tar-patch-out dist/1.0.2-to-1.0.3.tar.zst \
--from-version 1.0.1 \
--previous-installer dist/MyApp-1.0.1.app.tar.gz \
--patch-url https://releases.example.com/v1.0.3/1.0.1-to-1.0.3.zst \
--patch-out dist/1.0.1-to-1.0.3.zst \
--tar-patch-url https://releases.example.com/v1.0.3/1.0.1-to-1.0.3.tar.zst \
--tar-patch-out dist/1.0.1-to-1.0.3.tar.zst \
--require-tar-layer \
--signature-out dist/MyApp.app.tar.gz.sig \
--manifest dist/manifest.jsonFor a Windows NSIS release, fold the windows-x86_64 entry into the same
manifest.json. There is no tar layer on Windows, so omit the --tar-patch-*
flags and never pass --require-tar-layer:
delta-release \
--app-id com.example.myapp \
--platform windows-x86_64 \
--target-version 1.0.2 \
--from-version 1.0.1 \
--previous-installer dist/MyApp_1.0.1_x64-setup.exe \
--new-installer dist/MyApp_1.0.2_x64-setup.exe \
--installer-url https://releases.example.com/v1.0.2/MyApp_1.0.2_x64-setup.exe \
--patch-url https://releases.example.com/v1.0.2/1.0.1-to-1.0.2-windows.zst \
--patch-out dist/1.0.1-to-1.0.2-windows.zst \
--signature-out dist/MyApp_1.0.2_x64-setup.exe.sig \
--manifest dist/manifest.jsonThe four direct-patch predecessor flags must be repeated the same number of times, in matching order. The two tar-patch flags are either omitted or repeated once per predecessor. Every direct and tar patch is applied and checked before its metadata is published.
Each direct patch is published only when it is strictly below 30% of the full
installer (--max-direct-patch-percent). Oversized patches are deleted
independently: other qualifying versions retain their patches, while a client on
an omitted version safely uses Full.
Use darwin-x86_64 for an Intel build. --app-id, platform, and target version
are explicit because they enter the cryptographically authenticated release
identity; guessing them would hide a real security decision. The release tool
derives artifact digest and size itself and applies every generated patch before
publishing its metadata.
The asset set and pre-upload verification command are documented in docs/RELEASING.md.
A fresh installation has no official updater artifact cached:
first update after adoption or cache miss -> Full -> stage PENDING
updated app launches -> promote to ACTIVE
later compatible update -> TarDelta when published and valid
On macOS the first update also tries to rebuild the previous release's tar from
the installed .app and use it as the TarDelta base, but only if it matches the
published base exactly. Otherwise it takes Full as above. A real DMG install
on a macOS CI runner matched and took its first update as a TarDelta (research
F39). That runner built and installed the app as the same user; on a machine
where the file owner or layout differs, the bundle will not match and the first
update is Full, so treat this as a likely saving rather than a guarantee
(Decisions #37).
Differential updates are an optimization, not a promise for every release. A
missing/corrupt cache, missing patch, download failure, unsupported patch, or
reconstruction mismatch safely degrades to Full. Cache persistence failures are
returned as non-fatal Diagnostic values on the successful Outcome; they do
not turn a correct install into a failure.
Authenticated release-identity contradictions, downgrades, and final signature
failures are different: they fail closed and install nothing. Legacy signatures
without a delta-v1 identity remain Full-compatible but cannot use DirectDelta
or TarDelta.
Most apps need none. Builder permits explicit overrides for cache/work paths,
download limits, timeouts, redirect count, reconstruction Limits, and
CacheLimits. Server metadata can never raise those local ceilings.
Plain HTTP and explicit endpoint/base overrides exist only under the non-default
test-support feature used by this repository's E2E harness. They are absent
from a normal build.
Everything below is a constraint the code actually enforces.
crates/plugin/tests/supported_versions.rs checks this table against the
dependency constraints, workspace fields, pinned CI environment and engine
constants, so it fails rather than drifts.
| v0.2 | |
|---|---|
| Client platform | macOS .app.tar.gz; Windows NSIS -setup.exe |
| Architecture | macOS aarch64 and Windows x86_64 demonstrated; Intel macOS and Windows ARM64 not demonstrated |
| Rust | 1.88 or newer |
tauri |
2 |
tauri-plugin-updater |
>=2.10.1, <2.12.0 |
| Release bundler | tauri-cli 2.10.1 (exact) |
| Artifact representation | macOS app-tar-gz-v1; Windows opaque-v1 |
| Recompression recipe | macOS tauri-app-tar-gz-v1 (tar 0.4.x into flate2 with the zlib-rs backend); none for NSIS |
| Delta backend | zstd |
The updater range is narrow on purpose: six behaviours this plugin's safety
depends on are observations about tauri-plugin-updater's implementation
rather than guarantees of its API. They were read in 2.10.1 and again, site by
site, in 2.11.0. A caret range would let any of them change while CI stayed
green. 2.12.0 changes one of them: its verifier also reads a signed version from
the trusted comment, where this plugin keeps its release identity. That needs a
design decision before it can be admitted (Decisions #39).
Pin =2.10.1 if you want exactly the configuration this release tested. The
strongest evidence here is against that one version: its source was read for each
of the six behaviours, the real-install E2E runs ran against it, and a test
asserts this repository's lockfile resolves to it. 2.11.0 is supported on the
strength of the same source reading, and the upstream updater / range-max CI
job runs the plugin's suite against it; no real-install E2E has used it. The consequences of drift are degradation rather than
compromise — this plugin performs its own signature and identity verification
regardless of upstream, so a changed upstream behaviour costs the delta path and
falls back to a full download rather than weakening what gets installed — but if
you want the tested configuration, pin it:
tauri-plugin-updater = "=2.10.1"On Intel macOS. The engine is byte-oriented and architecture-independent, and
the same engine and release tests run on Linux and Windows x86_64 in CI. The
recompression recipe depends on tar and flate2 behaviour rather than on the
CPU. So x86_64 macOS is expected to work — but no real application install
has been performed there, so it is not claimed as demonstrated. A client that
meets a representation or recipe it does not implement declines the tar layer and
downloads in full rather than guessing.
- The plugin always installs through the exact Tauri
Updatereturned by the one authoritative check. Applications cannot construct that binding or a verified install handoff. - Cache contents are untrusted: kind, size, digest, and signature are checked on reuse. A successful install stages PENDING; only observing that version on a later launch promotes it to ACTIVE.
- The release identity format and security rationale live in docs/DECISIONS.md. Implementation flow and resource boundaries live in docs/ARCHITECTURE.md.
- The manifest itself is not authenticated and release freshness is not proven. This is not a TUF-style update framework.
- No telemetry is added. The plugin contacts only the updater and artifact URLs selected from the application's existing updater response.
- Releasing: publish and verify update assets.
- Architecture: internal data flow and safety boundaries.
- Decisions: why non-obvious choices were made.
- Research: measurements and claim evidence.
- Contributing: build, test, and contribution workflow.
MIT — see LICENSE.
