Swarm Swarm

Desktop signing & autoupdate

Status: draft

Two kinds of signing

  1. Apple code signing + notarization — proves the .app/.dmg comes from a known developer so macOS runs it without warnings. Uses a Developer ID Application certificate and Apple notarization. These are account-level, so the same credentials used elsewhere (e.g. an Electron project via electron-builder) work here — only the variable names differ.
  2. Tauri updater signing — a separate keypair Tauri uses to sign update packages so the app trusts them. This is Tauri-specific; there is no equivalent in an Electron setup, so it's a new secret. The public key is committed in apps/desktop/src-tauri/tauri.conf.json; the private key is a secret.

Variable mapping (electron-builder / Brainstorm → Tauri)

bun run desktop:build (and the release workflow) accept either name; the build maps them:

Purpose Brainstorm / electron-builder Tauri
Developer ID cert (base64 .p12) CSC_LINK / MAC_CSC_LINK APPLE_CERTIFICATE
…its password CSC_KEY_PASSWORD / MAC_CSC_KEY_PASSWORD APPLE_CERTIFICATE_PASSWORD
Notarize (Apple ID) APPLE_ID APPLE_ID
…app-specific password APPLE_APP_SPECIFIC_PASSWORD APPLE_PASSWORD
…team id APPLE_TEAM_ID APPLE_TEAM_ID
Notarize (API key) — key id APPLE_API_KEY_ID APPLE_API_KEY
…issuer APPLE_API_ISSUER APPLE_API_ISSUER
…key file path (.p8) APPLE_API_KEY APPLE_API_KEY_PATH
Updater private key (new) — TAURI_SIGNING_PRIVATE_KEY (+ _PASSWORD)

Local signed build

# Apple creds (reuse your existing ones) + the updater key, in your env:
export CSC_LINK=... CSC_KEY_PASSWORD=... APPLE_ID=... APPLE_APP_SPECIFIC_PASSWORD=... APPLE_TEAM_ID=...
export TAURI_SIGNING_PRIVATE_KEY="$(cat /path/to/swarm-updater.key)"
bun run desktop:build      # signs + notarizes, and emits updater artifacts + .sig

With none set, desktop:build produces an unsigned dev build.

CI (GitHub Releases)

.github/workflows/release.yml builds on a v* tag. Set these repository secrets (the Apple ones are the same values used for the other project):

CSC_LINK, CSC_KEY_PASSWORD, APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID, TAURI_SIGNING_PRIVATE_KEY, TAURI_SIGNING_PRIVATE_KEY_PASSWORD (empty if the key has no password).

The workflow uploads the .dmg plus the updater artifacts and latest.json to a draft Release; the app's updater polls releases/latest/download/latest.json.

Linux packaging: the daemon ships gzipped

AppImage bundling runs linuxdeploy, which walks every ELF file in the AppDir — usr/bin and the resource directory alike — and rewrites each one with patchelf --set-rpath. That appends to the file, which destroys the payload bun build --compile glues onto the end of its executables: the bundled daemon segfaults, and the gtk plugin's next ldd over the wrecked binary exits 1 and takes linuxdeploy down with it (Failed to run ldd, exit 134). That is why AppImage was off between v0.2.2 and v0.13.2.

So on Linux the daemon is not an externalBin at all:

  • tauri.linux.conf.json clears externalBin and adds bin/swarmd.gz to resources.
  • tools/desktop.ts compiles the daemon and gzips it there (77 MB → 36 MB). A .gz is not an ELF, so linuxdeploy walks past it untouched.
  • daemon_command in src-tauri/src/lib.rs unpacks it to ~/.swarm/bin/swarmd-<version> on the first run of each version, drops the copies older versions left, and runs that.

The same path serves .deb and .rpm, which keeps one Linux code path instead of two. NO_STRIP stays set in the workflow: linuxdeploy's own strip step fails on GitHub runners, and the release profile already strips.

The updater keypair

Generated once with bunx tauri signer generate. The public key lives in tauri.conf.json. Keep the private key secret (repo secret + a safe backup) — losing it means shipped apps can no longer verify updates.

Source: docs/11-desktop-signing.md · v0.16.0.