Distributing Mac apps outside of the App Store requires more than just building and zipping your .app.

Since macOS Catalina, Apple requires all apps to be notarized by Apple to run without warnings on users' machines. Notarization ensures the app is from a known developer and free of malicious code.

Without notarization, users will see a warning saying the app “can’t be opened because Apple cannot check it for malicious software.”

With proper notarization and signing, your app behaves like a first-class citizen on macOS, even outside the App Store.

Here’s a streamlined process to do it correctly:

Step 1: Archive Your App in Xcode

Open your project in Xcode and create an archive:

In the menu, go to Product > Archive.

Image description

After the archive builds, the Organizer window will open. Click "Distribute App" on the build you want to export, then click "Direct Distribution".

Image description

When it's done, click "Export", and save the app somewhere; you'll receive a signed .app.

Step 2: Create the notarized DMG

To simplify post-export steps, I wrote a script that takes your exported .app, packages it into a .dmg, submits it for notarization, and staples the result — all automatically.

Before doing anything, install the necessary tools:

brew install create-dmg

Now we want to export the credentials that you already have into Xcode to the keychain, so that other tools can use it:

xcrun notarytool store-credentials "AC_PASSWORD" --apple-id $EMAIL --team-id $TEAM_ID

Perfect - you can now use my create-my-dmg.sh script to create a DMG.

#!/bin/bash
# brew install create-dmg
# xcrun notarytool store-credentials "AC_PASSWORD" --apple-id $EMAIL --team-id $TEAM_ID
set -exu
APP_BUNDLE="$1"
if [[ ! -d "$APP_BUNDLE" ]]; then
echo "Usage: $0 <app-bundle-path>"
exit 1
fi
# Extract version and name from the app bundle
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist"
VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$INFO_PLIST")
if [[ -z "$VERSION" ]]; then
echo "Error: Could not determine version from Info.plist"
exit 1
fi
APPNAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" "$INFO_PLIST")
if [[ -z "$APPNAME" ]]; then
echo "Error: Could not determine name from Info.plist"
exit 1
fi
IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -n 1 | awk '{print $2}')
if [[ -z "$IDENTITY" ]]; then
echo "Error: Could not determine signing identity. In order to sign the app, you need to request a Developer ID Application certificate from Apple."
exit 1
fi
DMG_NAME="$APPNAME-$VERSION.dmg"
DMG_PATH="./$DMG_NAME"
VOLUME_NAME="$APPNAME $VERSION"
echo "Creating DMG for $APPNAME $VERSION -> $DMG_PATH"
# Create DMG (using create-dmg)
create-dmg \
--volname "$VOLUME_NAME" \
--volicon "$APP_BUNDLE/Contents/Resources/AppIcon.icns" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "${APPNAME}.app" 200 190 \
--hide-extension "${APPNAME}.app" \
--app-drop-link 600 185 \
--codesign "$IDENTITY" \
--notarize AC_PASSWORD \
"$DMG_PATH" \
"$APP_BUNDLE"

You can run it using:

create-my-dmg.sh YourMacApp.app

This will create a DMG like YourMacApp-1.2.0.dmg, correctly notarized and stapled.