The ModelRelay desktop app ships with Tauri v2's auto-updater so users never have to manually reinstall to get new versions. This page explains how it works end-to-end, how to cut a release that gets picked up by the updater, and how to rotate the signing key if it's ever compromised.
┌──────────────────────┐ ┌─────────────────────────────┐ ┌──────────────────────┐
│ ModelRelay.app (v1) │ ─GET─▶ │ modelrelay.io/updater/... │ ─GET─▶ │ GitHub Releases API │
│ tauri-plugin-updater │ ◀204── │ (Axum, in modelrelay-cloud) │ ◀JSON─ │ │
└──────────┬───────────┘ └─────────────────────────────┘ └──────────────────────┘
│ 200 + JSON when a newer `desktop-v*` release exists
▼
┌──────────────────────┐
│ Download .app.tar.gz │──► Verify minisign sig (pubkey baked into app)
│ / .AppImage / -setup │──► Unpack + replace binary
│ .exe from GitHub │──► Prompt user to restart
└──────────────────────┘
- On launch (5 seconds after startup) and when the user clicks Check for
Updates…, the desktop app calls
GET https://modelrelay.io/updater/desktop/{target}/{arch}/{current_version}. Tauri fills{target}in withlinux|darwin|windows,{arch}withx86_64|aarch64, and{current_version}with the running app's semver. - The cloud service fetches the latest
desktop-v*GitHub release (cached for 5 minutes), finds the.app.tar.gz/.AppImage/-setup.exeartifact matching the caller's target, reads its.sigfile contents, and returns a Tauri-compatible JSON payload. - If
current_versionis already at or ahead of the latest release, the server returns204 No Contentand the client does nothing. - If an update is available, the app verifies the minisign signature
against the public key baked into
tauri.conf.jsonbefore installing. A forged binary can't be installed even if the CDN is compromised. - After install, the user is prompted to restart. If they decline, the new binary loads next launch.
Releases are gated on a desktop-v* git tag. The
desktop-release.yml workflow
builds on Linux, macOS, and Windows runners, signs the updater artifacts
with the private key stored in GitHub Actions secrets, and uploads them
to the GitHub release.
# Bump version in crates/modelrelay-desktop/tauri.conf.json AND Cargo.toml
git tag desktop-v0.1.2
git push origin desktop-v0.1.2The workflow will:
- Build
.dmg/.app.tar.gzon macOS (bothx64andaarch64) - Build
.deb/.AppImageon Linux (x86_64) - Build
.msi/-setup.exeon Windows (x86_64) - Sign each updater artifact (
.app.tar.gz,.AppImage,-setup.exe) and upload a matching.sigfile - Publish everything to the GitHub release
Within ~5 minutes (the server-side cache TTL), every running desktop app will see the new version on its next check.
Pre-release tags (-alpha, -beta, -rc suffix) are marked as
GitHub pre-releases and are not returned by the updater endpoint — users
on stable builds won't be offered alpha updates.
The following GitHub Actions secrets must be set on the ericflo/modelrelay
repository for the release workflow to sign updater artifacts:
| Secret | Value |
|---|---|
TAURI_SIGNING_PRIVATE_KEY |
Contents of the minisign private key file (not a path). |
TAURI_SIGNING_PRIVATE_KEY_PASSWORD |
Password used when the keypair was generated (empty string if none). |
APPLE_CERTIFICATE |
Base64 of the Developer ID Application .p12. |
APPLE_CERTIFICATE_PASSWORD |
Password used when exporting the .p12. |
APPLE_SIGNING_IDENTITY |
e.g. Developer ID Application: Eric Florenzano (F6ZGE4FAML). |
APPLE_API_ISSUER |
App Store Connect API key issuer UUID. |
APPLE_API_KEY |
App Store Connect API key ID (10 chars). |
APPLE_API_KEY_BASE64 |
Base64 of the AuthKey_XXXX.p8 downloaded from App Store Connect. |
Without the TAURI_SIGNING_* secrets the workflow will still build the
installers but won't produce .sig files — and the cloud updater endpoint
will then return 204 (no update) because it refuses to serve unsigned
bundles. Without the APPLE_* secrets the macOS build succeeds but
downloaded .dmgs trip Gatekeeper with "ModelRelay is damaged and can't
be opened" — see Apple Code Signing below.
macOS .dmgs must be signed with a Developer ID Application certificate
and notarized by Apple for Gatekeeper to accept them on a fresh install.
The CI workflow handles signing, notarization (via notarytool), and
stapling automatically when the APPLE_* secrets above are present;
tauri-action creates a temp keychain, imports the .p12, signs with the
hardened runtime, and submits to notarytool.
First-time setup (per Apple Developer Program seat):
- Generate a Certificate Signing Request and keep the matching private
key — the
.p12is this key plus the cert Apple issues:mkdir -p ~/apple-signing-setup && cd ~/apple-signing-setup openssl genrsa -out developerID.key 2048 openssl req -new -key developerID.key -out developerID.csr \ -subj "/emailAddress=YOUR_EMAIL/CN=YOUR NAME/C=US"
- At https://developer.apple.com/account/resources/certificates create
a new Developer ID Application certificate, upload
developerID.csr, and download the resulting.cer. - Bundle the cert and key into a
.p12:openssl x509 -inform DER -in developerID.cer -out developerID.pem openssl pkcs12 -export -legacy \ -inkey developerID.key -in developerID.pem \ -out developerID.p12 -name "Developer ID Application" \ -passout pass:YOUR_P12_PASSWORD base64 -i developerID.p12 | pbcopy # → APPLE_CERTIFICATE
- Create an App Store Connect API key at
https://appstoreconnect.apple.com/access/integrations/api with the
"Developer" role. Download the
.p8(only available once — save it). Note the key ID (10 chars) and the issuer UUID:base64 -i AuthKey_XXXXXX.p8 | pbcopy # → APPLE_API_KEY_BASE64
- Upload all six secrets to the repo:
gh secret set APPLE_CERTIFICATE --repo ericflo/modelrelay < <(base64 -i developerID.p12) gh secret set APPLE_CERTIFICATE_PASSWORD --repo ericflo/modelrelay --body 'YOUR_P12_PASSWORD' gh secret set APPLE_SIGNING_IDENTITY --repo ericflo/modelrelay --body 'Developer ID Application: YOUR NAME (TEAMID)' gh secret set APPLE_API_ISSUER --repo ericflo/modelrelay --body 'ISSUER_UUID' gh secret set APPLE_API_KEY --repo ericflo/modelrelay --body 'KEY_ID' gh secret set APPLE_API_KEY_BASE64 --repo ericflo/modelrelay < <(base64 -i AuthKey_XXXXXX.p8)
The Developer ID certificate is valid for five years; renew before it
expires. The App Store Connect key can be rotated at any time — revoke
the old one in the portal and update the three APPLE_API_* secrets.
Note on .dmg notarization. tauri-action notarizes the .app
bundle but not the .dmg wrapper, so the workflow has a follow-up
xcrun notarytool submit + xcrun stapler staple step that runs on
the .dmg and re-uploads it to the release. Without this step the
downloaded .dmg would still trip Gatekeeper on first open even
though the .app inside is fully notarized.
The updater is only as trustworthy as the private key that signs bundles. If the key is ever compromised (leaked, lost, shared outside CI), rotate it immediately and cut a fresh release with the new key baked in.
# Install the Tauri CLI if you don't have it.
cargo install tauri-cli --version "^2"
# Generate a new keypair. Use a non-empty password for real releases.
cargo tauri signer generate --password "YOUR_PASSWORD" \
--write-keys ./modelrelay-updater.keyThis writes two files:
modelrelay-updater.key— private key. Keep out of git. Copy its full contents into theTAURI_SIGNING_PRIVATE_KEYGitHub secret.modelrelay-updater.key.pub— public key. Paste its contents intocrates/modelrelay-desktop/tauri.conf.jsonunderplugins.updater.pubkey.
After rotating:
- Commit the new pubkey change.
- Publish a new
desktop-v*release with the new private key configured in CI. - Users must install the new release manually (the old app doesn't trust the new key, so it can't auto-update across the rotation). Include a note in the release body explaining this.
The updater endpoint lives in
crates/modelrelay-cloud/src/routes/updater.rs.
- Route:
GET /updater/desktop/{target}/{arch}/{current_version} - 200 + JSON manifest when a newer release is available
- 204 No Content otherwise (also on parse errors, missing artifacts, unreachable GitHub API — clients will retry on the next check)
- Caches the latest GitHub release lookup for 5 minutes and the per-asset signature file contents for the process lifetime
- Unit tests cover semver comparison, platform suffix mapping, and the bundle/sig pair matcher
If a user wants to opt out (e.g. managed enterprise install), they can
quit the app and remove the updater entry from their preferences; the
updater won't fire without a user interaction after that. There's no
kill switch on the server side — if the endpoint returns 204 for long
enough (e.g. because releases stopped), the app simply keeps running the
current version.
"Update install failed: signature verification failed"
The signing key in GitHub secrets doesn't match the pubkey in
tauri.conf.json. Regenerate both, or re-paste the pubkey.
"Update check failed" in the settings UI
The /updater/desktop/... endpoint is unreachable. Check
modelrelay-cloud logs; the endpoint is exempt from the session guard so
it should respond even if the DB is down.
The app never offers an update after a release
- Confirm the release tag starts with
desktop-v. - Confirm the
.sigfiles are present on the release (the workflow uploads them automatically if signing secrets are configured). - Wait up to 5 minutes for the server-side cache to expire, then restart the app.
- Compare
{target}/{arch}in server logs againstartifact_suffixes— only the listed combinations are served.