Documentation menu▾

Shipping updates

Publish signed releases once and serve them to Sparkle, Velopack and Tauri updaters. License gated downloads and update periods for perpetual licenses.

Keygate can host your app's releases and serve them to the updater built into your app. You upload each build once, and Sparkle, Tauri and Velopack all read from the same release. Sparkle and Tauri check the signature of every download and Velopack checks its SHA256, so a tampered file is rejected on the user's machine.

What you get

  • One release groups the builds for every platform under a single version number.
  • Channels: stable, beta, alpha and dev. Beta users also see stable releases.
  • Each file is signed with your product's Ed25519 key when the release is published.
  • A release only becomes visible once it is published, so a half uploaded release never reaches users.
  • A bad release can be pulled at once and put back later.
  • Optional: only paying customers can download, and perpetual licenses only receive the releases their update period covers.

Turn on release storage

Release files live in S3 compatible storage, such as Cloudflare R2, AWS S3 or MinIO. Uploads go straight from your browser or CI to the bucket, and downloads come straight from it, so large files never pass through Keygate. Add the bucket to .env along with a master key for the signing keys:

.env (Cloudflare R2)
STORAGE_ENDPOINT=https://ACCOUNT_ID.r2.cloudflarestorage.com
STORAGE_REGION=auto
STORAGE_BUCKET=myapp-releases
STORAGE_ACCESS_KEY=...
STORAGE_SECRET_KEY=...
RELEASE_KEY_ENCRYPTION_KEY=   # openssl rand -hex 32

The bucket can stay private. The configuration reference lists the other storage settings, including how long download links stay valid.

Create a signing key

Open Releases, then Update settings, choose your product and create its signing key on the Signing tab. Keygate generates an Ed25519 key pair and keeps the private half encrypted with RELEASE_KEY_ENCRYPTION_KEY. The tab shows the public key in the form each updater expects. Add it to your app's updater configuration:

  • Sparkle: SUPublicEDKey in Info.plist.
  • Tauri: pubkey in the updater section of tauri.conf.json. Use the key labelled Public key for Tauri, which is in the format Tauri expects.

The public key is also available as a PEM file from the same panel.

Publish a release

  1. Create a release with a version number such as 1.4.0 and a channel. Add release notes if you like; updaters show them to users.
  2. Add one file per platform: darwin-arm64, darwin-x64, windows-x64, windows-arm64, linux-x64, linux-arm64 or linux-armhf. Each upload gets its own short lived upload link.
  3. When the uploads finish, Keygate checks each file in storage and records its size and SHA256 hash itself, rather than trusting the uploader.
  4. Publish. Keygate signs every file and the release appears in the feeds.

You can do all of this from the dashboard by hand, or from CI with an API key that has the releases:write scope.

Point your updater at the feed

Each updater reads its own feed. The Feed URLs tab in Update settings shows each address for your product, ready to copy. The examples below use my-app as the slug and licenses.example.com as the Keygate address.

Sparkle

Upload the archive of your .app that Sparkle installs, such as a .zip made with ditto, a .dmg or a .tar.gz. In Info.plist:

Info.plist
<key>SUFeedURL</key>
<string>https://licenses.example.com/api/v1/releases/my-app/feed.xml?platform=darwin-arm64</string>
<key>SUPublicEDKey</key>
<string>Public key for Sparkle, from the Signing tab</string>

Sparkle compares the version in the feed with the app's CFBundleVersion, so set CFBundleVersion to the version you publish in Keygate, such as 1.2.0, rather than a build number. Sparkle also stops reading a version at the first hyphen, so 1.3.0-beta.1 and 1.3.0 look the same to it. Give each beta you ship through Sparkle a plain version number higher than the last one.

Tauri

Upload the update bundle, on macOS the .app.tar.gz. Keygate signs it when the release is published, so the .sig file the Tauri CLI writes is not needed. In tauri.conf.json, the endpoint can use Tauri's own variables, which Keygate understands:

