Automatic Checking
Configure periodic update checks in the background
Wails v3 provides a built-in updater system that supports automatic update checking, downloading, and installation. The updater includes support for binary delta updates (patches) for minimal download sizes.
Automatic Checking
Configure periodic update checks in the background
Delta Updates
Download only what changed with bsdiff patches
Cross-Platform
Works on macOS, Windows, and Linux
Secure
SHA256 checksums and optional signature verification
Add the updater service to your application:
package main
import ( "github.com/wailsapp/wails/v3/pkg/application" "time")
func main() { // Create the updater service updater, err := application.CreateUpdaterService( "1.0.0", // Current version application.WithUpdateURL("https://updates.example.com/myapp/"), application.WithCheckInterval(24 * time.Hour), ) if err != nil { panic(err) }
app := application.New(application.Options{ Name: "MyApp", Services: []application.Service{ application.NewService(updater), }, })
// ... rest of your app app.Run()}Then use it from your frontend:
import { updater } from './bindings/myapp';
async function checkForUpdates() { const update = await updater.CheckForUpdate();
if (update) { console.log(`New version available: ${update.version}`); console.log(`Release notes: ${update.releaseNotes}`);
// Download and install await updater.DownloadAndApply(); }}The updater supports various configuration options:
updater, err := application.CreateUpdaterService( "1.0.0", // Required: URL where update manifests are hosted application.WithUpdateURL("https://updates.example.com/myapp/"),
// Optional: Check for updates automatically every 24 hours application.WithCheckInterval(24 * time.Hour),
// Optional: Allow pre-release versions application.WithAllowPrerelease(true),
// Optional: Update channel (stable, beta, canary) application.WithChannel("stable"),
// Optional: Require signed updates application.WithRequireSignature(true), application.WithPublicKey("your-ed25519-public-key"),)Host an update.json file on your server:
{ "version": "1.2.0", "release_date": "2025-01-15T00:00:00Z", "release_notes": "## What's New\n\n- Feature A\n- Bug fix B", "platforms": { "macos-arm64": { "url": "https://updates.example.com/myapp/myapp-1.2.0-macos-arm64.tar.gz", "size": 12582912, "checksum": "sha256:abc123...", "patches": [ { "from": "1.1.0", "url": "https://updates.example.com/myapp/patches/1.1.0-to-1.2.0-macos-arm64.patch", "size": 14336, "checksum": "sha256:def456..." } ] }, "macos-amd64": { "url": "https://updates.example.com/myapp/myapp-1.2.0-macos-amd64.tar.gz", "size": 13107200, "checksum": "sha256:789xyz..." }, "windows-amd64": { "url": "https://updates.example.com/myapp/myapp-1.2.0-windows-amd64.zip", "size": 14680064, "checksum": "sha256:ghi789..." }, "linux-amd64": { "url": "https://updates.example.com/myapp/myapp-1.2.0-linux-amd64.tar.gz", "size": 11534336, "checksum": "sha256:jkl012..." } }, "minimum_version": "1.0.0", "mandatory": false}| Platform | Key |
|---|---|
| macOS (Apple Silicon) | macos-arm64 |
| macOS (Intel) | macos-amd64 |
| Windows (64-bit) | windows-amd64 |
| Linux (64-bit) | linux-amd64 |
| Linux (ARM64) | linux-arm64 |
The updater exposes methods that are automatically bound to your frontend:
interface UpdateInfo { version: string; releaseDate: Date; releaseNotes: string; size: number; patchSize?: number; mandatory: boolean; hasPatch: boolean;}
interface Updater { // Get the current application version GetCurrentVersion(): string;
// Check if an update is available CheckForUpdate(): Promise<UpdateInfo | null>;
// Download the update (emits progress events) DownloadUpdate(): Promise<void>;
// Apply the downloaded update (restarts app) ApplyUpdate(): Promise<void>;
// Download and apply in one call DownloadAndApply(): Promise<void>;
// Get current state: idle, checking, available, downloading, ready, installing, error GetState(): string;
// Get the available update info GetUpdateInfo(): UpdateInfo | null;
// Get the last error message GetLastError(): string;
// Reset the updater state Reset(): void;}Listen for download progress events:
import { Events } from '@wailsio/runtime';
Events.On('updater:progress', (data) => { console.log(`Downloaded: ${data.downloaded} / ${data.total}`); console.log(`Progress: ${data.percentage.toFixed(1)}%`); console.log(`Speed: ${(data.bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s`);});Here’s a complete example with a React component:
import { useState, useEffect } from 'react';import { updater } from './bindings/myapp';import { Events } from '@wailsio/runtime';
interface Progress { downloaded: number; total: number; percentage: number; bytesPerSecond: number;}
export function UpdateChecker() { const [checking, setChecking] = useState(false); const [updateInfo, setUpdateInfo] = useState<any>(null); const [downloading, setDownloading] = useState(false); const [progress, setProgress] = useState<Progress | null>(null); const [error, setError] = useState<string | null>(null);
useEffect(() => { // Listen for progress events const cleanup = Events.On('updater:progress', (data: Progress) => { setProgress(data); });
// Check for updates on mount checkForUpdates();
return () => cleanup(); }, []);
async function checkForUpdates() { setChecking(true); setError(null);
try { const info = await updater.CheckForUpdate(); setUpdateInfo(info); } catch (err) { setError(err.message); } finally { setChecking(false); } }
async function downloadAndInstall() { setDownloading(true); setError(null);
try { await updater.DownloadAndApply(); // App will restart automatically } catch (err) { setError(err.message); setDownloading(false); } }
if (checking) { return <div>Checking for updates...</div>; }
if (error) { return ( <div> <p>Error: {error}</p> <button onClick={checkForUpdates}>Retry</button> </div> ); }
if (!updateInfo) { return ( <div> <p>You're up to date! (v{updater.GetCurrentVersion()})</p> <button onClick={checkForUpdates}>Check Again</button> </div> ); }
if (downloading) { return ( <div> <p>Downloading update...</p> {progress && ( <div> <progress value={progress.percentage} max={100} /> <p>{progress.percentage.toFixed(1)}%</p> <p>{(progress.bytesPerSecond / 1024 / 1024).toFixed(2)} MB/s</p> </div> )} </div> ); }
return ( <div> <h3>Update Available!</h3> <p>Version {updateInfo.version} is available</p> <p>Size: {updateInfo.hasPatch ? `${(updateInfo.patchSize / 1024).toFixed(0)} KB (patch)` : `${(updateInfo.size / 1024 / 1024).toFixed(1)} MB`} </p> <div dangerouslySetInnerHTML={{ __html: updateInfo.releaseNotes }} /> <button onClick={downloadAndInstall}>Download & Install</button> <button onClick={() => setUpdateInfo(null)}>Skip</button> </div> );}func (a *App) OnStartup(ctx context.Context) { // Check for updates after a short delay go func() { time.Sleep(5 * time.Second) info, err := a.updater.CheckForUpdate() if err == nil && info != nil { // Emit event to frontend application.Get().EmitEvent("update-available", info) } }()}Configure automatic background checks:
updater, _ := application.CreateUpdaterService( "1.0.0", application.WithUpdateURL("https://updates.example.com/myapp/"), application.WithCheckInterval(6 * time.Hour), // Check every 6 hours)menu := application.NewMenu()menu.Add("Check for Updates...").OnClick(func(ctx *application.Context) { info, err := updater.CheckForUpdate() if err != nil { application.InfoDialog().SetMessage("Error checking for updates").Show() return } if info == nil { application.InfoDialog().SetMessage("You're up to date!").Show() return } // Show update dialog...})Delta updates (patches) allow users to download only the changes between versions, dramatically reducing download sizes.
Patches are generated using the bsdiff algorithm. You’ll need the bsdiff tool:
# Install bsdiff (macOS)brew install bsdiff
# Install bsdiff (Ubuntu/Debian)sudo apt-get install bsdiff
# Generate a patchbsdiff old-binary new-binary patch.bsdiffOrganize your patches in your update directory:
updates/├── update.json├── myapp-1.2.0-macos-arm64.tar.gz├── myapp-1.2.0-windows-amd64.zip└── patches/ ├── 1.0.0-to-1.2.0-macos-arm64.patch ├── 1.1.0-to-1.2.0-macos-arm64.patch ├── 1.0.0-to-1.2.0-windows-amd64.patch └── 1.1.0-to-1.2.0-windows-amd64.patchUpdates can be hosted on any static file server:
Example S3 bucket structure:
s3://my-updates-bucket/myapp/├── stable/│ ├── update.json│ ├── myapp-1.2.0-macos-arm64.tar.gz│ └── patches/│ └── 1.1.0-to-1.2.0-macos-arm64.patch└── beta/ └── update.jsonIf hosting on a different domain, configure CORS:
{ "CORSRules": [ { "AllowedOrigins": ["*"], "AllowedMethods": ["GET"], "AllowedHeaders": ["*"] } ]}All downloads are verified against SHA256 checksums in the manifest:
{ "checksum": "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}For additional security, enable signature verification:
Generate a key pair:
# Generate Ed25519 key pairopenssl genpkey -algorithm Ed25519 -out private.pemopenssl pkey -in private.pem -pubout -out public.pemSign your update manifest:
openssl pkeyutl -sign -inkey private.pem -in update.json -out update.json.sigConfigure the updater:
updater, _ := application.CreateUpdaterService( "1.0.0", application.WithUpdateURL("https://updates.example.com/myapp/"), application.WithRequireSignature(true), application.WithPublicKey("MCowBQYDK2VwAyEA..."), // Base64-encoded public key)macos-arm64)bspatch is available on the system