Tauri 2 · wraps the official updater plugin
Ship in-app updates from private GitHub repositories.
The official updater returns 404 on private repos.
This crate embeds an Authorization: Bearer token at
build time and presets it on the official
tauri-plugin-updater — on both the manifest request and
the installer download. No fork. No extra commands. Your frontend
keeps the official JS API, unchanged.
Install both — on the Rust side this crate and a direct tauri-plugin-updater dependency (Tauri's ACL permission collection only sees direct dependencies); the frontend keeps the official @tauri-apps/plugin-updater, unchanged:
# 1 · Rust — src-tauri/Cargo.toml (both required)
[dependencies]
tauri-updater-private = "0.1"
tauri-plugin-updater = "2" # 2 · npm — frontend (the official package)
npm i @tauri-apps/plugin-updater One header is all it takes
The official updater fetches a latest.json manifest and
the installer asset over plain HTTPS. On a private repository both
requests are rejected with 404 unless an authenticated
token is attached. Nothing about the update flow needs to change —
only the transport.
-
Your app
check()·downloadAndInstall()
official JS API, zero changes -
tauri-updater-private
Authorization: Bearer •••Accept: application/octet-stream -
GitHub
manifest via
raw.githubusercontent.com
asset viaapi.github.com -
minisign verify
pubkeypinned intauri.conf.json
independent of transport auth
Update authenticity is unaffected: installers are still verified
against the minisign pubkey before anything is installed.
Transport auth and update signing are separate concerns — kept separate.
Three steps to private updates
Point the updater at GitHub
tauri.conf.json — the manifest lives on your default branch.
{
"plugins": {
"updater": {
"pubkey": "<minisign public key>",
"endpoints": [
"https://raw.githubusercontent.com/{owner}/{repo}/main/latest.json"
]
}
}
} Register the plugin via this crate
Token is embedded at build time from UPDATER_GH_TOKEN. Grant updater:default in your capability file.
// src-tauri/src/lib.rs
.builder(|b| b.plugin(tauri_updater_private::updater_builder()?.build())) Keep the official frontend API
Headers are applied automatically — pass no options.
import { check } from '@tauri-apps/plugin-updater';
const update = await check(); // Authorization header applied automatically
await update?.downloadAndInstall(); // ...also on the asset download
In your release workflow, export UPDATER_GH_TOKEN (a
long-lived fine-grained PAT — the Actions-default
GITHUB_TOKEN expires when the job ends and cannot be embedded):
- uses: tauri-apps/tauri-action@v0
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.UPDATER_SIGNING_KEY }}
UPDATER_GH_TOKEN: ${{ secrets.UPDATER_GH_TOKEN }} A topology that survives contact with GitHub
Private-repo endpoints have sharp edges. This crate encodes the combination verified end-to-end against a real private-repo Tauri 2 app on macOS (check → download → install → relaunch, v0.1.0 → v0.1.1), signature verification and version gating included.
- Manifest:
raw.githubusercontent.com/{owner}/{repo}/main/latest.json— Bearer honored. Committed to the default branch; cached ~5 min after each push. - Installer:
api.github.com/repos/{owner}/{repo}/releases/assets/{asset-id}— the crate presetsAccept: application/octet-stream; without it the API returns asset metadata JSON instead of the binary. - Known dead end:
github.com/…/releases/latest/download/…ignoresAuthorizationon private repos and returns404to PATs. Don't fight it — the topology above is the verified path.
Security posture, stated plainly
A stolen token can't ship malware
Installers are verified against the minisign pubkey
pinned in tauri.conf.json before any install step.
A leaked token can read your repo — it cannot push an update to
your users.
Least-privilege by construction
Fine-grained PAT scoped to the target repository only, permission Contents: Read-only, with an expiry. Application code never handles the token — it's embedded at build time from a CI secret.
Honest about extraction, easy to rotate
The embedded token can be pulled from a distributed binary with
strings. That trade-off is documented, not hidden:
rotate by replacing the secret and rebuilding. A dedicated
updates-only repo is documented as a future hardening path.
Add private-repo updates in three lines of Cargo.toml.
Both required — the wrapper plus a direct official-plugin dependency; frontend keeps the official npm package:
# 1 · Rust — src-tauri/Cargo.toml
[dependencies]
tauri-updater-private = "0.1"
tauri-plugin-updater = "2" # 2 · npm — official
npm i @tauri-apps/plugin-updater