tauri.conf.json
"plugins": {
  "updater": {
    "pubkey": "Public key for Tauri, from the Signing tab",
    "endpoints": [
      "https://licenses.example.com/api/v1/releases/my-app/upgrade.json?platform={{target}}-{{arch}}"
    ]
  }
}

Keygate keeps one file per platform, and the updater installs it the way the running copy was installed. Pick one bundle type per platform and keep it: on Windows the NSIS or the MSI installer, on Linux the AppImage, the .deb or the .rpm.

Velopack

Upload the full package that vpk pack creates, such as MyApp-1.2.0-win-full.nupkg, and publish it under the same version you passed to vpk pack -v. Give Velopack the base address, ending in the platform the app was built for. Velopack adds the feed file and downloads the packages from there:

csharp
var mgr = new UpdateManager("https://licenses.example.com/api/v1/releases/my-app/velopack/windows-x64");
var update = await mgr.CheckForUpdatesAsync();
if (update != null)
{
    await mgr.DownloadUpdatesAsync(update);
    mgr.ApplyUpdatesAndRestart(update);
}

Velopack's SDKs for Rust, Node.js, Python and C++ take the same base address. The C# library also works without the platform, because it sends the runtime of the machine, but an x64 build running on an ARM64 PC would then be offered the ARM64 package. Velopack checks each download against the SHA256 in the feed rather than a signature, so serve Keygate over HTTPS.

Channels

Feeds serve the stable channel unless you ask for another. Sparkle and Tauri add channel=beta to the feed address. Velopack uses the channel the app was packed with: the default channels (win, osx, linux) read stable, and a package made with vpk pack --channel beta reads beta. A channel named after a runtime, which Velopack suggests for apps built for several architectures, also works: win-x64 reads stable for that platform and win-x64-beta reads beta. Feeds are public unless the product requires a license, which is safe because trust comes from the signature or hash of each file, not from keeping the address secret.

Minimum supported version

If old versions must stop working, for example after a protocol change, open Update settings, go to Required update and turn on Require older versions to update. Pick the minimum version from your published stable releases and add an optional message. From the API, these are the product's minimum_supported_version and minimum_supported_message; a message is refused unless a version is set with it or already. A feed never asks for more than it can deliver: if no release on it reaches the minimum, it asks for its newest release instead.

  • Sparkle marks every update at or above that version as critical for apps below it, and removes the option to skip it.
  • The Tauri feed adds the version and your message to its answer. The app reads them from update.rawJson and can tell the user why the update matters.
  • Velopack's feed has no place for it.

License gated downloads

For a download page that only customers can use, your site or app asks for a download link with the license key:

bash
curl -X POST https://licenses.example.com/api/v1/license/download \
  -H "Content-Type: application/json" \
  -d '{"license_key":"KG-ABCD2345-EFGH6789-JKLM2345-NPQR6789","platform":"darwin-arm64"}'

The answer contains a link that works for ten minutes, plus the version, size and SHA256 hash of the file. Leave out version for the newest release, or pin one.

Update periods

A perpetual license with an update period only covers releases published before its updates_until date. Keygate enforces this on the feeds and on downloads, but only if the feed knows which license is asking. That is what setting Access to Only customers with a valid license in Update settings does: from then on the feeds answer 401 LICENSE_KEY_REQUIRED unless the updater identifies the license.

The updater can do that in one of two ways:

  • Send the key in an X-License-Key header. Never put the key in the URL, where it ends up in proxy and CDN logs.
  • Or add the signed token from activate or verify as license_token in the address: Sparkle's SUFeedURL or feedParametersForUpdater, Tauri's endpoint, or the base address given to Velopack's C# UpdateManager. The token expires with the license's check in interval, so build the address from a fresh token. Velopack's other SDKs replace the query string of the address, so with them send the token in an X-License-Token header instead.

Pulling a bad release

If a release turns out to be broken, yank it. It disappears from the feeds straight away and updaters go back to offering the previous version. A yanked release can be put back if the problem turns out to be elsewhere, and a request for that exact version answers 410 while it is yanked. Your webhooks receive release.published, release.yanked and release.unyanked events.

Last updated October 4, 2026