Testing OTA Updates Locally
Testing OTA Updates Locally
Section titled “Testing OTA Updates Locally”This guide walks you through setting up a local environment to test OTA updates end-to-end — from bundle creation to signature verification to asset serving.
Prerequisites
Section titled “Prerequisites”# macOSbrew install minisign
# Other platforms: see https://jedisct1.github.io/minisign/1. Generate a signing keypair
Section titled “1. Generate a signing keypair”minisign -G -W -p minisign.pub -s minisign.keyThe -W flag skips the password prompt (fine for local testing). Keep the public key — you’ll need it for your Tauri config.
The output will show your public key:
Files signed using this key pair can be verified with the following command:
minisign -Vm <file> -P RWR+iJ9ehTe/IxJtbA0haSUz...2. Create and sign a test bundle
Section titled “2. Create and sign a test bundle”Create a directory with your updated frontend assets:
bundle-v1/├── index.html├── style.css # optional — multi-file bundles work└── app.js # optionalThen package and sign:
cd bundle-v1tar czf ../bundle-v1.tar.gz .minisign -Sm ../bundle-v1.tar.gz -s ../minisign.keyThis produces bundle-v1.tar.gz and bundle-v1.tar.gz.minisig.
3. Run a local test server
Section titled “3. Run a local test server”Create a minimal Node.js server that implements the server contract:
import { createServer } from "node:http";import { readFile, stat } from "node:fs/promises";import { join, dirname } from "node:path";import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));const PORT = 3333;
const signature = (await readFile(join(__dirname, "bundle-v1.tar.gz.minisig"), "utf-8")).trim();const bundlePath = join(__dirname, "bundle-v1.tar.gz");const bundleSize = (await stat(bundlePath)).size;
const server = createServer(async (req, res) => { const url = new URL(req.url, `http://localhost:${PORT}`); console.log(`${req.method} ${url.pathname}${url.search}`);
// Check endpoint: GET /api/ota/:currentSequence const checkMatch = url.pathname.match(/^\/api\/ota\/(\d+)$/); if (checkMatch) { const currentSeq = parseInt(checkMatch[1], 10);
if (currentSeq >= 1) { res.writeHead(204); res.end(); return; }
res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ version: "0.1.0-ota.1", sequence: 1, min_binary_version: "0.1.0", url: `http://localhost:${PORT}/bundles/bundle-v1.tar.gz`, signature, notes: "Test OTA update", pub_date: new Date().toISOString(), bundle_size: bundleSize, })); return; }
// Bundle download const bundleMatch = url.pathname.match(/^\/bundles\/(.+)$/); if (bundleMatch) { try { const data = await readFile(join(__dirname, bundleMatch[1])); res.writeHead(200, { "Content-Type": "application/gzip", "Content-Length": data.length, }); res.end(data); } catch { res.writeHead(404); res.end("Not found"); } return; }
res.writeHead(404); res.end("Not found");});
server.listen(PORT, () => console.log(`Test server on http://localhost:${PORT}`));Run it:
node server.mjs4. Configure your app
Section titled “4. Configure your app”In tauri.conf.json:
{ "plugins": { "hotswap": { "endpoint": "http://localhost:3333/api/ota/{{current_sequence}}", "pubkey": "YOUR_PUBLIC_KEY_FROM_STEP_1", "require_https": false } }}
require_https: falseis required forhttp://localhost. Never disable this in production.
5. Build and run
Section titled “5. Build and run”Important:
cargo tauri ios devandcargo tauri android devproxy all asset requests to the dev server and bypass theAssetstrait entirely. OTA asset serving is not tested in dev mode on mobile. Always use production builds for testing.
Desktop (macOS, Windows, Linux)
Section titled “Desktop (macOS, Windows, Linux)”cargo tauri build --debug# Then run the binary from target/debug/# Requires Xcode with a signing identitycargo tauri ios build --debugInstall on simulator:
xcrun simctl install booted path/to/hotswap-example.appxcrun simctl launch booted com.example.hotswapAndroid
Section titled “Android”cargo tauri android build --debugInstall on emulator:
# Forward the test server port to the emulatoradb reverse tcp:3333 tcp:3333
# Install and launchadb install -r path/to/app-universal-debug.apkadb shell am start -n com.example.hotswap/.MainActivity6. Verify the flow
Section titled “6. Verify the flow”- App starts with embedded assets (the version bundled in the binary)
- Check for Update — the app hits your local server and finds seq 1
- Apply Update — downloads the bundle, verifies the minisign signature, extracts to disk
- Reload — the app now serves the OTA assets from the filesystem
- Rollback — returns to the previous version (or embedded assets)
- Restart the app — OTA assets persist across restarts; if
notifyReady()wasn’t called, auto-rollback kicks in