Skip to content

Design Philosophy

Opinionated defaults. Extensible when you need it.

This plugin ships with strong defaults so you can go from zero to OTA updates in minutes. But every layer is swappable — you’re never locked into a pattern that doesn’t fit your architecture.


These choices are baked in because they’re the right default for most apps:

DefaultWhy
Minisign signaturesEvery bundle is verified before extraction. No opt-out — unsigned updates are a security risk.
HTTPS enforcedNon-HTTPS endpoints are rejected. Disable explicitly if you need local development.
Auto-rollbackIf notifyReady() isn’t called after an update, the next launch rolls back. Crash loops are caught automatically.
Atomic operationsExtraction goes to a temp directory, then renames. Pointer updates use temp file + rename. No half-written state.
Sequence-based orderingMonotonic integers, not semver comparison. Simple, unambiguous, works across any versioning scheme.
Filesystem-based cachingUpdates live on disk as extracted files. No database, no custom binary format, easy to inspect and debug.

The server contract is opinionated (but optional)

Section titled “The server contract is opinionated (but optional)”

The built-in HttpResolver expects a specific shape from your endpoint:

OpinionWhat it means
URL template{{current_sequence}} is replaced client-side — your server receives the current sequence in the URL path, not as a body field
Query params sent automaticallybinary_version, platform, arch, channel — you don’t choose what’s sent, but your server can ignore what it doesn’t need
204 = no updateNot a JSON response with { available: false } — just an empty 204. Keeps the happy path simple.
200 = JSON manifestA flat object with version, sequence, url, signature, min_binary_version. No envelope, no pagination.
Sequences, not semverUpdate ordering uses monotonic integers. Semver is for display only — the plugin never parses or compares version strings.

These opinions exist because they work for 90% of apps. A simple endpoint with a database query and a CDN-hosted bundle is all you need.

But if they don’t fit — you’re not stuck. Implement HotswapResolver and the entire server contract disappears. The plugin only cares about getting a HotswapManifest back. How you get there is up to you.


Every extension point exists because real apps needed it:

The HotswapResolver trait decouples update checking from the transport layer. The built-in HttpResolver calls a URL. But you can implement the trait to check anywhere — a local file, a database, a custom protocol:

use tauri_plugin_hotswap::{HotswapResolver, CheckContext, HotswapManifest};
struct MyResolver { /* ... */ }
impl HotswapResolver for MyResolver {
fn check(&self, ctx: &CheckContext)
-> Pin<Box<dyn Future<Output = Result<Option<HotswapManifest>>> + Send>>
{
// Check a local SQLite DB, a gRPC service, a message queue — anything.
}
}

Channel, endpoint, and headers are all changeable at runtime via configure(). No rebuild, no restart:

// Switch a beta tester to the internal channel
await configure({
channel: 'internal',
headers: { 'Authorization': 'Bearer user-token' }
});
// Point at a staging server for QA
await configure({
endpoint: 'https://staging.example.com/api/updates/{{current_sequence}}'
});

applyUpdate() does everything in one call. But if you want more control — download in the background, activate on next launch, prompt the user first — use downloadUpdate() + activateUpdate() separately.

tar.gz works out of the box. Enable features = ["zip"] for zip archives. The extraction layer is internal, but the manifest format is open — you control what URL the bundle lives at and how it’s hosted.


  • Not a CDN. You host the bundles. S3, Cloudflare R2, your own Nginx — anything that serves files over HTTPS.
  • Not a build system. You build and sign the bundle. The plugin handles everything after that.
  • Not a CI pipeline. You upload and publish the manifest. The plugin checks and downloads it.

The plugin is the last mile: check → download → verify → extract → serve. Everything before that is yours.