Implementing Auto-Update for Desktop Applications
Auto-update is critical for desktop apps. Without it, users work with old versions for months. With poor implementation — they lose data or get broken updates. Let's cover Electron and Tauri.
Electron: electron-updater
electron-updater from electron-builder package is the standard tool. Supports GitHub Releases, S3, custom servers.
npm install electron-updater
// main/updater.js
const { autoUpdater } = require('electron-updater');
const { app, BrowserWindow } = require('electron');
const log = require('electron-log');
autoUpdater.logger = log;
autoUpdater.autoDownload = false;
autoUpdater.autoInstallOnAppQuit = true;
function setupAutoUpdater(mainWindow) {
autoUpdater.on('update-available', (info) => {
mainWindow.webContents.send('update:available', {
version: info.version,
releaseNotes: info.releaseNotes,
releaseDate: info.releaseDate
});
});
autoUpdater.on('download-progress', (progress) => {
mainWindow.webContents.send('update:progress', {
percent: Math.round(progress.percent)
});
});
autoUpdater.on('update-downloaded', (info) => {
mainWindow.webContents.send('update:downloaded', {
version: info.version
});
});
const { ipcMain } = require('electron');
ipcMain.handle('updater:check', () => autoUpdater.checkForUpdates());
ipcMain.handle('updater:download', () => autoUpdater.downloadUpdate());
ipcMain.handle('updater:install', () => autoUpdater.quitAndInstall(false, true));
setInterval(() => autoUpdater.checkForUpdates(), 4 * 60 * 60 * 1000);
setTimeout(() => autoUpdater.checkForUpdates(), 10000);
}
module.exports = { setupAutoUpdater };
Configuration for GitHub Releases
# electron-builder.yml
publish:
provider: github
owner: your-github-username
repo: your-repo-name
private: false
When building, electron-builder creates latest.yml with version metadata. autoUpdater reads it to check for updates.
UI Component for Updates
// renderer/components/UpdateNotification.tsx
import { useEffect, useState } from 'react';
type UpdateState =
| { status: 'idle' }
| { status: 'checking' }
| { status: 'available'; version: string }
| { status: 'downloading'; percent: number }
| { status: 'ready'; version: string }
| { status: 'error'; message: string };
export function UpdateNotification() {
const [state, setState] = useState<UpdateState>({ status: 'idle' });
useEffect(() => {
const unsubscribers = [
window.electronAPI.onUpdateAvailable((info) =>
setState({ status: 'available', version: info.version })
),
window.electronAPI.onUpdateProgress((p) =>
setState({ status: 'downloading', percent: p.percent })
),
window.electronAPI.onUpdateDownloaded((info) =>
setState({ status: 'ready', version: info.version })
),
];
return () => unsubscribers.forEach(fn => fn?.());
}, []);
if (state.status === 'available') {
return (
<div className="update-banner">
<span>Version {state.version} available</span>
<button onClick={() => window.electronAPI.downloadUpdate()}>Download</button>
</div>
);
}
if (state.status === 'ready') {
return (
<div className="update-banner">
<span>Version {state.version} ready</span>
<button onClick={() => window.electronAPI.installUpdate()}>Restart and Install</button>
</div>
);
}
return null;
}
Tauri: Built-in Updates
# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-updater = "2"
// src-tauri/src/lib.rs
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_updater::Builder::default().build())
.invoke_handler(tauri::generate_handler![check_for_updates])
.run(tauri::generate_context!())
.expect("error running app");
}
#[tauri::command]
async fn check_for_updates(app: tauri::AppHandle) -> Result<(), String> {
let updater = app.updater().map_err(|e| e.to_string())?;
let response = updater.check().await.map_err(|e| e.to_string())?;
if let Some(update) = response {
app.emit("update:available", &update.version).unwrap();
update.download_and_install(
|chunk, total| {
if let Some(total) = total {
let percent = (chunk * 100 / total) as u8;
app.emit("update:progress", percent).unwrap();
}
},
|| { app.emit("update:installed", ()).unwrap(); }
).await.map_err(|e| e.to_string())?;
}
Ok(())
}
Tauri requires signed updates — can't install unsigned patches.
Update without Restart
Full seamless updating is technically impossible for a running binary. But minimize disruption by:
- Downloading updates in background silently
- Offering installation at next app close
- Showing notification in tray
- Saving state before restart and restoring after







