Shipping a macOS app means choosing a distribution channel. The three main options — Mac App Store, direct notarized distribution, and Homebrew — have different requirements, costs, and user expectations.
This guide compares them across code signing, sandboxing, updates, pricing, discoverability, and the developer experience of each.
The App Store is the most restrictive channel and the one with the most built-in infrastructure.
AppTransaction where available, or receipt validation when you need to support older APIs.Sandboxing is the most common source of friction. The app runs in a container with minimal default access. Common capabilities and their entitlements:
| Capability | Entitlement |
|---|---|
| Read files or folders chosen in an open dialog | com.apple.security.files.user-selected.read-only |
| Read/write the user’s Downloads folder | com.apple.security.files.downloads.read-write |
| Network access (outbound) | com.apple.security.network.client |
| Network access (inbound, e.g. a local server) | com.apple.security.network.server |
| Camera | com.apple.security.device.camera |
| Microphone | com.apple.security.device.microphone |
| USB devices | com.apple.security.device.usb |
If the app manages files outside the container (a text editor, a media organizer), you need to use security-scoped bookmarks or NSOpenPanel to gain access. Users pick files through the standard open dialog, you save a bookmark, and restore access on subsequent launches.
App Review checks that sandbox entitlements match the app’s described functionality. Declaring network server access for a note-taking app that does not need it can raise review questions or lead to rejection.
The App Store handles updates automatically. Users get notified, the store downloads the update, and the system installs it. No work on your side beyond submitting a new version.
Under Apple’s standard terms, the commission is 30% for paid apps and in-app purchases. It drops to 15% for auto-renewable subscriptions after the first year and for developers enrolled in the App Store Small Business Program, which has a $1M annual proceeds threshold across associated accounts.
Distributing outside the App Store — through your own website or GitHub Releases, often with an updater such as Sparkle — is the most flexible option.
Hardened Runtime is not a sandbox. It is a set of security protections. Some entitlements allow exceptions to those protections, while others grant access to protected resources such as the camera or microphone:
| Capability | Entitlement |
|---|---|
| Allow JIT compilation | com.apple.security.cs.allow-jit |
| Allow unsigned executable memory | com.apple.security.cs.allow-unsigned-executable-memory |
| Allow DYLD environment variables | com.apple.security.cs.allow-dyld-environment-variables |
| Camera | com.apple.security.device.camera |
| Audio input | com.apple.security.device.audio-input |
Unlike sandboxing, Hardened Runtime does not restrict file system access by default — subject to macOS permissions, the app can read and write anywhere the user can. Some entitlements allow hardening exceptions; others grant access to protected resources.
# 0. Store notarization credentials once
xcrun notarytool store-credentials "AC_PASSWORD" \
--apple-id "you@example.com" \
--team-id "TEAMID"
# 1. After exporting and signing MyApp.app, package it
ditto -c -k --keepParent MyApp.app MyApp.zip
# 2. Upload for notarization
xcrun notarytool submit MyApp.zip \
--keychain-profile "AC_PASSWORD" \
--wait
# 3. Staple the ticket to the app
xcrun stapler staple MyApp.app
# 4. Recreate the ZIP so it contains the stapled app
ditto -c -k --keepParent MyApp.app MyApp.zipThe --wait flag blocks until Apple finishes scanning. For CI, use polling instead.
Without the App Store, you need an update mechanism. Sparkle is the de facto standard. It is an open-source framework that checks for updates, downloads them, and applies them.
Sparkle requires:
<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<item>
<title>Version 2.0</title>
<sparkle:version>2.0</sparkle:version>
<enclosure url="https://example.com/MyApp-2.0.zip"
sparkle:edSignature="..."
length="1234567"
type="application/octet-stream" />
<sparkle:releaseNotesLink>
https://example.com/release-notes-2.0.html
</sparkle:releaseNotesLink>
</item>
</channel>
</rss>Sparkle handles differential updates, background downloading, and installation. It is more work than the App Store’s built-in updates but gives you full control.
Homebrew is a package manager for command-line tools and GUI apps that appeals to developers and power users. For GUI apps, a cask usually installs the same signed and notarized artifact offered as a direct download.
cask "myapp" do
version "2.0"
sha256 "abc123..."
url "https://example.com/MyApp-#{version}.zip"
name "MyApp"
desc "A useful macOS app"
homepage "https://example.com"
livecheck do
url :homepage
strategy :page_match
regex(/href=.*?MyApp[._-]v?(\d+(?:\.\d+)+)\.zip/i)
end
depends_on macos: ">= :ventura"
app "MyApp.app"
zap trash: [
"~/Library/Application Support/MyApp",
"~/Library/Preferences/com.example.MyApp.plist",
]
endFor open-source command-line tools that Homebrew can build from source, use a formula:
class MyTool < Formula
desc "A useful CLI tool"
homepage "https://example.com"
url "https://example.com/mytool-2.0.tar.gz"
sha256 "def456..."
license "MIT"
def install
system "make", "install", "PREFIX=#{prefix}"
end
test do
system "#{bin}/mytool", "--version"
end
endBinary-only command-line tools may need a cask or a personal tap instead of a formula in homebrew/core.
homebrew/homebrew-cask (or write a formula and submit it to homebrew/core for a suitable CLI tool).brew install --cask myapp.New formulas and casks in Homebrew’s official repositories are included in autobump automation by default. A livecheck block helps Homebrew discover new upstream versions when the default URL-based detection is insufficient. Definitions excluded from autobumping need manual update PRs.
brew upgrade.| Mac App Store | Direct + Notarization | Homebrew | |
|---|---|---|---|
| Developer fee | $99/year | $99/year | None (but you still need $99/year for code signing GUI apps) |
| Sandbox required | Yes | No | No |
| App review | Yes | No (notarization only) | Cask or formula review |
| Revenue share | 15-30% | None | None |
| Updates | Automatic | App-managed (for example, Sparkle) | brew upgrade or the app’s updater |
| User base | General Mac users | General Mac users | Developers/power users |
| Bandwidth | Apple hosts | You host | You host |
| Trial/demo support | No built-in | Yes | Depends on the distributed app |
| Distribution control | Apple | You | You |
Many macOS developers use a multi-channel strategy:
App Store distribution requires a sandboxed build signed for the store. Direct downloads require a Developer ID-signed build and usually need an update mechanism such as Sparkle. A Homebrew cask can install the same signed and notarized artifact that you distribute directly.
Xcode supports multiple build configurations per target. Set up:
Release — for the App Store (sandbox enabled, App Store certificate).Release-Direct — for direct distribution (no sandbox, Developer ID certificate).The CODE_SIGN_IDENTITY differs per configuration. The App Store build uses sandbox entitlements. If the direct build needs Hardened Runtime exceptions or protected-resource permissions, keep them in a separate file:
App.entitlements — includes sandbox entitlements.App-Direct.entitlements — includes only the Hardened Runtime exceptions or protected-resource permissions that the direct build needs.For a simple app bundle, the signing flow looks like this. If your app embeds frameworks, helpers, or extensions, sign those nested components first and sign the outer app last.
# Sign and package for direct distribution
# Add --entitlements App-Direct.entitlements if the direct build needs it.
codesign --force --options runtime --timestamp \
--sign "Developer ID Application: Your Name (TEAMID)" \
MyApp.app
ditto -c -k --keepParent MyApp.app MyApp.zip
# Submit for notarization
xcrun notarytool submit MyApp.zip \
--keychain-profile "AC_PASSWORD" \
--wait
# Staple
xcrun stapler staple MyApp.app
# Recreate the ZIP so it contains the stapled app
ditto -c -k --keepParent MyApp.app MyApp.zip