Ziqi (Katrina) Ding

Back

The Problem#

If you distribute a macOS app outside the App Store, there’s no built-in way to push updates. Users stay on whatever version they downloaded first, and every bug fix means asking them to go find the new build themselves.

The Solution#

Sparkle is the de facto open source update framework for macOS. The app checks an appcast.xml feed you host, and if there’s a newer version it downloads, verifies and installs it.

Getting it working end to end has two parts:

  • Phase 1: integrate Sparkle into the Xcode project and confirm it can reach the feed.
  • Phase 2: sign, notarise and staple the app so macOS lets the update actually run. I did this via Xcode for the first release.

I set this up for my own app SideTime, so the examples below use its name and feed URL.

How to Do It#

Phase 1: Integrating Sparkle#

1. Add Sparkle via SPM (Xcode).#

  1. Go to File -> Add Dependencies
  2. In the search input, enter Sparkle’s repo url: https://github.com/sparkle-project/Sparkle
  3. Version: use latest
  4. Click Add Package Add sparkle package
  5. On the next window, select your app under Add to Target Add Sparkle to your app
  6. Click Add package
  7. Xcode will automatically embed XPC services for sandboxed updates

2. Generate Sparkle EdDSA Keys#

Once Sparkle is added, you should be able to see it from your file navigator on Xcode under Package Dependencies. Then, do:

  1. Right click on the Sparkle dependency Sparkle x.x.x, select Show in Finder Show Sparkle in Finder
  2. Find the generate_keys from Finder and double click to run it. I recommend using Column View here for easy navigation Find generate_keys executable
  1. It will generate a public key for your app. Save the key somewhere, we will need to add it into Info.plist later. Generated public key

The private key is stored in your login Keychain. You can also export it to a file for scripting later with generate_keys -x private_key.txt. Keep it out of git. If you lose it, existing installs can no longer verify your updates.

3. Continue Xcode Configuration#

  1. If your app is sandboxed, enable incoming and outgoing network connection.

    • Outgoing Connections are needed for the app to:
      • Fetch the appcast.xml file which contains metadata about available updates
      • Download the update package
      • Verify the update signature using the public key
    • Incoming Connections are needed since Sparkle uses XPC services (interprocess communication) to
      • Launch a privileged helper tool that installs updates securely
      • Communicate between your app and the Sparkle updater process
      Enable incoming and outgoing network connection
  2. Selectively add the following configs into Info.plist

    <!-- Where Sparkle fetches metadata about available updates -->
    <key>SUFeedURL</key> 
    <string>https://katrinaading.github.io/SideTime/appcast.xml</string>
    
    <!-- The public key generated by Sparkle -->
    <key>SUPublicEDKey</key>
    <string>[YOUR_PUBLIC_KEY]</string>
    
    <!-- Enable auto checking for new updates -->
    <key>SUEnableAutomaticChecks</key>
    <true/>
    
    <!-- 
    (Optional) How often Sparkle checks for updates automatically, in seconds.
    If we don't set it, Sparkle defaults to 86400 sec (1 day)
    -->
    <key>SUScheduledCheckInterval</key>
    <integer>86400</integer>
    
    <!-- Required by sandboxed app or if you want to support secure, silent updates -->
    <key>SUEnableInstallerLauncherService</key>
    <true/>
    xml

    You can also verify or edit your configs from Xcode. Don’t forget to replace the SUFeedURL and SUPublicEDKey with your own values. Note that I didn’t set the SUScheduledCheckInterval here to use Sparkle’s default value (1 day).

    Verify or edit configs in Xcode
  3. If you have SUEnableInstallerLauncherService enabled, you will need to configure following entitlements as well. This entitlement is a temporary exception that allows your sandboxed macOS app to communicate with specific Mach services—in this case, Sparkle’s XPC services used for secure updates:

    • -spks: Sparkle’s status service, used to report update progress
    • -spki: Sparkle’s installer service, used to perform the actual update See official document here. In your <YOUR_APP>.entitlements, add the following:
<key>com.apple.security.temporary-exception.mach-lookup.global-name</key>
<array>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spks</string>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)-spki</string>
</array>
xml

You can also add or verify them from Xcode

Configure entitlements with InstallerLauncher enabled

4. Prepare and host a sample appcast.xml#

Create a appcast.xml with no release items and make it publicly accessible (e.g., Github Pages), ensuring Sparkle can fetch information regarding future updates through this file.

<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
	xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle"
	xmlns:dc="http://purl.org/dc/elements/1.1/">
	<channel>
		<title>SideTime Updates</title>
		<link><!-- Your SUFeedURL here --></link>
		<description>Software updates for SideTime</description>
		<language>en</language>
		<!-- No release items yet - will show "You're up to date!" until first release published -->
	</channel>
</rss>
xml

For more information, checkout official documentation:

5. Local Testing#

Build and run your app, and try checking for updates. You should see something like this:

I have version 0.0.1 here since I set this version in Xcode

If you see error, double check if your appcast.xml is accessible by go to the url directly.

Update Error

Phase 2: Signing, notarising and stapling your app (via Xcode)#

Brief intro#

What are they?

Signing

Code signing proves the app was created by a trusted developer and hasn’t been tampered with. It includes a timestamp so the signature remains valid even after your certificate expires. It also enables Hardened Runtime, which is required for notarisation.

Notarising

App notarisation is a security process Apple requires for distributing macOS apps outside the Mac App Store. It’s essentially Apple’s way of saying, “We’ve scanned this app, it’s free of known malware, and it’s allowed to run on macOS.” Although it’s technically optional, notarisation is highly recommended for distributing your MacOS software, otherwise users would see Gatekeeper warnings when launching.

