Packaging app for macOS
Instructions for packaging a Flet app into a macOS application bundle.
This guide provides detailed macOS-specific information. Complementary and more general information is available here.
For a quicker, PyInstaller-based way to package desktop apps — no Flutter
toolchain required — see flet pack.
Prerequisites
Rosetta 2
Flutter, which we use for packaging, requires Rosetta 2 on Apple Silicon:
sudo softwareupdate --install-rosetta --agree-to-license
Xcode
Xcode 15 or later is required to compile native Swift or Objective-C code.
CocoaPods
CocoaPods 1.16 or later is required to install and compile Flutter plugins.
flet build macos
This command can be run on macOS only.
Builds a macOS application bundle from your Flet app.
Target architecture
By default, flet build macos creates a universal bundle that runs on both
Apple Silicon and Intel Macs. Packaging downloads Python wheels for both
arm64 and x86_64 architectures.
To limit packaging to a specific architecture, see this. This affects which Python wheels are bundled and, in turn, which CPU architectures the app will run on. You will then have to provide your users with the correct build for their Macs.
Permissions
macOS permissions are declared through Info.plist privacy usage strings and
app entitlements. You can also use the cross-platform permission bundles
to inject common entries, then override or extend them with platform-specific values.
Info.plist
Add or override Info.plist entries for macOS builds.
These values are written to macos/Runner/Info.plist of the build project.
Resolution order
Its value is determined in the following order of precedence:
--info-plist[tool.flet.macos.info]- Values injected by cross-platform permission bundles, if any.
Supported value forms
- flet build
- pyproject.toml
Accepts repeated <key>=<value> entries.
The <value> can be in one of the following forms:
trueorfalse(case-insensitive) for boolean values- integer and real number literals, for example
32or0.5 - TOML array literals, for example
["basic", "advanced"] - TOML inline tables, for example
{ NSAllowsArbitraryLoads = false } - any other value is treated as a string
Both simple and complex structures are supported:
- string
- boolean
- integer
- real
- dictionary (nested key-value object)
- array of strings
- array of booleans
- array of integers
- array of reals
- array of dictionaries (including dictionaries that contain arrays)
Example
- flet build
- pyproject.toml
flet build macos \
--info-plist LSApplicationCategoryType="public.app-category.utilities" \
--info-plist NSSupportsSuddenTermination=true \
--info-plist ExampleInteger=32 \
--info-plist ExampleReal=0.5 \
--info-plist 'SupportedModes=["basic", "advanced"]' \
--info-plist 'FeatureFlags=[true, false]' \
--info-plist 'RetryDelays=[1, 2, 3]' \
--info-plist 'OpacitySteps=[0.25, 0.5, 0.75]' \
--info-plist 'NSAppTransportSecurity={ NSAllowsArbitraryLoads = false }' \
--info-plist 'CFBundleDocumentTypes=[{ CFBundleTypeName = "Data File", CFBundleTypeExtensions = ["dat"] }, { CFBundleTypeName = "JSON File", CFBundleTypeExtensions = ["json"] }]'
[tool.flet.macos.info]
LSApplicationCategoryType = "public.app-category.utilities"
NSSupportsSuddenTermination = true
ExampleInteger = 32
ExampleReal = 0.5
SupportedModes = ["basic", "advanced"]
FeatureFlags = [true, false]
RetryDelays = [1, 2, 3]
OpacitySteps = [0.25, 0.5, 0.75]
NSAppTransportSecurity = { NSAllowsArbitraryLoads = false }
CFBundleDocumentTypes = [
{ CFBundleTypeName = "Data File", CFBundleTypeExtensions = ["dat"] },
{ CFBundleTypeName = "JSON File", CFBundleTypeExtensions = ["json"] },
]
Template translation
In the macos/Runner/Info.plist, the
example above will be translated accordingly into this:
<plist version="1.0">
<dict>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>NSSupportsSuddenTermination</key>
<true/>
<key>ExampleInteger</key>
<integer>32</integer>
<key>ExampleReal</key>
<real>0.5</real>
<key>SupportedModes</key>
<array>
<string>basic</string>
<string>advanced</string>
</array>
<key>FeatureFlags</key>
<array>
<true/>
<false/>
</array>
<key>RetryDelays</key>
<array>
<integer>1</integer>
<integer>2</integer>
<integer>3</integer>
</array>
<key>OpacitySteps</key>
<array>
<real>0.25</real>
<real>0.5</real>
<real>0.75</real>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<false/>
</dict>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>Data File</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>dat</string>
</array>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>JSON File</string>
<key>CFBundleTypeExtensions</key>
<array>
<string>json</string>
</array>
</dict>
</array>
</dict>
</plist>
Entitlements
Entitlements are property-list key-value pairs that grant an executable permission to use a service or technology. The supported value type depends on the entitlement key defined in the Apple Developer Entitlements Reference.
Entitlements are written to macos/Runner/DebugProfile.entitlements and
macos/Runner/Release.entitlements in the build template.
Resolution order
Its value is determined in the following order of precedence:
-
[tool.flet.macos.entitlement] -
Values injected by cross-platform permission bundles, if any.
-
Defaults:
[tool.flet.macos.entitlement]"com.apple.security.app-sandbox" = false"com.apple.security.cs.allow-jit" = true"com.apple.security.cs.allow-unsigned-executable-memory" = true"com.apple.security.network.client" = true"com.apple.security.network.server" = true"com.apple.security.files.user-selected.read-write" = truenotecom.apple.security.cs.allow-unsigned-executable-memoryis required forctypes/cfficallbacks to work on Intel Macs when the app is signed with the hardened runtime (Apple'slibffiallocates writable-and-executable closure memory onx86_64). Apple Silicon is unaffected. Set it tofalseif your app targets onlyarm64and you want the strictest hardened runtime.
Supported value forms
- flet build
- pyproject.toml
Accepts repeated <key>=<value> entries.
The <value> can be in one of the following forms:
trueorfalse(case-insensitive) for boolean values- integer and real number literals, for example
32or0.5 - TOML array literals, for example
["group.example.one", "group.example.two"] - TOML inline tables, for example
{ "com.apple.mail" = ["compose"] } - any other value is treated as a string
Both simple and complex structures are supported:
- string
- boolean
- integer
- real
- dictionary (nested key-value object)
- array of strings
- array of booleans
- array of integers
- array of reals
- array of dictionaries (including dictionaries that contain arrays)
Example
- flet build
- pyproject.toml
flet build macos \
--macos-entitlements com.apple.security.network.client=true \
--macos-entitlements com.apple.developer.ubiquity-kvstore-identifier=ABCDE12345.dev.example.myapp \
--macos-entitlements ExampleInteger=32 \
--macos-entitlements ExampleReal=0.5 \
--macos-entitlements 'com.apple.security.application-groups=["group.dev.example.myapp", "group.dev.example.shared"]' \
--macos-entitlements 'ExampleBooleanArray=[true, false]' \
--macos-entitlements 'com.apple.security.scripting-targets={ "com.apple.mail" = ["compose", "send"] }' \
--macos-entitlements 'ExampleArrayOfDictionaries=[{ Name = "alpha", Enabled = true }, { Name = "beta", Enabled = false }]'
[tool.flet.macos.entitlement]
"com.apple.security.network.client" = true
"com.apple.developer.ubiquity-kvstore-identifier" = "ABCDE12345.dev.example.myapp"
ExampleInteger = 32
ExampleReal = 0.5
"com.apple.security.application-groups" = [
"group.dev.example.myapp",
"group.dev.example.shared",
]
ExampleBooleanArray = [true, false]
"com.apple.security.scripting-targets" = { "com.apple.mail" = ["compose", "send"] }
ExampleArrayOfDictionaries = [
{ Name = "alpha", Enabled = true },
{ Name = "beta", Enabled = false },
]
Template translation
In both macos/Runner/DebugProfile.entitlements and
macos/Runner/Release.entitlements, the example above
will be translated accordingly into this:
<key>com.apple.security.network.client</key>
<true />
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>ABCDE12345.dev.example.myapp</string>
<key>ExampleInteger</key>
<integer>32</integer>
<key>ExampleReal</key>
<real>0.5</real>
<key>com.apple.security.application-groups</key>
<array>
<string>group.dev.example.myapp</string>
<string>group.dev.example.shared</string>
</array>
<key>ExampleBooleanArray</key>
<array>
<true />
<false />
</array>
<key>com.apple.security.scripting-targets</key>
<dict>
<key>com.apple.mail</key>
<array>
<string>compose</string>
<string>send</string>
</array>
</dict>
<key>ExampleArrayOfDictionaries</key>
<array>
<dict>
<key>Name</key>
<string>alpha</string>
<key>Enabled</key>
<true />
</dict>
<dict>
<key>Name</key>
<string>beta</string>
<key>Enabled</key>
<false />
</dict>
</array>
Code signing
By default, the built app bundle is ad-hoc signed: it runs fine on the Mac that built it, but when other users download it, macOS Gatekeeper steps in. Since macOS 15 (Sequoia), there is no Control-click bypass anymore — users must approve every blocked item in System Settings → Privacy & Security → Open Anyway with an administrator password, and a Python app can trigger this per bundled library. For public distribution (excluding the Mac App Store), sign your app with a Developer ID Application certificate and notarize it.
Prerequisites
-
An Apple Developer Program membership.
-
A Developer ID Application certificate (create one, then install it — with its private key — into your login keychain). Verify with:
security find-identity -v -p codesigning
Signing the app
- flet build
- pyproject.toml
- env var
flet build macos --macos-signing-identity "Developer ID Application: Jane Doe (TEAM123456)"
[tool.flet.macos.signing] # or [tool.flet.macos.signing.<lane>]
identity = "Developer ID Application: Jane Doe (TEAM123456)"
FLET_MACOS_SIGNING_IDENTITY="Developer ID Application: Jane Doe (TEAM123456)"
The identity may be the exact certificate name, its SHA-1 fingerprint, or a
unique substring (for example, just the team ID). Passing "-" produces an
explicit ad-hoc signature.
Resolution order
The signing identity is determined in the following order of precedence:
--macos-signing-identity[tool.flet.macos.signing.<lane>].identity(per-lane)[tool.flet.macos.signing].identityFLET_MACOS_SIGNING_IDENTITYenvironment variable- Default: none — a plain build keeps its ad-hoc signature and no signing step runs, while notarize and App Store builds auto-discover the certificate.
When a real identity is configured, flet build macos will, after the build:
- Sign every bundled binary — including the embedded Python runtime and all native modules from your dependencies — "inside out", as Apple requires, with the hardened runtime enabled and a secure timestamp (both required for notarization). Entitlements are applied to the app bundle and to helper executables and helper bundles shipped by your dependencies; frameworks and libraries are signed without entitlements, per Apple guidance.
- Verify the result with
codesign --verify --deep --strictand check that no binary was left unsigned.
The build fails with an actionable error if the identity is not found in the keychain, if any file fails to sign, or if verification fails. An expired or revoked certificate is called out by name and status instead of appearing missing.
Identity auto-discovery
An identity counts as not configured only when the CLI option, the
pyproject.toml key, and the environment variable are all unset — the
resolution order above runs first, and any configured
value is matched as given, never silently replaced.
With no identity configured anywhere, build modes that cannot proceed without one discover it from the keychain:
- notarizing builds use your Developer ID Application certificate;
- App Store builds use your Apple Distribution
certificate (or its legacy equivalent,
3rd Party Mac Developer Application) for the app and your installer certificate for the.pkg.
Discovery succeeds when the keychain holds exactly one valid certificate of the required type — the chosen identity is printed in the build output. With several candidates (for example, certificates from two teams), the build fails with the candidate list; configure the certificate name or SHA-1 fingerprint explicitly. Plain builds (neither notarize nor App Store mode) never auto-discover.
Certificate types also scope explicit identities in these modes: a partial identity such as your team ID only has to be unique among certificates of the required type, not among all your certificates.
Distribution lanes
flet build macos signs and packages for one of three lanes, selected by a
single setting:
none(default) — sign only when a signing identity is configured; without one, the app keeps its ad-hoc signature.developer-id— sign with the hardened runtime, notarize and staple for direct distribution.app-store— sandboxed store signing plus a signed installer.pkgfor the Mac App Store.
By default, all lanes write to the same output directory, so each build overwrites the existing files. To keep separate output for each lane, pass a custom output directory for each build.
Resolution order
The distribution lane is determined in the following order of precedence:
--macos-distribution[tool.flet.macos.signing].distribution- Default:
none
Per-lane settings
Every [tool.flet.macos.signing] key except distribution may be set
in a per-lane subtable, which overrides the flat key when that lane builds.
This matters for the one setting whose value genuinely differs per lane —
the identity, whenever auto-discovery cannot
pick for you (say, certificates from several teams in one keychain):
- flet build
- pyproject.toml
The command line has no per-lane syntax: an invocation selects exactly one lane, so its options are inherently scoped to it — per-lane values are simply per-run values:
flet build macos --macos-distribution developer-id \
--macos-signing-identity "Developer ID Application: Jane Doe (TEAM123456)" \
--macos-notary-profile flet-notary
flet build macos --macos-distribution app-store \
--macos-signing-identity "Apple Distribution: Jane Doe (TEAM123456)" \
--macos-installer-identity "3rd Party Mac Developer Installer: Jane Doe (TEAM123456)" \
--macos-provisioning-profile path/to/File.provisionprofile \
# --output build/macos-store
[tool.flet.macos.signing]
# Decides which distribution lane subtable below gets chosen for the current build.
# To change lane without editing pyproject.toml, consider using the `--macos-distribution` CLI option instead.
distribution = "developer-id"
[tool.flet.macos.signing.developer-id]
identity = "Developer ID Application: Jane Doe (TEAM123456)"
notary_profile = "flet-notary"
[tool.flet.macos.signing.app-store]
identity = "Apple Distribution: Jane Doe (TEAM123456)"
installer_identity = "3rd Party Mac Developer Installer: Jane Doe (TEAM123456)"
provisioning_profile = "path/to/File.provisionprofile"
Settings resolve as: CLI option → lane subtable → flat key → environment
variable. For a key only one lane reads (like notary_profile
on developer-id lane or provisioning_profile on app-store lane),
the subtable and the flat form are equivalent — group by lane for readability,
or keep them flat for less nesting. A misnamed/inexisting subtable fails the build.
Switching lanes
One pyproject.toml can hold both lanes' settings — the notary profile,
provisioning profile, and installer identity are each read only by their
own lane, and App Store Info.plist keys are harmless in
Developer ID builds. Leave the identities to
auto-discovery (a pinned identity fits only one
lane), set your default lane in pyproject.toml, and flip it per build:
flet build macos --macos-distribution app-store
Notarization
A Developer-ID-signed app must also be notarized by Apple for Gatekeeper to open it without warnings. Notarization uploads the app to Apple's notary service (a malware scan, typically a few minutes), after which the resulting "ticket" is stapled to the app so it validates even offline.
Credentials
Setting up credentials takes two steps: create one with Apple, then make it available to Flet.
Creating a credential
Apple's notary service accepts two kinds — get whichever suits you:
- App Store Connect API key (recommended; also reusable for
store uploads) — in App Store Connect, open
Users and Access → Integrations
→ App Store Connect API → Team Keys → +. Name the key, give it
the Developer role, then download the
AuthKey_<KEY_ID>.p8file — possible only once — and note the key's Key ID and the Issuer ID shown at the top of the page. - Apple ID + app-specific password — at
account.apple.com → Sign-In and Security →
App-Specific Passwords → +, generate a
password dedicated to notarization
(your regular Apple ID password is not meant here and won't work with
notarytool).
Providing it to Flet
-
Keychain profile (best for local development) — a one-time setup that saves either kind of credential into the macOS keychain under a name of your choice. With an API key:
xcrun notarytool store-credentials flet-notary \--key /path/to/AuthKey_ABC123DEFG.p8 --key-id ABC123DEFG \--issuer 12345678-90ab-cdef-1234-567890abcdefor with an Apple ID (prompts for the app-specific password):
xcrun notarytool store-credentials flet-notary \--apple-id you@example.com --team-id TEAM123456From then on, only the profile name (here
flet-notary) is needed; the secrets never appear in your shell history, environment, orpyproject.toml. -
Environment variables (best for CI) — pass an App Store Connect API key inline on each run by setting
APPLE_API_KEY(path to the.p8file),APPLE_API_KEY_ID, andAPPLE_API_ISSUER. Nothing is stored on the machine, which suits ephemeral CI runners where no keychain profile exists — inject the values from your repository secrets.
Resolution order
Credentials are determined in the following order of precedence:
--macos-notary-profile[tool.flet.macos.signing.developer-id].notary_profile(per-lane)[tool.flet.macos.signing].notary_profileFLET_MACOS_NOTARY_PROFILEenvironment variable- The
APPLE_API_KEY,APPLE_API_KEY_IDandAPPLE_API_ISSUERenvironment variables (all three must be set) - Default: none — notarizing builds fail without credentials.
A configured profile intentionally has precedence over the APPLE_API_* variables, which
other tooling (Fastlane, CI images) may have exported for a different team.
Notarizing the app
- flet build
- pyproject.toml
- env var
flet build macos \
--macos-distribution developer-id --macos-notary-profile flet-notary
[tool.flet.macos.signing]
distribution = "developer-id"
notary_profile = "flet-notary"
FLET_MACOS_NOTARY_PROFILE="flet-notary"
The lane must still be selected with
--macos-distribution
developer-id (or [tool.flet.macos.signing].distribution); the
distribution lane has no environment-variable
equivalent.
When notarizing, the signing identity may be omitted entirely — see Identity auto-discovery.
If notarization is rejected, the build fails and prints Apple's notarization log, which lists the exact offending files.
Distributing
flet build leaves you with a signed, notarized, and stapled .app —
ship it as a single downloadable file, either a DMG (recommended) or a
zip archive.
- DMG
- zip archive
First, create the image — plain, or with a custom Finder look:
- Plain
- Custom look (dmgbuild)
hdiutil create -volname "MyApp" -srcfolder build/macos/MyApp.app -ov -format UDZO MyApp.dmg
hdiutil create packs the app into a compressed read-only image (UDZO)
containing just the app. For an Applications drop-shortcut, a
background image, and icon placement, see the Custom look tab.
For the polished look — background image, app on the left, Applications
on the right — use dmgbuild, a small
pip-installable tool that writes the Finder layout directly (no Finder or
GUI session involved, so it works the same in CI):
pip install "dmgbuild>=1.6.7"
Keep dmgbuild at 1.6.7 or later: images built with older versions show a blank background on macOS 26.2+.
Create dmg_settings.py next to your project, and customize accordingly:
files = ["build/macos/MyApp.app"]
symlinks = {"Applications": "/Applications"}
# 600x400 image; place an optional dmg/background@2x.png sibling
# next to it for Retina — both are combined automatically.
background = "dmg/background.png"
window_rect = ((200, 200), (600, 400))
icon_size = 110
icon_locations = {
"MyApp.app": (150, 210),
"Applications": (450, 210),
}
format = "UDZO"
The window is sized in points equal to the 1x image's pixel size, so draw any "drag the app to Applications" guidance directly into the background image.
The example above is just the classic subset — the
settings reference
covers much more: solid-color backgrounds, a custom volume icon (or a badge
composited onto the standard disk icon), extra files with hidden-file and
hidden-extension control, icon/text sizes and list-view layouts, a
multi-language license agreement shown before mounting (attach it before
signing the image), alternative image formats (UDBZ, ULFO) and the
APFS filesystem, and more.
Then build the image:
dmgbuild -s dmg_settings.py "MyApp" MyApp.dmg
Then sign, notarize, and staple the image:
codesign -f --timestamp -s "Developer ID Application: Jane Doe (TEAM123456)" MyApp.dmg
xcrun notarytool submit MyApp.dmg --keychain-profile flet-notary --wait
xcrun stapler staple MyApp.dmg
The result is the conventional macOS download: users open the image and drag the app into Applications.
ditto -c -k --keepParent build/macos/MyApp.app MyApp.zip
Use ditto (or Finder's Compress) rather than plain zip, which can
mangle the symlinks inside the bundled frameworks and break the app's
signature. A zip involves no extra signing — it can't be signed or
stapled — so after extraction Gatekeeper relies on the staple already on
the .app inside.
Signing and notarizing in CI
GitHub Actions
The steps below cover only signing and notarization — graft them onto a complete build workflow such as the one in the CI/CD guide.
A CI runner starts with an empty keychain, so the one-time setup is about getting your certificate and notary credentials into repository secrets:
-
Export the Developer ID Application certificate together with its private key: in Keychain Access, under login → My Certificates, right-click the certificate → Export… and save it in the
.p12format, protected by an export password (Apple's guide). The.cerfile downloadable from the developer portal is not a substitute: it holds only the public certificate, while the private key exists solely in the keychain of the Mac that created the certificate request — hence the export from Keychain Access there. -
Store the secrets — in the repository's Settings → Secrets and variables → Actions, or with the
ghCLI. Secrets hold text, so the binary.p12is stored base64-encoded (base64 -i path/to/certificate.p12 | pbcopyfills the clipboard), while the.p8key is already text (PEM) and goes in as-is:gh secret set MACOS_CERTIFICATE_P12 --body "$(base64 -i path/to/certificate.p12)"gh secret set MACOS_CERTIFICATE_PASSWORDgh secret set MACOS_SIGNING_IDENTITY --body "Developer ID Application: Jane Doe (TEAM123456)"gh secret set APPLE_API_KEY_P8 < path/to/AuthKey_ABC123DEFG.p8gh secret set APPLE_API_KEY_ID --body "ABC123DEFG"gh secret set APPLE_API_ISSUER --body "12345678-90ab-cdef-1234-567890abcdef"
The workflow then imports the certificate into the runner's keychain and
exposes the credentials to flet build:
- uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ secrets.MACOS_CERTIFICATE_P12 }}
p12-password: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
- name: Build, sign and notarize
env:
APPLE_API_KEY: ${{ runner.temp }}/AuthKey.p8
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
FLET_MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
run: |
printf '%s' "${{ secrets.APPLE_API_KEY_P8 }}" > "$APPLE_API_KEY"
flet build macos --macos-distribution developer-id
Troubleshooting
| Symptom | Cause and fix |
|---|---|
"MyApp" is damaged and can't be opened on users' Macs | The bundle was modified after signing — most commonly the app writes next to its own files at runtime. Write user data to os.getcwd() (Flet points it at a writable location) instead of paths derived from __file__. Also triggered by building with --no-compile-app/--no-compile-packages, which lets Python create __pycache__ inside the bundle at runtime. |
errSecInternalComponent when signing in CI | The keychain is locked — unlock it in the job, or use apple-actions/import-codesign-certs, which handles it. |
Notarization status Invalid | Read the printed notary log: typical causes are an unsigned binary that was added to the bundle after signing, or a certificate that is not a Developer ID Application certificate. |
library load disallowed by system policy | A native library is signed with a different Team ID than the app (or not at all). Rebuild so all binaries are re-signed together, or — if your app must load externally acquired native code at runtime — add the com.apple.security.cs.disable-library-validation entitlement. |
| Notarization takes very long | The first-ever submission for a new account can take up to an hour or more; subsequent submissions typically finish within minutes. |
Build hangs at the signing step (codesign at 0% CPU) | macOS is waiting on a keychain prompt — possibly hidden behind other windows — for permission to use the private key, common after importing a key from the terminal. Click Always Allow on the prompt, or pre-authorize codesign with security set-key-partition-list -S apple-tool:,apple: -s -k <login-password> login.keychain-db. |
Warning: unable to build chain to self-signed root | Apple's intermediate certificate authorities are missing from the keychain, so the signature can't chain up to Apple's root. Sign in to Xcode (Settings → Accounts), which installs them, or download them from Apple PKI. |
Mac App Store
The signing support above targets direct distribution (your website,
GitHub releases, etc.). For the Mac App Store — including TestFlight —
flet build macos has a dedicated mode that produces a signed installer
.pkg ready for App Store Connect. In this mode the app is signed with
your Apple Distribution certificate — sandboxed, without the hardened
runtime — your provisioning profile is embedded, the store-mandated
application-identifier/team-identifier entitlements are applied (helper
executables and helper bundles get the sandbox inherit pair), and every
hardened-runtime
exception entitlement (com.apple.security.cs.*, including the defaults)
is stripped: they are meaningless without the hardened runtime and
scrutinized by App Review. Notarization does not apply to store
submissions — the app-store lane never notarizes.
Store prerequisites
One-time setup, requiring an Apple Developer Program membership.
Creating the distribution certificates
Store builds need two certificates. Create both under Certificates → + (if you don't have a certificate request file yet, see Generating a CSR — the process is identical for macOS):
-
Apple Distribution — signs the app bundle.
-
Mac Installer Distribution — signs the installer
.pkg. It appears in your keychain as3rd Party Mac Developer Installer, and because it signs packages rather than code,security find-identity -v -p codesigningdoes not list it. Verify it with:security find-identity -v -p basic
Download each certificate and double-click it to install it — with its private key — into your login keychain.
Registering an App ID
Under Identifiers
→ + → App IDs → type App, register an explicit App ID whose
bundle ID exactly matches your app's (by default <org>.<project name>
from pyproject.toml). No extra capabilities are needed.
Creating the provisioning profile
Under Profiles → +, select Mac App Store Connect (under Distribution), then:
- Select the App ID registered above.
- Select your Apple Distribution certificate.
- Name the profile and click Generate.
- Download the
.provisionprofilefile and keep it with your project — it contains no secrets (it is a document signed by Apple authorizing your App ID and team), so it is safe to commit.
Creating the App Store Connect app record
In App Store Connect → My Apps → + → New App: platform macOS, the bundle ID from above, any name and SKU. Then note the app's numeric Apple ID under App Information → General Information — command-line uploads are keyed to it.
Provisioning profile
The profile created above. A relative
path resolves against the project directory (where pyproject.toml lives).
The build embeds it at Contents/embedded.provisionprofile — sealed by the
app's signature — and fails fast when the profile's App ID does not cover
the app's bundle ID, a mismatch that would otherwise surface only after
upload as ITMS-90889.
Resolution order
The provisioning profile is determined in the following order of precedence:
--macos-provisioning-profile[tool.flet.macos.signing.app-store].provisioning_profile(per-lane)[tool.flet.macos.signing].provisioning_profileFLET_MACOS_PROVISIONING_PROFILEenvironment variable- Default: none — App Store builds fail without one.
Installer identity
The certificate that signs the .pkg — the exact certificate name (as
listed by security find-identity -v -p basic), its SHA-1 fingerprint, or
a unique substring, matched only among installer certificates.
Resolution order
The installer identity is determined in the following order of precedence:
--macos-installer-identity[tool.flet.macos.signing.app-store].installer_identity(per-lane)[tool.flet.macos.signing].installer_identityFLET_MACOS_INSTALLER_IDENTITYenvironment variable- Default: none — the certificate is auto-discovered.
Building for the App Store
- flet build
- pyproject.toml
- env var
flet build macos --macos-distribution app-store \
--macos-provisioning-profile certs/MyApp_MacAppStore.provisionprofile \
--info-plist LSApplicationCategoryType="public.app-category.productivity" \
ITSAppUsesNonExemptEncryption=False
[tool.flet.macos.info]
# required by App Store validation
LSApplicationCategoryType = "public.app-category.xxx-yyy-zzz"
# optional
ITSAppUsesNonExemptEncryption = false
[tool.flet.macos.signing]
distribution = "app-store"
provisioning_profile = "certs/MyApp_MacAppStore.provisionprofile"
FLET_MACOS_PROVISIONING_PROFILE="certs/MyApp_MacAppStore.provisionprofile"
The lane must still be selected with
--macos-distribution
app-store (or [tool.flet.macos.signing].distribution); the
distribution lane has no environment-variable
equivalent.
- Setting
LSApplicationCategoryTypeis required — App Store validation rejects the package without it. See supported/possible values here. - Setting
ITSAppUsesNonExemptEncryptionis optional but answers the export-compliance question once and for all. If it's not set, App Store Connect walks you through an export compliance questionnaire every time you upload a new version of your app. If set tofalseindicates that your app does not use encryption, which can help streamline the submission process.
Both are ordinary Info Property List keys.
Neither signing identity appears in the examples above: both are
auto-discovered when not configured. To pin
them explicitly, configure the signing identity for
the app certificate and the installer identity for
the .pkg certificate.
The lane's resolution order is shared by all distribution channels.
Uploading
Upload the .pkg with Transporter
or from the command line, authenticating with the same
App Store Connect API key used for notarization — altool
reads the .p8 file from ~/.appstoreconnect/private_keys/:
xcrun altool --validate-app -f build/macos/MyApp.pkg -t macos \
--apiKey <KEY_ID> --apiIssuer <ISSUER_ID>
xcrun altool --upload-package build/macos/MyApp.pkg -t macos \
--apiKey <KEY_ID> --apiIssuer <ISSUER_ID> \
--apple-id <NUMERIC_APP_ID> --bundle-id <BUNDLE_ID> \
--bundle-version <BUILD_NUMBER> --bundle-short-version-string <VERSION>
<NUMERIC_APP_ID> is the app record's Apple ID
noted earlier, and every
upload needs a unique build number.
After processing (minutes; failures arrive by email as ITMS-xxxx codes),
the build appears in the TestFlight tab of your app record — internal
testers can install it without beta review. Note that --validate-app
does not catch everything processing checks, so a clean upload is only
confirmed once processing completes.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
ITMS-90889: Invalid Provisioning Profile after upload | The embedded profile doesn't match the upload. flet build pre-checks that the profile covers the app's bundle ID, so this usually means the profile doesn't include the Apple Distribution certificate that signed the app — regenerate the profile selecting that certificate and rebuild. |
91109: Invalid package contents … com.apple.quarantine | A file in the package carries the quarantine attribute macOS puts on downloads. flet build strips it from everything it packages, so this points to files added after the build — rebuild, or clean with xattr -cr before re-packaging. |
| The store build can't read or write files the direct build could | The app-store lane enables the mandatory App Sandbox: file access is confined to the app's container (~/Library/Containers/<bundle-id>). Relative paths and os.getcwd() already land there; for anything outside, let the user pick the path with FilePicker — user-selected locations are granted to a sandboxed app. |
After a TestFlight install, flet build macos fails with Permission denied under build/ | macOS app relocation: the installer updates an existing copy with the same bundle ID wherever it finds one — including your local build products — leaving root-owned files (_MASReceipt) behind. sudo rm -rf the affected build/macos directory, and delete dev copies of the app before installing the store build. |