Auto-Updates
Tynd ships a first-class auto-updater with Ed25519 signature verification. The CLI has tynd keygen + tynd sign so you can produce signed manifests without third-party tools.
Flow
┌───────────────────┐ GET /update.json ┌─────────────────────┐
│ Running app │──────────────────────▶│ Your update server │
│ │◀──────────────────────│ (any HTTPS host) │
│ currentVersion │ manifest JSON └─────────────────────┘
│ = 1.0.0 │
│ │ download url
│ │──────────────────────▶ artifact (MyApp-1.2.3.exe)
│ │ verify Ed25519 sig
│ │ over raw bytes
│ │
│ match? install. │
└───────────────────┘Generate an updater keypair
tynd keygen --out release/updaterProduces:
release/updater.key— PKCS#8 private key. Store offline, never commit.release/updater.pub— raw 32-byte public key (base64). Commit this into your app source.
Bake the public key into your app
export const UPDATER_PUB_KEY = "cFpG...RVDv/RQ=";Import and pass to updater.downloadAndVerify:
import { updater } from "@tynd/core/client";
import { UPDATER_PUB_KEY } from "./updater-key";
const info = await updater.check({
endpoint: "https://releases.example.com/update.json",
currentVersion: "1.0.0",
});
if (info) {
const off = updater.onProgress(({ phase, loaded, total }) => {
console.log(`${phase}: ${loaded}/${total ?? "?"}`);
});
const { path } = await updater.downloadAndVerify({
url: info.url,
signature: info.signature,
pubKey: UPDATER_PUB_KEY,
});
off();
await updater.install({ path }); // swaps binary + relaunches
}Publish a release
Build the signed artifact
tynd build --bundle nsis # or app, msi, deb, etc.Sign it
tynd sign release/MyApp-1.2.3-setup.exe \
--key release/updater.key \
--out release/MyApp-1.2.3-setup.exe.sigOutputs a base64 signature over the raw bytes of the artifact.
Write the manifest
Tauri-compatible format:
{
"version": "1.2.3",
"notes": "Bug fixes & perf.",
"pub_date": "2026-04-19T12:00:00Z",
"platforms": {
"windows-x86_64": {
"url": "https://releases.example.com/MyApp-1.2.3-setup.exe",
"signature": "<base64 Ed25519 — contents of .sig file>"
},
"darwin-aarch64": {
"url": "https://releases.example.com/MyApp-1.2.3.dmg",
"signature": "<base64 Ed25519>"
},
"linux-x86_64": {
"url": "https://releases.example.com/MyApp-1.2.3.AppImage",
"signature": "<base64 Ed25519>"
}
}
}Publish both the manifest and the artifact
Host the manifest at a stable URL (https://releases.example.com/update.json). Upload the signed artifacts alongside. Serve both over HTTPS.
Manifest details
- Platform key —
<os>-<arch>, whereos ∈ { windows | darwin | linux }andarch ∈ { x86_64 | aarch64 }.macosisdarwinfor parity with GitHub Releases / Tauri. - Version comparison — semver. An update is offered iff
manifest.versionis strictly newer thancurrentVersion. - Manifest is plain HTTPS — no meta-signature. Trust model: a compromised manifest server can only redirect to a URL whose bytes still have to verify against your local pubkey.
Install semantics
install({ path, relaunch? = true }):
- Windows —
cmd /c timeout 2 & move /y <new> <current> & start <current>. The timeout lets the current process exit so the.exeunlocks, then cmd replaces the binary and relaunches. - Linux (AppImage + any single-file binary) —
fs::rename+ chmod +x + spawn + exit. Linux keeps the old inode mapped as long as the exe is live, so this is safe mid-run. - macOS — not yet implemented.
.appbundles are directories and updates ship as archives; handle the swap manually via the returnedpathfor now.
Returns just before the current process exits. Your frontend code should not rely on any state after install resolves.
Trust model
- The public key travels bundled with the app. Rotating it requires shipping a new build — an attacker who compromises your release server can’t replace the key without also signing with the current private key.
- The private key never touches your build server. Sign artifacts on a trusted workstation (or a hardened CI runner) and upload the
.sigalongside the artifact. downloadAndVerifystreams the artifact to disk while hashing; verification happens over the full downloaded bytes beforeinstalltouches anything.
CI pattern
# .github/workflows/release.yml (partial)
- name: Sign artifact
env:
UPDATER_KEY: ${{ secrets.UPDATER_KEY }} # base64-encoded PKCS#8 from tynd keygen
run: |
echo "$UPDATER_KEY" | base64 -d > /tmp/updater.key
bunx tynd sign release/MyApp-1.2.3-setup.exe \
--key /tmp/updater.key \
--out release/MyApp-1.2.3-setup.exe.sig
rm /tmp/updater.key
- name: Upload to release
uses: softprops/action-gh-release@v2
with:
files: |
release/MyApp-1.2.3-setup.exe
release/MyApp-1.2.3-setup.exe.sigGenerate the update.json manifest as the last step of the release job (or regenerate on any publish via a small script that reads the signature files).
Periodic checks — do it yourself
import { updater } from "@tynd/core/client";
import pkg from "../package.json";
async function checkForUpdates() {
try {
const info = await updater.check({
endpoint: "https://releases.example.com/update.json",
currentVersion: pkg.version,
});
if (info) showUpdatePrompt(info);
} catch (err) {
console.warn("update check failed:", err.message);
}
}
// Check at startup, then every 6 hours.
void checkForUpdates();
setInterval(checkForUpdates, 6 * 60 * 60 * 1000);Not yet handled
- macOS
.appswap (extract from.dmg/.tar.gz). - Delta updates (binary diff).
- Rollback on failed install.
- Signed manifest (double-sig).
For macOS today, download the artifact, extract it manually, then tell the user where to drop the new .app.