Resource: https://developer.apple.com/documentation/security/notarizing-macos-software-before-distribution

During notarising, Apple scans your signed app for malware and code-signing issues, then returns a notarisation ticket if the scan passes. This ticket is stored on Apple’s servers and can be stapled to your app.

Stapling

Stapling is to embed the notarisation ticket into your app or .dmg, so Gatekeeper can verify it offline. Without stapling, Gatekeeper must contact Apple’s servers to verify the ticket.

Here’s a summary table

StepXcode GUICLI ToolPurposeRequired For…
SigningcodesignAuthenticates developer, enables runtimeNotarization, Gatekeeper
NotarisingnotarytoolMalware scan + ticket issuanceGatekeeper approval
StaplingstaplerEmbeds ticket for offline verificationSmooth user installs

In this guide, I will go through the manual distribution process via Xcode first.

Preparation#

To sign and notarise your app, we will need to enrol in the Apple Developer Program. Go to Apple Developer, sign in or create a new account, pay AU$149 annual fee and wait for approval.

Then, in your account, go to Certificates, IDs & Profiles-> Certificates,

Certificates entry

We should see that we have an existing Certificate with Development type. This is not enough for app distribution. We will need to create one as Developer ID Application type.

Certificates

Press the ”+” button next to the title Certificates. On the new page, select Developer ID Application, and hit Continue.

Certificate type to create with

On the next page, it requires us to create and submit a Certificate Signing Request (CSR). This can be done via MacOS built-in app Keychain Access.

Keychain Access

In the pop-up, enter your email that matches your Apple developer account email and common name; select Save to disk. For more information, see Apple documentation.

Certificate created

Then, back to the Developer site, click Choose File and upload the certificate we just created. Hit Continue.

On the success page, follow the hint to install the certificate.

Download your certificate to your Mac, then double click the .cer file to install in Keychain Access. Make sure to save a backup copy of your private and public keys somewhere secure.

It will open in Keychain Access and install under My Certificates. You can also verify it’s installed by running:

security find-identity -p codesigning
sh

Look for:

Developer ID Application: Your Name (TEAMID)
sh

Properly store the certificates, we will need that to setup automation in later stages.

To verify if Xcode has recognised the certificate, go to Xcode -> Settings -> Accounts -> Your account -> Manage Certificates, The Developer ID Application Certificates should be listed here.

Xcode -> Settings -> Accounts -> Your account -> Manage Certificates

We also need to enable Hardened Runtime as mentioned before. It’s in Xcode → your app target → Signing & Capabilities → + Capability → Hardened Runtime. Leave all exceptions off unless you truly need one (JIT, unsigned memory, etc.).

Add Capability

Now, we can start our distribution process.

Manually select signing certificate

Archiving, Signing and Notarising#

Ensure the correct developer account is selected under Signing & Capabilities; set a version and build number.

Double check the correct account is selected Version and build number

Then, select your build target to Any Mac

Build Target

Then, select Product -> Archive from top left app menu. It will start building; when building finished, the Archives window will pop up. Select the build and select Direct Distribution as distribution method.

Archives

It takes time for Apple to scan and notarise the app. For me it took 4 days. When the status becomes Ready to distribute, hover on the row, and click Export to export the notarised app. Xcode will automatically staple the notarisation ticket to your app.

You can double check the result on the exported .app:

# Signed with the Developer ID certificate?
codesign -dv --verbose=4 SideTime.app 2>&1 | grep -E "Authority|Runtime"

# Notarised and stapled?
xcrun stapler validate SideTime.app
spctl -a -vvv SideTime.app   # last line should say "source=Notarized Developer ID"
sh

Package for distribution#

Sparkle needs a downloadable archive to point at from the appcast. A plain .app won’t do. I ship a ZIP, which Sparkle handles natively and is the simplest to generate:

# Use ditto, NOT zip -- only ditto preserves the code signature correctly
ditto -c -k --sequesterRsrc --keepParent SideTime.app SideTime-v1.0.0.zip
sh

If you prefer a .dmg for first-time downloads, hdiutil does the job and the same signed, notarised .app goes inside:

hdiutil create -volname "SideTime" -srcfolder SideTime.app -ov -format UDZO SideTime-v1.0.0.dmg
sh

Either way, upload the archive somewhere public (I use a GitHub Release), then run Sparkle’s generate_appcast against the folder to sign it with your EdDSA key and produce the real appcast.xml. That publishing step deserves its own post.

Tips#

  • ditto, not zip. Zipping with zip strips resource forks and Sparkle’s signature check fails on the user’s machine.
  • Apple’s notarisation queue is usually minutes, not days. If Xcode’s Organizer sits on “Processing” for ages, you can submit the exported app from the CLI with xcrun notarytool submit --wait instead.
  • SUFeedURL must be https. Sparkle refuses plain http feeds by default.
  • Bump both the version and build number before every archive. Sparkle compares CFBundleVersion to decide whether an update is newer.
  • Back up the EdDSA private key and the Developer ID certificate together. Losing either one means existing users can’t update.

Summary#

With Sparkle added via SPM, an EdDSA key in Info.plist, the right entitlements, and a signed, notarised and stapled build exported from Xcode, your macOS app can update itself from a hosted appcast.xml.

References#

Integrate Sparkle to enable auto-update on MacOS app
https://katrina-ziqi-ding.com/blog/sparkle-setup
Author Ziqi (Katrina) Ding
Published at 04-10-2025