Updates
Nani has a custom auto-update mechanism. It's a few hundred lines of Swift in UpdateChecker.swift — no Sparkle, no XPC service, no privileged installer. This page explains how it works and why we built it that way.
The flow
Each launch, Nani:
- Fetches
https://nani.ooo/update.jsonover HTTPS. - Parses the manifest —
{ version, build, url, signature, size, notes }. - Compares the manifest's
build(an integer) against the currentCFBundleVersion. - If newer, streams the
.dmgto~/Library/Caches/nani-updates/in the background. - Verifies the file size matches the manifest.
- Verifies the Ed25519 signature with
Curve25519.Signing.PublicKey.isValidSignature(_:for:)using the public key baked intoInfo.plist. - Surfaces a "Relaunch to Update" prompt in the menu and on next launch.
If you click Relaunch to Update, Nani:
- Writes a small bash script to
/tmp. - Spawns the script as a child process and quits.
- The script waits for the PID to exit, mounts the verified DMG with
hdiutil, copies the new.appto a staging directory, atomicallymvs it over the oldNani.appin/Applications, unmounts, deletes the DMG, and re-launches.
The atomic mv is the important part. If anything between mounting and the swap fails, your existing app bundle is untouched.
Why not Sparkle?
Sparkle is the standard macOS auto-update framework. It's been around for 20+ years, is widely used, and is generally well-engineered. We removed it for a few specific reasons:
- Sparkle is a compiled binary blob. Auditing it line-by-line is impractical. The Nani updater is ~300 lines of Swift you can read in one sitting.
- Sparkle includes XPC services and an installer that can run as root. Nani's updater runs entirely as your user, with no privileged components.
- Sparkle's EdDSA verification is good but it's bolted on top of an architecture that pre-dates modern crypto. Our updater uses CryptoKit's
Curve25519.Signingdirectly — the same code path Apple uses for its own signing. - One less external dependency. Nani's principle is to vendor every dependency locally and avoid networked package fetches at build time. Sparkle's auto-update infrastructure depended on us hosting and maintaining a separate
appcast.xmlflow we didn't need.
Why Ed25519
We picked Ed25519 because:
- It's standard, audited, and supported natively by Apple's CryptoKit.
- Signatures are 64 bytes; verification is fast and constant-time.
- The keypair is deterministic from the seed — no per-signature randomness, no possibility of a leaky RNG breaking the key.
- It's the same algorithm used by SSH, Signal, Tor, and many others.
The private key lives offline. It's loaded only at release time, on a clean machine, to sign one DMG. We don't store it on a build server.
What you see
By default, Nani checks for updates silently in the background on launch. If an update is available, you'll see a small prompt — there's no nagware, no modal you have to click through. You can also trigger a check manually from Menu Bar → Check for Updates.
If a download is in progress when you trigger a manual check, the menu shows the progress. If a verified update is ready to install, the menu shows "Relaunch to Update" with the version number and release notes.
What happens if verification fails
If the size doesn't match the manifest or the signature is invalid, the downloaded file is deleted and the update is rejected. The status shows "Signature verification failed — update rejected" and the existing app continues to run normally.
A failed update never replaces your installed app. The atomic mv only happens after both checks pass and the script has the file in a staging directory.
What if you don't want auto-updates
Nani's updater only checks once per launch and only downloads when there's a new build. There's currently no setting to disable the check entirely. If you really need to, you can block nani.ooo in your firewall or /etc/hosts — the update check will fail silently and the app will continue to work.
For the curious: how releases are signed
The release-side workflow is documented in docs/release-signing.md in the macOS repo. It's a two-step openssl pkeyutl invocation followed by uploading the signed DMG and an updated update.json to the release server. The private key is loaded from a password manager only at sign time and never written to disk in the build environment.
What's next
Read Security for the broader threat model, or FAQ for common questions.