Skip to content

iOS build & device testing — step by step ​

An end-to-end walkthrough: from a clean Mac to the QR Setu app running on a physical iPhone.

Machine setup (Xcode, Node, CocoaPods) lives in macOS · iOS Build Environment — this page assumes it is done and covers the build and test loop itself. What to actually check once it runs is in Parity Verification.

Read this first — your iPhone X must be on iOS 16.4 or newer

Verified against this repo's lockfile, not assumed:

  • React Native 0.86 requires iOS 15.1+ and Xcode 16.1+.
  • But expo-router depends on @expo/ui, expo-glass-effect and expo-symbols, and the first two declare :ios => '16.4' in their podspecs. They are direct dependencies of the router, not optional extras we chose, so they cannot be dropped. The effective floor is therefore iOS 16.4.
  • An iPhone X tops out at iOS 16.7.x — its final release. So it works, but with only ~0.3 of headroom.

Before you start: Settings → General → About → check the iOS version. If it is below 16.4, update it (Settings → General → Software Update). On an iPhone X you cannot go higher than 16.7.x, and that is fine.

Disk space — the real constraint on a 256 GB Mac mini ​

This matters more than the M4's speed. Budget honestly before you begin:

ItemSize
Xcode (app + one iOS platform)~20–25 GB
Additional iOS Simulator runtime, if you add one~7–10 GB each
This repo — node_modules alone is 5.2 GB~6.5 GB
ios/ after prebuild — Pods + build artefacts~2–4 GB
~/Library/Developer/Xcode/DerivedData — grows every build2–10 GB, unbounded
CocoaPods spec repo + cache (~/.cocoapods, ~/Library/Caches/CocoaPods)~1–3 GB

Plan for ~40 GB before you write a line of code. Two habits that keep a 256 GB machine healthy:

bash
# DerivedData is pure cache — safe to delete any time, Xcode rebuilds it.
rm -rf ~/Library/Developer/Xcode/DerivedData/*

# List and remove Simulator runtimes you are not using (each is several GB).
xcrun simctl runtime list
xcodebuild -downloadPlatform iOS -exportPath /dev/null   # only if a runtime is missing

You do not need extra Simulator runtimes for this project — Xcode ships with one, and your real target is the iPhone X anyway.

Cleanup is allowlist-only ​

Reclaim from the table below and nothing else. Never sort ~/Library/Developer by size and delete the big folders. The rule that matters: everything that frees meaningful space is regenerable cache, and everything that breaks your device is tiny. There is no path that is both large and dangerous, so a size-sorted cleanup has no upside and a real downside. Delete from the ✅ rows and stop.

PathFreesVerdict
~/Library/Developer/Xcode/DerivedData/*2–10 GB✅ Pure build cache
Unused Simulator runtimes (xcrun simctl runtime list)7–10 GB each✅ Not needed here
~/Library/Caches/CocoaPods + ~/.cocoapods1–3 GB✅ Re-downloads on next pod install
~/Library/Developer/CoreSimulator/Caches1–5 GB✅ Regenerates
apps/mobile/ios/2–4 GB✅ Generated — but redo signing 6c + 6d after the next prebuild
~/Library/Developer/Xcode/iOS DeviceSupport/*3–8 GB⚠️ Regenerates, but costs a 5–20 min "Preparing debugger support" pass during which the phone is not a usable build destination. Only delete under real pressure
~/Library/MobileDevice/Provisioning Profiles/KB❌ No space win. Deleting breaks signing until Xcode re-mints (6d)
Keychain → Apple Development: … certificate0 bytes❌ Never. Must be recreated by hand (6b)
/var/db/lockdown/KB❌ No space win. Destroys the trust pairing, and the phone vanishes from Xcode entirely

This has already bitten us once

A 15 GB cleanup on the Mac mini (2026-07-28) removed enough of the above that the iPhone X stopped appearing in Xcode as a build destination. Nothing was broken on the phone and nothing needed reinstalling — but the recovery order is not guessable, which is why it is now written down in The iPhone disappeared from Xcode below. Tracked as QRS-232.

The M4 needs no special handling

Apple Silicon is the native, default path for Expo/RN today. No Rosetta, no arch -x86_64 prefixes, no excluded_architectures Podfile hacks. Ignore any Stack Overflow answer telling you otherwise — those are Intel-era workarounds and they will actively break an M4 build. 16 GB of RAM is comfortable for this project.

Why local builds, and when EAS earns its place ​

A fair question, since the EAS free tier looks like it should remove the need for any of this. It does not — and the reason is Apple's, not Expo's.

EAS Build cannot install an app on your iPhone without a paid Apple Developer account. Expo's own documentation is explicit that internal/ad-hoc distribution "requires a paid Apple Developer account", because the build must be signed with a distribution certificate and an ad-hoc provisioning profile listing the device UDID — credentials that only a $99/yr membership can create. The free Personal Team used by local Xcode signing is a different mechanism: Xcode generates a 7-day, machine-local profile that cannot be exported to a CI service.

So the comparison is not "15 free cloud builds vs. a local build". It is:

Local on the Mac miniEAS Free
Runs on your iPhone X✅ free, via Personal Team signing❌ needs the $99 Apple membership first
iOS Simulator build✅ unlimited✅ no Apple account needed
iOS builds/monthunlimited15 (not 25 — 15 iOS + 15 Android, counted separately)
Turnaround~10–20 min first build, then incremental + hot reloadfull cloud build every time
Queuenonelow priority, 1 concurrency, 45-min timeout
Cost$0 (hardware already owned)$0

The decisive line is the first one. Everything below it only matters if the first one passes.

And even with unlimited free cloud builds, the dev loop would still be local. No one iterates against a 20-minute round trip when expo start hot-reloads JS changes in under a second. EAS is a distribution and CI tool, not a development tool — comparing it to the local loop is comparing two different jobs.

Where EAS Free is genuinely worth using ​

Not "never" — just not here:

  • Android release builds. 15/month is ample for releases, EAS manages the keystore, and it sidesteps the documented Windows OOM pathology that scripts/build-android.mjs exists to work around. This is a real win worth taking independently of iOS.
  • iOS, once the $99 is paid — which will be needed anyway for TestFlight and the store. At that point EAS handles ad-hoc credentials and TestFlight upload, which is genuinely tedious by hand.
  • The macOS CI runner the parity plan calls for — EAS is one way to satisfy it without self-hosting.

eas build --local is a third path: it runs the EAS pipeline on your own machine and consumes no build quota, giving eas.json reproducibility without cloud waits. It does not change the Apple-account requirement for device builds.

The recommendation ​

Local Mac builds for the iOS dev loop; EAS Free for Android release builds; revisit iOS-on-EAS when the $99 is paid for distribution. Buy the Apple membership when you need TestFlight, push notifications, or Associated Domains — or when the 7-day re-signing becomes more annoying than $99/yr. Not before.

Step 1 — Get the code ​

bash
git clone https://github.com/digious-platforms/qrsetu.git
cd qrsetu
git checkout feat/mobile-nativewind-b2   # or the branch you are testing

nvm install && nvm use                   # honours .nvmrc (Node 22)
node -v                                  # confirm before continuing

npm install                              # workspace install, from the ROOT — takes a few minutes

Always install from the repo root

This is an npm-workspaces monorepo. Running npm install inside apps/mobile creates a second, broken dependency tree and produces confusing native build errors later.

Step 2 — Verify the toolchain before building ​

Cheap to check, expensive to debug later.

bash
xcodebuild -version        # must be 16.1 or newer, and must NOT say "CommandLineTools"
pod --version
node -v                    # 22.x

If xcodebuild reports the CommandLineTools path instead of Xcode:

bash
sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
sudo xcodebuild -license accept

Step 3 — Generate the native iOS project ​

bash
cd apps/mobile
npx expo prebuild -p ios

This reads app.json and generates apps/mobile/ios/, then runs pod install. First run downloads the CocoaPods spec repo and compiles nothing yet — expect a few minutes.

ios/ is generated — never edit it by hand

apps/mobile/ios/ and apps/mobile/android/ are both gitignored. Anything you change inside them is destroyed by the next expo prebuild and never reaches another machine or CI.

All native configuration belongs in app.json (or a config plugin): ios.bundleIdentifier, ios.buildNumber, ios.deploymentTarget, plugins. This is the same lesson the Android gradle.properties incident taught — a native file edited by hand is a change that exists on exactly one machine.

Confirm the deployment target that was actually generated:

bash
grep "platform :ios" ios/Podfile
grep -m1 IPHONEOS_DEPLOYMENT_TARGET ios/*.xcodeproj/project.pbxproj

(The .xcodeproj is named by prebuild from expo.name in app.json, so the glob avoids guessing it.)

If either shows a value below 16.4, CocoaPods will warn or fail on @expo/ui/expo-glass-effect. Pin it explicitly in app.json and prebuild again:

jsonc
"ios": {
  "bundleIdentifier": "in.digious.qrsetu",
  "buildNumber": "1",
  "deploymentTarget": "16.4",   // add this only if the generated value is lower
  "supportsTablet": true
}
bash
npx expo prebuild -p ios --clean

Step 4 — Run on the Simulator first ​

Always prove the build works on the Simulator before involving code signing. It removes an entire class of variables.

bash
npm run ios          # = expo run:ios

The first compile is the slow one (10–20 minutes is normal; the M4 handles it well). Later runs are incremental and fast. Metro starts automatically; leave it running.

Once it launches, sanity-check the things this phase actually fixed:

  • The splash background flows into BrandSplash with no colour jump, in light and dark mode.
  • Buttons scale on press.
  • Toggle dark mode: Simulator → Features → Toggle Appearance (or ⌘⇧A).

Step 5 — Prepare the iPhone X ​

  1. Update to iOS 16.7.x if it is below 16.4 (see the warning at the top).
  2. Connect it to the Mac mini by cable, unlock it, and tap Trust This Computer (it asks for the passcode).
  3. Enable Developer Mode: Settings → Privacy & Security → Developer Mode → on, then restart the phone. iOS 16 requires this before it will run any self-signed app.

The Developer Mode menu item does not exist yet — do not go looking for it first

On iOS 16+, Developer Mode only appears in Settings once a Mac has attempted to install a development build. On a phone that has never had one, that row is simply absent, and hunting for it reads like something is broken. So the real order is: finish Step 6, run Step 7, let it fail, then enable Developer Mode and re-run Step 7. That is why this is listed third here and not as a prerequisite.

Step 6 — Set up free signing (no $99 needed) ​

One-time, in the Xcode GUI. Do all four sub-steps before running any build command — they are strictly ordered, each one produces what the next needs, and skipping ahead yields errors that name the wrong cause. expo run:ios can only reuse signing assets; it never creates a certificate, registers a device, or mints a profile.

bash
open ios/*.xcworkspace     # the .xcworkspace, NOT the .xcodeproj

If Xcode has never been launched, open it first (open -a Xcode) and let it install its additional components. Its menu bar lives at the top of the screen and only shows Xcode's menus when Xcode is frontmost; Settings is under the Xcode menu (Cmd + ,), not File, and was called Preferences on older versions.

6a · Add your Apple ID. Cmd + , → Accounts → + → Apple ID → sign in. A team named <Your Name> (Personal Team) appears. No payment, no enrollment.

6b · Create the development certificate. Adding the Apple ID does not create one, and without it every build fails with CommandError: No code signing certificates are available to use. Still in Accounts: select your Apple ID → Manage Certificates… → + → Apple Development → wait for the row to appear → Done.

6c · Configure the target. In the project navigator click the topmost row — the app project (blue icon), not the Pods project below it and not a file inside it. The editor splits into PROJECT / TARGETS; under TARGETS pick the app target (not …Tests) → Signing & Capabilities tab → tick Automatically manage signing → Team = your Personal Team.

6d · Select the physical device as the destination — this is the step everyone misses. If Status shows:

⚠ Your team has no devices from which to generate a provisioning profile ⚠ No profiles for in.digious.qrsetu were found

…the cause is that the destination selector at the top of the window is still on a Simulator. Simulator builds need no signing, so Xcode has no device to register and nothing to build a profile for. Click the destination selector, choose your iPhone under the physical-device section, and both warnings clear together — they are one root cause stated twice. Click Try Again if they linger. Selecting a real device is what registers its UDID against your Personal Team and mints the iOS App Development profile.

Bundle identifier conflicts

If Xcode says the bundle identifier is unavailable, change it in app.json, not in Xcode — e.g. in.digious.qrsetu.dev — and re-run prebuild. ios/ is generated, so an Xcode edit is wiped by the next prebuild.

The keychain prompt during codesign ​

The first signed build raises "codesign wants to access key 'Apple Development: …' in your keychain." That asks for the Mac login password — not the Apple ID password. Click Always Allow, not Allow, or it reappears on every build.

If it seems to reject a password you know is right:

  1. Click inside the password field first. The dialog often appears without keyboard focus — especially when raised by xcodebuild from a terminal — so keystrokes go nowhere.
  2. Verify the password independently: security unlock-keychain ~/Library/Keychains/login.keychain-db. Silence means correct; passphrase you entered is not correct means the login keychain is still on an older password, which happens when the account password was reset via Apple ID/iCloud recovery rather than System Settings.
  3. Remove the prompt entirely: Keychain Access → login → My Certificates → expand Apple Development: … → select the private key → Cmd+I → Access Control → Allow all applications to access this item → Save.
  4. Check for duplicate identities: security find-identity -v -p codesigning. Two or more Apple Development entries means codesign may be picking one whose private key is missing — no password fixes that; remove the dead duplicate.

Do not use the widely-copied security set-key-partition-list -k <password> … one-liner: it puts your login password in plaintext into shell history.

Step 7 — Run on the iPhone X ​

bash
npx expo run:ios --device --configuration Release   # for parity testing — see the warning below

Pick your iPhone from the list. If the Xcode GUI build stalls on signing, the terminal command is worth trying anyway — the two take different paths through the keychain and one succeeding where the other fails is not unusual.

Release, not debug, for anything you intend to compare against Android

The Android side is tested as a release APK (npm run build:android), so an iOS debug build is not a like-for-like comparison: debug does not embed the JS bundle (it is fetched from Metro at every cold start, so the app needs the Mac running and the same Wi-Fi and re-bundles on each launch), keeps dev warnings on, and runs JS + reanimated unoptimized. Animation smoothness and startup in particular will differ for reasons that have nothing to do with iOS.

A Release install is also the only one that behaves like a normally-installed app: unplug the cable, no Mac, no Wi-Fi dependency. The cable is only required during the install itself. Use debug (npx expo start --dev-client, press i) for iterating on JS; re-install Release before signing off on parity.

On first launch the app will refuse to open and iOS will show "Untrusted Developer" — that is expected. Trust the certificate once:

Settings → General → VPN & Device Management → Apple Development: <your Apple ID> → Trust

The phone needs internet for this — iOS verifies the certificate with Apple, and on a dead connection it hangs on "Verifying…" with no useful message. Then launch the app from the home screen.

Step 8 — Verify ​

Work through the checklist in Parity Verification. The iPhone X is a particularly good device for this because it has a notch and a home indicator but no Dynamic Island, so it exercises the safe-area handling that was broken on the iOS PWA (the transitional surface iOS native has since replaced).

Pay special attention to:

  • Safe areas — nothing under the notch; the tab bar and FAB sit above the home indicator.
  • Haptics — buttons produce a light tap. This is new and iOS-only (the approved carve-out: Android gets a ripple, iOS gets haptics). If you feel nothing, check Settings → Sounds & Haptics → System Haptics is on — iOS suppresses them globally when it is off, which is correct behaviour, not a bug.
  • Card shadow — the Setu Card preview in onboarding must have a visible shadow. It was previously clipped away on iOS by overflow: 'hidden'.
  • Swipe-back from the screen edge, against our custom headers.
  • Light and dark, both.

The iPhone disappeared from Xcode ​

A six-rung recovery ladder. The phone stops appearing in the destination selector, usually after a disk cleanup, a macOS update, or an Xcode upgrade. Nothing is wrong with the phone and the app does not need reinstalling — one of five pieces of local state is missing, and they have to be restored in order, because each rung's symptom masks the next one.

Diagnose first — this tells you which rung you are on:

bash
xcrun devicectl list devices                              # Xcode 15+ — what CoreDevice can see
xcrun xctrace list devices                                # works on older Xcode too
system_profiler SPUSBDataType | grep -A4 -i iphone        # is it even on the USB bus?

Work down until the phone reappears. Do not skip — a missing certificate looks identical to a missing profile.

Rung 0 · Rule out the boring cause. If system_profiler shows no iPhone, this is a cable, port or lock-screen problem, not a software one. Use a known-good data cable (charge-only cables are the classic trap), a different port, and make sure the phone is unlocked. 10 seconds, and it eliminates half the possibilities.

Rung 1 · Re-establish the trust pairing. Cleanup tools remove /var/db/lockdown, which is where the Mac stores its pairing records. The phone then charges normally and is simply invisible to Xcode.

  1. Unplug, lock and unlock the phone, plug back in.
  2. Tap Trust This Computer and enter the passcode.
  3. If that prompt never appears, the stale record is still there. Force it back with Settings → General → Transfer or Reset iPhone → Reset → Reset Location & Privacy. This is non-destructive — it clears permission grants, not data — and the trust prompt returns on the next connect.

Rung 2 · Re-enable Developer Mode. Re-pairing can switch it off. Settings → Privacy & Security → Developer Mode → on, then restart the phone. If the row is missing entirely, that is the Step 5 trap, not a new fault: iOS only shows it after a Mac has attempted a development install. Run npx expo run:ios --device once, let it fail, and the row appears.

Rung 3 · Let Xcode rebuild the device-support symbols. This is the most likely cause after a large cleanup. Open Xcode → Window → Devices and Simulators (Shift + Cmd + 2) and look at the phone's row. If it reads "Preparing debugger support for iPhone…" with a progress bar, ~/Library/Developer/Xcode/iOS DeviceSupport/ was deleted and Xcode is regenerating it.

The phone is not a selectable build destination until that finishes, which takes 5–20 minutes for iOS 16.7.16 on a first pass. Keep it plugged in and unlocked, and do not interrupt it. This is the rung that looks most like broken hardware and is purely a matter of waiting.

Rung 4 · Read the warning triangle. If the phone is listed with a yellow triangle, click the row — Xcode states the reason in plain text there rather than in the build log. On Xcode 26.6 against iOS 16.7 the support files are built in, so anything mentioning an unsupported OS version means Rung 3 has not completed.

Rung 5 · Restore the signing assets. Once the phone is visible but the build fails, two independent things may have been removed. Check both:

bash
security find-identity -v -p codesigning          # expect an "Apple Development: <you>" line
ls ~/Library/MobileDevice/Provisioning\ Profiles/ # expect at least one .mobileprovision
  • No certificate → redo 6b (Xcode → Settings → Accounts → Manage Certificates → + → Apple Development).
  • No profile → redo 6d: select the physical iPhone as the destination. That is what re-registers its UDID and mints a fresh iOS App Development profile. Neither expo run:ios nor a rebuild will create these for you.

Rung 6 · Regenerate the native project. Safe at any time, because ios/ is generated output:

bash
cd apps/mobile && rm -rf ios && npx expo prebuild -p ios

Then redo 6c and 6d — a fresh ios/ has no signing configuration, and this is the step that gets forgotten.

Installing a Release build on the phone ​

A Release-configuration build installs on your own device with free signing:

bash
cd apps/mobile
npx expo run:ios --device --configuration Release

"Production build" means something different on a free Personal Team

This is a local Release build for your own registered device, and it still carries the 7-day expiry below. It is not a distributable build: TestFlight, ad-hoc distribution and App Store submission all require the $99 Apple Developer Program membership. Use this for performance and parity verification — which is what it is for — and do not read a successful Release build as "ready to ship to users".

⚠ --configuration Release ON iOS TARGETS PRODUCTION, AND ANDROID DOES NOT [QRS-671] ​

Use a Debug build for device testing and drift review. A Release iOS build silently reads .env.production, so it talks to qr-setu-prod — a different database with the pre-v2 schema. This page recommended --configuration Release in three places until 2026-08-14; that advice was wrong.

The chain, measured rather than assumed:

StepWhat happens
react-native-xcode.sh:55any non-Debug configuration ⇒ DEV=false
@expo/cli .../exportEmbedAsync.js:208setNodeEnv(options.dev ? 'development' : 'production')
@expo/env getEnvFiles({ mode = process.env.NODE_ENV })loads .env.${NODE_ENV} ⇒ .env.production

So iOS Release ⇒ NODE_ENV=production ⇒ .env.production ⇒ qr-setu-PROD. And if .env.production is absent on that machine (it is git-ignored, like every .env.*), @expo/env logs "NODE_ENV … required but was not specified", falls back to .env.local + .env, finds neither, and you get a Release build with no credentials at all — the QRS-668 symptom, in a binary where there is no Metro log to read.

Android is immune, and only because someone hardened it: build-android.mjs:150 force-sets childEnv.NODE_ENV = ENV (defaulting to development) and prints the target project, with --env=production as the deliberate opt-in. iOS has no equivalent wrapper — this is the same guard asymmetry as QRS-668.

For device testing, use Debug — it resolves NODE_ENV=development ⇒ .env.development ⇒ qr-setu-dev:

bash
cd apps/mobile
npm run check:env                       # ✓ must print dyhjofjjuazhyqcvlrkx (qr-setu-dev)
npm run ios -- --device                 # Debug. No --configuration Release.

⚠ Do not work around this with NODE_ENV=development … --configuration Release. It does load the right env file (setNodeEnv respects a pre-set value), but setNodeEnv also sets globalThis.__DEV__ = NODE_ENV !== 'production', so you get a Release binary running dev-mode React with dev warnings and dev-only code retained. That measures nothing useful about release performance or size. A real Release-against-Dev build needs an iOS wrapper mirroring build-android.mjs; until it exists, Release is for Prod-targeted builds only.

⚠ Personal development teams … do not support the Sign in with Apple capability [QRS-674] ​

If Xcode's Signing & Capabilities pane shows this, it is the real cause and the No profiles were found error below is only its downstream symptom. A free Personal Team cannot sign com.apple.developer.applesignin, so Xcode refuses to create any profile for the bundle id — which the CLI reports as a provisioning problem, sending you to re-run builds, delete ios/ and regenerate certificates. None of that can work while the entitlement is present.

Fixed in-repo by apps/mobile/plugins/withoutAppleSignInEntitlement.js, which strips the entitlement unless EXPO_APPLE_SIGNIN=1. If you hit this, you are on a commit before that fix or your ios/ predates it:

bash
cd apps/mobile
npx expo config --type introspect | grep -c applesignin   # expect 0; if 1, pull latest
npx expo prebuild -p ios                                  # regenerate the entitlements file

⚠ The entitlement is not in app.json, so you cannot rule it out by reading the config.ios.usesAppleSignIn is unset and expo-apple-authentication is not in the plugins array — yet @expo/prebuild-config's createLegacyPlugin auto-applies the package's plugin whenever the dependency is installed. Introspection is the only reliable check.

The feature is not removed: expo-apple-authentication stays a dependency and signInWithAppleNative degrades to provider_unavailable. Reverting this is a required step when the paid membership arrives — see Apple Developer Program Enrolment, which is the single checklist for every free-team accommodation in the repo.

⚠ No profiles for 'in.digious.qrsetu' were found — and re-running the SAME command can never fix it [QRS-673] ​

❌  No profiles for 'in.digious.qrsetu' were found: Xcode couldn't find any iOS App Development
    provisioning profiles matching 'in.digious.qrsetu'. Automatic signing is disabled and unable to
    generate a profile. To enable automatic signing, pass -allowProvisioningUpdates to xcodebuild.
CommandError: Failed to build iOS project. "xcodebuild" exited with error code 65.

This is a self-perpetuating trap in Expo CLI, not a misconfiguration on your side, and the loop is the important part: npx expo run:ios will produce this same error forever once you are in it. Measured in node_modules/@expo/cli:

js
// run/ios/codeSigning/configureCodeSigning.js:68
async function ensureDeviceIsCodeSignedForDeploymentAsync(projectRoot) {
  if (isCodeSigningConfigured(projectRoot)) return null;      // ← team already in the .pbxproj
  return configureCodeSigningAsync(projectRoot);
}

// run/ios/XcodeBuild.js:280
const developmentTeamId = await ensureDeviceIsCodeSignedForDeploymentAsync(props.projectRoot);
if (developmentTeamId) {
  args.push(`DEVELOPMENT_TEAM=…`, '-allowProvisioningUpdates', '-allowProvisioningDeviceRegistration');
}

So -allowProvisioningUpdates is passed only on the run that has to configure signing from scratch. Once DEVELOPMENT_TEAM is in the .pbxproj, the helper returns null, the flag is omitted, and xcodebuild is left able to use an existing profile but never to create or renew one. If the profile is missing — a regenerated ios/, or the 7-day expiry — the CLI cannot recover on its own.

Your own log proves which branch ran. The line › Auto signing app using team(s): "…" is emitted from inside isCodeSigningConfigured's return true path (configureCodeSigning.js:88) — i.e. the path that leads to return null and a suppressed flag. Seeing that line together with this error is the signature.

Fix — mint the profile with something that is allowed to talk to Apple ​

Option A (recommended, and the only one that also registers the device): one pass through Xcode.

bash
open apps/mobile/ios/Setu.xcworkspace
  1. Top bar → destination → select the physical iPhone (not a Simulator).
  2. Signing & Capabilities → Automatically manage signing ticked → Team = your Personal Team.
  3. Press ⌘R. Xcode registers the device with the team and mints a fresh 7-day iOS App Development profile.
  4. Once it launches, go back to the CLI — expo run:ios now only has to use the profile, which it can.

Option B (no GUI): run xcodebuild yourself with the flag Expo omitted, then hand back to the CLI.

bash
cd apps/mobile/ios
xcodebuild -workspace Setu.xcworkspace -scheme Setu -configuration Release \
  -destination "id=<the UDID expo printed>" \
  -allowProvisioningUpdates -allowProvisioningDeviceRegistration \
  DEVELOPMENT_TEAM=<your team id> build
cd .. && npx expo run:ios --device --configuration Release   # now installs

⚠ Do not "fix" this by deleting ios/ and re-running prebuild. That is the move that most often causes it: a regenerated project has no profile, and whichever run first writes DEVELOPMENT_TEAM back into the .pbxproj closes the door on the CLI's own ability to mint one. Regenerate ios/ only for a real native-config change, and expect to do an Xcode pass afterwards.

The ⚠️ Script has ambiguous dependencies … 'Upload Debug Symbols to Sentry' warning in the same output is benign and unrelated — Sentry's build phase has no declared outputs, so Xcode re-runs it every build. It costs a few seconds and nothing else.

The 7-day expiry ​

Free Personal Team provisioning profiles expire after 7 days. The app then refuses to launch — this is Apple's limit, not a bug and not something to debug. The Apple ID and the Personal Team do not expire and never need re-adding; only the machine-local profile does, and a rebuild mints a fresh one:

bash
npx expo run:ios --device    # re-signs and reinstalls

Other free-tier limits: 3 apps per device, no TestFlight, and no push notifications or Associated Domains (which is why qrsetu:// is the iOS interim deep-link scheme). Buy the $99 membership when distribution is needed — development and parity verification do not need it.

Everyday loop, after the first build ​

bash
cd apps/mobile
npx expo start --dev-client     # Metro; press "i" to open on the device/Simulator

You only need to repeat expo run:ios when native code changes — a new native dependency, an app.json change, or a prebuild. Pure JS/TS changes hot-reload; do not rebuild for them.

Troubleshooting ​

pod install fails after a dependency change

bash
cd apps/mobile/ios && pod install --repo-update

If it stays broken, delete and regenerate — ios/ is generated output, so discarding it is always safe:

bash
cd apps/mobile && rm -rf ios && npx expo prebuild -p ios

Personal development teams … do not support the Push Notifications capability (plus Provisioning Profile … does not support the Push Notifications capability and Entitlements file defines the value "aps-environment" — all three are one root cause). expo-notifications writes the aps-environment entitlement on every iOS build and a free Personal Team cannot sign it. Fixed in-repo by the withoutPushEntitlement config plugin (QRS-234); if you hit it, you are on a commit before that fix, or ios/ predates it:

bash
cd apps/mobile
npx expo config --type introspect | grep -c aps-environment   # expect 0; if 1, pull latest
npx expo prebuild -p ios                                      # regenerate the entitlements file
npx expo run:ios --device --configuration Release

We use local notifications only, so the entitlement is not something we are giving up — it declares remote APNs push, which R1 does not have and which needs the $99 membership regardless.

The iPhone is not in the destination list at all — work the recovery ladder. Most common after a disk cleanup; usually Rung 1 (trust pairing) or Rung 3 (device-support symbols regenerating).

"Preparing debugger support for iPhone…" and the device is greyed out — Rung 3. iOS DeviceSupport was deleted; wait 5–20 minutes with the phone plugged in and unlocked.

No code signing certificates are available after a cleanup — Rung 5. The keychain certificate was removed; verify with security find-identity -v -p codesigning and redo Step 6b.

Your team has no devices… / No profiles for in.digious.qrsetu were found after a cleanup — Rung 5, profile half. Re-select the physical device as the destination (Step 6d) to re-mint it.

"Untrusted Developer" on launch — Step 7 covers it; trust the certificate in Settings.

"Unable to install… device is locked" — unlock the phone and keep it unlocked during install.

The app launched fine last week and now won't — the 7-day expiry. Re-run expo run:ios --device.

Build errors mentioning a deployment target or an unavailable API — the iOS 16.4 floor from Step 3. Check the phone's iOS version and the generated Podfile.

Metro serves a stale bundle

bash
npx expo start -c

Disk full mid-build — clear DerivedData (see the disk-space section); it is pure cache.

Anything suggesting arch -x86_64, Rosetta, or excluded_architectures — ignore it. Those are Intel-era workarounds and they break Apple Silicon builds.