tauri-updater
Skill125 starsChanged 7 months agoMITView on GitHub
Use when adding software auto-update to a Tauri 2 desktop app, or when users ask about tauri-plugin-updater integration, app update checking, distinguishing installer vs portable builds for updates, or generating update manifests for GitHub Releases.
1 other project uses a version of this.
Adopted by
Most recently taken up by
- bkywksj/knowledge-basetheir own version, first seen Oct 8, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- bkywksj/knowledge-base★ 340same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository4 other files it gives its agents.
- AGENTS.mdAGENTS.md
- app-releaseSkill
- npm-releaseSkill
- worth-fixSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 2 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 10
Add self-update to a Tauri 2 desktop app using tauri-plugin-updater.
- Distinctive: line 36
Detecting portable at runtime: Use a Cargo feature flag:
- Very specific: line 64
When is_portable_build() returns true, the frontend shows a GitHub Releases link instead of the auto-update flow.
- Very specific: line 142
Update CSP connect-src to allow GitHub release asset domains:
- Very specific: line 147
src-tauri/capabilities/default.json: Add these permissions: - Distinctive: line 195
Error handling pattern: Never expose raw errors:
- Very specific: line 312
Signature errors: If the updater reports signature verification failure, the pubkey in tauri.conf.json likely doesn't match the private key …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Tauri Updater
Overview
Add self-update to a Tauri 2 desktop app using tauri-plugin-updater. Covers macOS DMG, Windows NSIS (auto-install), and Windows portable (manual download link). Uses a GitHub Releases-hosted latest.json manifest with signed artifacts.
When to Use
- User asks to add "auto-update", "software update", "check for updates" to a Tauri app
- User asks about
tauri-plugin-updaterintegration - User wants to distinguish installer vs portable builds for update behavior
- User needs to generate update manifests for CI
Don't use for:
- Tauri 1.x apps (different updater API)
- Mobile apps (different update mechanisms)
- Apps distributed only via app stores (use store update mechanisms)
Architecture
Installer vs Portable
Tauri updater only works with installers (NSIS, MSI, DMG). Portable builds need a different path.
| Build | Update method | Implementation |
| macOS DMG | Auto-download + install | tauri-plugin-updater full flow |
| Windows NSIS | Auto-download + install | tauri-plugin-updater full flow |
| Windows Portable | Link to GitHub Releases | Button opens releases page |
Detecting portable at runtime: Use a Cargo feature flag:
# Cargo.toml
[features]
portable = []// Rust command
#[tauri::command]
pub fn is_portable_build() -> bool {
cfg!(feature = "portable")
}Normal build: cargo build --release
Portable build: cargo build --release --features portable
In lib.rs, conditionally register the updater plugin and skip it when the portable feature is active:
#[cfg(all(desktop, not(feature = "portable")))]
app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())
.expect("Failed to register updater plugin");When is_portable_build() returns true, the frontend shows a GitHub Releases link instead of the auto-update flow.
Implementation Steps
1. Generate Signing Key (one-time)
bun tauri signer generate -w ~/.tauri/<app-name>.keyHit enter twice for empty password. Produces:
~/.tauri/<app-name>.keyis the private key → store as GitHub SecretTAURI_SIGNING_PRIVATE_KEY~/.tauri/<app-name>.key.pubis the public key → paste intotauri.conf.json
Empty password is fine because the key lives in encrypted GitHub Secrets. Adding a password means also managing TAURI_SIGNING_PRIVATE_KEY_PASSWORD in CI.
2. Rust Backend
src-tauri/Cargo.toml:
[features]
portable = []
[dependencies]
tauri-plugin-updater = "2"
tauri-plugin-process = "2"src-tauri/src/lib.rs:
// Always register process plugin (needed for relaunch after update)
.plugin(tauri_plugin_process::init())
.setup(|app| {
// Skip updater for portable builds
#[cfg(all(desktop, not(feature = "portable")))]
app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())
.expect("Failed to register updater plugin");
Ok(())
})Register is_portable_build as a Tauri command in the invoke handler.
Command file (e.g. commands/settings.rs):
#[tauri::command]
pub fn is_portable_build() -> bool {
cfg!(feature = "portable")
}3. Tauri Configuration
src-tauri/tauri.conf.json:
{
"bundle": {
"createUpdaterArtifacts": true
},
"plugins": {
"updater": {
"pubkey": "<PUBLIC KEY FROM .pub FILE>",
"endpoints": [
"https://github.com/<owner>/<repo>/releases/latest/download/latest.json"
]
}
}
}The releases/latest/download/ URL pattern uses GitHub's redirect to always serve the latest release's asset.
Update CSP connect-src to allow GitHub release asset domains:
connect-src 'self' ipc: http://ipc.localhost https://api.github.com https://github.com https://objects.githubusercontent.com https://github-releases.githubusercontent.comsrc-tauri/capabilities/default.json. Add these permissions:
{
"permissions": [
"updater:default",
"process:default",
"process:allow-restart"
]
}4. Frontend
Install:
bun add @tauri-apps/plugin-updater@^2 @tauri-apps/plugin-process@^2State machine:
idle → checking → up-to-date
→ downloading → ready-to-restart
→ available (download retry) → downloading → ready-to-restart
→ error → idle (retry)
Portable: always shows "GitHub Releases" link button| State | UI | Action |
idle | "Check for Updates" button | check() |
checking | Spinner, disabled button | Wait |
up-to-date | "Up to date" | None |
downloading | Version number + cumulative byte progress | Wait |
available | Version number + "Download & Install" | Retry downloadAndInstall() after a download failure |
ready-to-restart | "Restart Now" button | relaunch() |
error | Friendly message + Retry | check() |
Key API details:
check()returnsnullwhen up-to-date (not an error)check()throws on network failure or 404 (manifest doesn't exist yet), so show a friendly message, not the raw errordownloadAndInstall()progress callback: useevent.data.chunkLength(notposition/length). Track cumulative bytes with a state updater.- Store the
Updateobject in auseRefto avoid stale closure in the progress callback.
Error handling pattern. Never expose raw errors:
try {
const result = await check();
if (result) { /* downloadAndInstall() immediately */ }
else { /* up-to-date */ }
} catch {
// Network error, 404, rate limit, etc.
setStatus("error"); // shows friendly t("updater.error") message
}5. CI Pipeline
Build job. Pass the signing key:
- name: Tauri build
uses: tauri-apps/tauri-action@v0
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
with:
args: --target ${{ matrix.target }}Rename updater artifacts (after build, before upload):
- name: Rename updater artifacts
shell: bash
run: |
case "${{ matrix.target }}" in
universal-apple-darwin)
TAR=$(find "$BUNDLE/macos" -name "*.app.tar.gz" | head -1)
[ -n "$TAR" ] && mv "$TAR" "$BUNDLE/<App>-${VERSION}-macOS.app.tar.gz"
[ -f "${TAR}.sig" ] && mv "${TAR}.sig" "$BUNDLE/<App>-${VERSION}-macOS.app.tar.gz.sig"
;;
x86_64-pc-windows-msvc)
EXE_SIG=$(find "$BUNDLE/nsis" -name "*.exe.sig" | head -1)
[ -f "$EXE_SIG" ] && mv "$EXE_SIG" "$BUNDLE/<App>-${VERSION}-Windows.exe.sig"
;;
esacWindows portable rebuild:
- name: Create portable zip (Windows)
if: matrix.target == 'x86_64-pc-windows-msvc'
shell: pwsh
run: |
cargo build --release --manifest-path src-tauri/Cargo.toml --target ${{ matrix.target }} --features portable
# ... package the portable binary as beforeThe --features portable flag compiles a binary where is_portable_build() returns true and the updater plugin is not registered.
Generate latest.json (in release job, after all artifacts are available):
- name: Generate updater manifest
run: |
MACOS_SIG=$(cat artifacts/macos/*.app.tar.gz.sig 2>/dev/null || echo "")
WINDOWS_SIG=$(cat artifacts/windows/*.exe.sig 2>/dev/null || echo "")
jq -n \
--arg version "$VERSION" \
--arg pub_date "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
--arg macos_sig "$MACOS_SIG" \
--arg macos_url "https://github.com/<owner>/<repo>/releases/download/${VERSION}/<App>-${VERSION}-macOS.app.tar.gz" \
--arg windows_sig "$WINDOWS_SIG" \
--arg windows_url "https://github.com/<owner>/<repo>/releases/download/${VERSION}/<App>-${VERSION}-Windows.exe" \
'{
version: $version,
notes: "",
pub_date: $pub_date,
platforms: {
"darwin-x86_64": { signature: $macos_sig, url: $macos_url },
"darwin-aarch64": { signature: $macos_sig, url: $macos_url },
"windows-x86_64": { signature: $windows_sig, url: $windows_url }
}
}' > latest.json
- name: Upload manifest
run: gh release upload "$TAG_NAME" latest.jsonmacOS universal binary: Both darwin-x86_64 and darwin-aarch64 point to the same .app.tar.gz with the same signature.
Platform Artifacts Summary
| Build | Standard output | Updater artifact | Updater uses? |
| macOS universal | <App>-*.dmg | <App>-*.app.tar.gz + .sig | Yes (tar.gz) |
| Windows NSIS | <App>-*.exe | <App>-*.exe + .sig | Yes (exe re-used) |
| Windows Portable | <App>-*-Portable.zip | N/A | No (releases link) |
Common Mistakes
| Mistake | Fix |
| Pushing tag before main | Always push main first. Tag on unpushed commit won't trigger CI on correct SHA |
Forgetting createUpdaterArtifacts | No .app.tar.gz or .sig files will be generated |
| CSP blocking download | Add objects.githubusercontent.com and github-releases.githubusercontent.com to connect-src |
Missing process:allow-restart | relaunch() fails silently |
| Showing raw errors to user | Catch and show friendly message. The first release won't have latest.json yet. That's a 404, not a bug |
| Wrong progress event fields | Tauri 2 uses event.data.chunkLength, not position/length |
Treating check() null as error | null = no update available, it's a success case |
| Stale closure in download callback | Store Update in useRef, not just useState |
| Manifest URL case mismatch | latest.json in endpoints must match the uploaded filename exactly |
| Not cleaning up unused i18n keys | When replacing a manual releases button with updater UI, remove old translation keys |
Post-Release Notes
- First release: Existing users won't have the updater code, so they must download this release manually. After that, updates are automatic.
- Homebrew: The
.app.tar.gzand.sigare separate from the DMG. Homebrew cask formulas are unaffected. - Signature errors: If the updater reports signature verification failure, the pubkey in
tauri.conf.jsonlikely doesn't match the private key used in CI. Regenerate and redeploy. - Testing: Build a newer version to test the full download-and-install flow end-to-end.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

