Guide
Signing and distribution
Putting a build on a real device and into a store — where the settings live, what you have to get from Apple and Google yourself, and which of these paths have never been run.
Everything else in this documentation runs on a simulator or an emulator, signed with a key that means nothing. This page is about the other half: a build that installs on a phone somebody hands you, and a file a store will take.
That half cannot be done for you. A certificate is issued to a person, a
provisioning profile lists devices by their serial numbers, and an upload key is
a secret you generate and then must never lose. What an can do is know where
those things are, check them before it spends two minutes compiling, and say
which one is missing in a sentence you can act on. What it cannot do is have
them.
What exists
Section titled “What exists”| Command | What comes out | Needs |
|---|---|---|
an ios --physical |
The .app, signed, installed on a connected iPhone or iPad |
A development certificate and a profile listing that device |
an ios --archive |
<Name>.xcarchive and <Name>.ipa |
A distribution certificate and an App Store or ad-hoc profile |
an android --sign |
<Name>-release.apk |
A keystore you generate |
an android --aab |
<Name>-release.aab for Google Play |
The same keystore, plus bundletool |
an wearos --sign, an wearos --aab |
The same, for a Wear OS device | The same keystore |
an macos --sign |
The .app, Developer ID signed, hardened runtime |
A Developer ID certificate |
an macos --notarize |
The same, notarised and stapled | That, plus notarytool credentials |
an macos --dmg |
<Name>.dmg |
Nothing, but see below |
And what does not exist: tvOS, visionOS, watchOS and Wear OS have no device or
store path of their own. They build for their simulators and stop there. There
is no upload command anywhere either — no an talks to App Store Connect or to
the Play Console, and nothing here submits an app for review.
Where the settings live
Section titled “Where the settings live”In angular-native.json, next to app and platforms, in a signing object
keyed by platform:
{ "app": { "name": "MyApp", "bundleId": "com.example.myapp", "entry": "src/main.native.ts" }, "platforms": ["ios", "android"], "signing": { "ios": { "team": "ABCDE12345", "identity": "Apple Development", "profile": "ios/profiles/development.mobileprovision" }, "macos": { "identity": "Developer ID Application", "notaryProfile": "an-notary" }, "android": { "keystore": "../secrets/release.keystore", "keyAlias": "upload", "storePasswordEnv": "AN_ANDROID_KEYSTORE_PASSWORD", "keyPasswordEnv": "AN_ANDROID_KEY_PASSWORD" } }}| Key | What it is |
|---|---|
ios.team |
The ten-character Team ID. Optional: with none, the profile’s own is used, and it is the authority anyway. |
ios.identity |
A prefix of the certificate’s name, as security find-identity -v -p codesigning prints it. Defaults to Apple Development for --physical and Apple Distribution for --archive. |
ios.profile |
Path to the .mobileprovision, relative to the project. |
macos.identity |
Same, and it is nearly always Developer ID Application. |
macos.notaryProfile |
The name of a notarytool keychain profile. Not a credential. |
android.keystore |
Path to the keystore, relative to the project. |
android.keyAlias |
Which key inside it. |
android.storePasswordEnv |
The name of an environment variable. Defaults to AN_ANDROID_KEYSTORE_PASSWORD. |
android.keyPasswordEnv |
Same, defaulting to AN_ANDROID_KEY_PASSWORD, and to the store password when that variable is unset. |
There is no macos.team, on purpose. Nothing on that platform needs it: the
certificate carries the team and notarytool gets it from the keychain profile.
A key in the manifest that changes nothing is a key somebody spends an afternoon
getting right.
The environment wins
Section titled “The environment wins”Every setting has an environment variable that overrides the file:
| Variable | Overrides |
|---|---|
AN_IOS_TEAM |
signing.ios.team |
AN_IOS_IDENTITY |
signing.ios.identity |
AN_IOS_PROFILE |
signing.ios.profile |
AN_MACOS_IDENTITY |
signing.macos.identity |
AN_MACOS_NOTARY_PROFILE |
signing.macos.notaryProfile |
AN_ANDROID_KEYSTORE |
signing.android.keystore |
AN_ANDROID_KEY_ALIAS |
signing.android.keyAlias |
AN_BUNDLETOOL |
Where bundletool.jar is |
That is what CI uses, where the keystore is decoded into a temporary directory
and the path is different on every run. It is also the only source there is when
an runs inside this repository, which has no project manifest at all.
Never commit a credential
Section titled “Never commit a credential”No password goes in angular-native.json. The file is committed; a password
in it is a password in your history. So the manifest holds the name of an
environment variable, and a literal one is refused by name:
angular-native.json: signing.android.storePassword is a secret, and this file is committed.Name an environment variable instead of holding the value: "storePasswordEnv": "AN_SOMETHING_PASSWORD"and export the value where the build runs. If this password has already beenpushed, it has to be changed.That check runs when the manifest is read, which every command in a project does — not only the signing ones. A secret noticed only by the command that needs it is a secret that sits in a repository for months.
The files are checked too. If the keystore or the profile is inside the project and git is not ignoring it, the build stops before compiling anything:
release.keystore is the release keystore, and git is not ignoring it: the next`git add .` commits it.Add it to .gitignore first: echo 'release.keystore' >> .gitignoreand if it is already committed, the message says the credential itself is spent — because it is. Anybody who has ever cloned the repository has it.
an init puts *.keystore, *.jks, *.p12 and *.mobileprovision into the
project’s .gitignore for exactly this reason. The best place for a keystore is
still outside the project altogether.
What you have to get: Apple
Section titled “What you have to get: Apple”None of this can be automated, and all of it is done once.
-
An Apple ID, and for most of it a paid membership. A free Apple ID can sign a build onto your own device — Xcode issues a seven-day certificate for it. Everything else needs the Apple Developer Program, which is $99 a year: TestFlight, the App Store, and a Developer ID certificate for distributing a Mac app outside the store. There is no way around that and no free tier of it.
-
A certificate, in this Mac’s keychain. For
--physical, an Apple Development certificate — Xcode ▸ Settings ▸ Accounts ▸ Manage Certificates ▸ + ▸ Apple Development creates one and installs it in a single step. For--archive, an Apple Distribution one, from developer.apple.com/account/resources/certificates. Foran macos --sign, a Developer ID Application one, from the same page.A
.cerdownloaded from that page is only half of it: it carries the public certificate, and it is useless without the private key that was generated when you made the request. If you are moving to a new Mac, export a.p12from the old one’s keychain — that is the file that carries both. -
An App ID, at developer.apple.com/account/resources/identifiers, matching your
app.bundleIdexactly. A wildcard one (com.example.*) works for development and is not accepted for anything using push notifications or iCloud. -
The device’s UDID, at developer.apple.com/account/resources/devices.
xcrun devicectl list devicesprints it with the phone plugged in. -
A provisioning profile, at developer.apple.com/account/resources/profiles, tying the three together: an iOS App Development profile for
--physical, listing that device; an App Store profile for--archive. Download it and put it wheresigning.ios.profilepoints. -
For
an macos --notarize, notarytool credentials. An app-specific password from account.apple.com ▸ Sign-In and Security ▸ App-Specific Passwords, stored once in the keychain:Terminal window xcrun notarytool store-credentials an-notary \--apple-id you@example.com \--team-id ABCDE12345 \--password xxxx-xxxx-xxxx-xxxxan-notaryis then whatsigning.macos.notaryProfilenames. The password stays in the keychain and never reaches the project. -
On the device: Developer Mode on. Settings ▸ Privacy & Security ▸ Developer Mode. The device restarts. Without it
devicectlrefuses the install, and the refusal does not say why.
What you have to get: Google
Section titled “What you have to get: Google”Less, and none of it costs anything until you publish.
-
A keystore, which you generate. Nobody issues it. It is a file, it holds a key, and Google Play ties your app to it forever — an app already published cannot be updated with a different key without asking Google to reset it.
Terminal window keytool -genkeypair -v -keystore ~/secrets/myapp-release.keystore \-alias upload -keyalg RSA -keysize 2048 -validity 10000Keep it somewhere the project cannot reach. Back it up. Losing it is the one mistake on this page with no clean recovery.
-
bundletool, for--aab. It is not part of the Android SDK — Gradle pulls it in as a dependency, and there is no Gradle here.Terminal window python3 scripts/fetch-android-deps.pyleaves it in
vendor/android/tools/bundletool.jar. Or pointAN_BUNDLETOOLat a copy you already have. -
A Play Console account, $25 once, at play.google.com/console. You create the app there, and the first upload is where Play offers Play App Signing: accept it. Google then holds the key that signs what users install, and the key in your keystore becomes the upload key — the one that proves the bundle came from you. If you lose an upload key you can be issued another; there is no equivalent for the app signing key, which is why Play would rather hold it.
-
A version code that goes up.
android:versionCodeinandroid/AndroidManifest.xml. Play refuses a bundle whose version code it has already seen, and it refuses it on upload — after everything has been built.an android --aabchecks that there is one before it compiles anything, but it cannot know which numbers you have already used.
The commands
Section titled “The commands”an ios --physical
Section titled “an ios --physical”an ios --physical # the only device plugged inan ios --physical --device "Jane's iPhone"an ios --physical --no-launch # build and sign, install by handIt builds for aarch64-apple-ios rather than the simulator target, embeds the
profile in the bundle as embedded.mobileprovision, and signs with the
entitlements the profile grants. That last part is not a detail: the system
gives an app nothing its profile does not carry, so entitlements signed on top of
one produce an app that installs and is killed the moment it launches. The
plugins’ requested keys are merged in and the profile wins every collision.
With one device connected it is used; with several you are asked to name one.
an ios --archive
Section titled “an ios --archive”an ios --archiveImplies --release. It writes build/ios/<Name>.xcarchive — the app under
Products/Applications, the debug symbols under dSYMs — and
build/ios/<Name>.ipa, whose path it prints.
There is no --method flag. Whether that .ipa can go to TestFlight, to the
App Store or onto a handful of ad-hoc devices was decided by the certificate and
the profile you signed it with; a flag here would only be a second place to say
the same thing and a second place to get it wrong.
Uploading is not part of this. Open the .xcarchive in Xcode’s Organizer, or use
xcrun altool --upload-app.
an android --sign and --aab
Section titled “an android --sign and --aab”export AN_ANDROID_KEYSTORE_PASSWORD='…'an android --sign --release # a release-signed APKan android --aab --release # the bundle Play takes--release is about the compiler and --sign is about the key. They are
separate because they are separate things, and a build for Play wants both.
--aab implies --sign: Play takes nothing signed with a debug key.
The artefacts are named apart — <Name>-release.apk, <Name>.apk — so a
release build cannot quietly overwrite the debug APK your emulator has been
running.
A bundle is not an APK with a different extension. Its manifest and resources are
protobuf, its layout is a module zip, bundletool assembles it and jarsigner
signs it, because apksigner refuses one. All of that is an’s problem, not
yours; what is yours is the keystore and the version code.
an macos --sign, --notarize, --dmg
Section titled “an macos --sign, --notarize, --dmg”an macos . --sign # Developer ID + hardened runtimean macos . --notarize # that, submitted, waited on, stapledan macos . --notarize --dmg # and packed into a signed, notarised .dmgWithout --sign, an macos signs ad hoc: it runs on the machine that built it
and nowhere else. --dmg on its own still works and says so out loud — a .dmg
of an ad-hoc build is a fine way to check the packaging and useless as a
download.
Signing turns on the hardened runtime, which notarisation requires, and which
forbids mapping writable executable memory — the first thing a JavaScript engine
does. So the build declares com.apple.security.cs.allow-jit. Without it the
app is killed on startup with a Killed: 9 that mentions no entitlement
anywhere; it is the single most confusing failure on this page, and it is handled
rather than left to you.
Stapling is not optional and is easy to skip: without it the app is notarised but
the ticket lives on Apple’s servers, so the first person to open it offline is
told the app cannot be opened, with nothing to distinguish that from an app that
was never notarised at all. --notarize staples.
The .dmg is signed and notarised in its own right when --notarize is on,
because the image is the file that gets downloaded. A .dmg carrying a perfectly
notarised app is still an unsigned download.
When something is missing
Section titled “When something is missing”Every one of these stops before anything is compiled, and names the thing:
- a platform with no
signingsection — and shows the block to paste and the environment variables that do the same job; - a profile that is not there, or that is not a profile;
- a profile that expired, with the date;
- a profile for another app, with both bundle ids;
- a team that disagrees with the profile’s;
- a certificate that is not in the keychain — listing the ones that are;
- a keystore that is not there, with the
keytoolline that creates one; - a keystore whose password is wrong, or whose alias is not in it: the keystore
is opened up front, so both arrive in a third of a second rather than after two
minutes at
apksigner; - an environment variable nobody exported, by name;
bundletoolmissing, naming all three places it was looked for;- a
devicectlthat is too old, and the Xcode that has one.
And when a tool refuses anyway, what it said is repeated with the two things it never mentions: which identity was used, and which file was being signed.
What has actually been run
Section titled “What has actually been run”Being precise about this matters more here than anywhere else in these docs, because a signing path that has never executed looks exactly like one that has.
Exercised end to end by scripts/check-signing.sh, on a machine with no Apple
account: the whole Android release path. A keystore generated by the check, an
APK signed with it and opened afterwards to confirm whose certificate is on it, a
bundle assembled, signed and unzipped to confirm its layout. Also the reading and
checking of provisioning profiles — the check builds real CMS-wrapped profiles
with openssl and a throwaway certificate, so expiry, app id and team are
validated against real files.
Exercised as far as the credential: everything else Apple. The check
confirms an ios --physical and --archive get through the profile and stop at
codesign naming the identity they wanted, and that an macos --notarize
reports the certificate and the notary profile together. The codesign
invocation itself is not run, because there is no certificate to run it with.
Never run, by anybody, anywhere: devicectl installing on a physical
iPhone, notarytool submitting to Apple, stapler, and any store upload. Those
were written from Apple’s documentation. If you are the first person to hold an
Apple Developer account and point this at a real device, expect to find something
here — and the --no-launch variants build and sign the artefact either way, so
you can drag it into Xcode’s Devices window and compare.