Sharing a React Native iOS Simulator Build — and the Firebase Keychain (-34018) Bug That Broke Remote Config
How to build and hand a React Native iOS app to a client for the Simulator, why an .ipa fails to install (send the .app), and the errSecMissingEntitlement -34018 keychain bug that silently breaks Firebase Remote Config on unsigned simulator builds.
A client needed to run our React Native app on their iOS Simulator — no device, no provisioning, no TestFlight invite. "Just send me something I can run." Simple, right?
That one request took me through four separate rabbit holes: an
.ipathat refused to install, a launch screen frozen on Firebase Remote Config, a Firebase backend that tested perfectly healthy, and finally a crypticSecItemCopyMatching (-34018)keychain error.This post is the full trail — every wrong turn, the script that evolved along the way, and the one-line cause that explained all of it. If you've read my iOS CI/CD → TestFlight guide, think of this as the "I just need a build on a simulator, today" companion.
🧩 Why a Simulator Build At All?
TestFlight is the right way to share a device build. But it has friction:
- The tester needs an Apple ID added to the project and a TestFlight invite.
- Every build goes through App Store Connect processing.
- You need a device.
My client didn't have any of that set up — they had a Mac with Xcode and the iOS Simulator. The fastest possible loop is: build for the simulator, hand them a file, they drag it in. No accounts, no processing, no cables.
So the goal became: produce a single artifact I can send over Slack that runs on their simulator.
⚙️ Attempt 1 — Build a Simulator .ipa
I started from the recipe everyone copies off the internet: build for iphonesimulator, then wrap the .app in a Payload/ folder and zip it into an .ipa.
cd ios
xcodebuild \
-workspace Myapp.xcworkspace \
-scheme Myapp \
-configuration Release \
-sdk iphonesimulator \
-derivedDataPath simulator-build \
CODE_SIGNING_ALLOWED=NO
cd simulator-build/Build/Products/Release-iphonesimulator
mkdir Payload
cp -R Myapp.app Payload/
zip -r Myapp-Simulator.ipa Payload
I turned that into a small, reusable script (ios/build-sim-ipa.sh) so I wasn't retyping it — auto-detecting the built .app name, cleaning up stale output, and cd-ing to its own directory so it runs from anywhere.
First run failed immediately — not the script's fault:
error: Unable to open base configuration reference file
'.../ios/Pods/Target Support Files/Pods-Myapp/Pods-Myapp.release.xcconfig'
CocoaPods wasn't installed. A pod install later, the build produced a shiny Myapp-Simulator.ipa. 🎉
Except it wasn't shiny at all.
🚧 Attempt 2 — The .ipa Won't Install
The client dragged Myapp-Simulator.ipa onto the simulator and… nothing installed. The simulator just offered to save the file into the Files app.
Here's the thing nobody mentions: the iOS Simulator installs a .app bundle, not an .ipa. An .ipa is a zip wrapper meant for real-device / App Store tooling (xcrun devicectl, Transporter, TestFlight). The Simulator doesn't unwrap it, so dragging it in is meaningless — it treats it as a generic document.
What actually installs is the .app:
# Drag Myapp.app onto a booted simulator, or:
xcrun simctl install booted Myapp.app
The catch: a .app is a folder (a bundle macOS shows as one icon). You can't email a folder. So the correct thing to send is a zip of the .app — the recipient unzips and drags the .app in.
I updated the script to stop making a useless .ipa and instead zip the bundle:
# The Simulator installs a .app (drag-and-drop or `simctl install`), NOT an .ipa.
# Ship a zip of the .app itself; the recipient unzips and drags Myapp.app in.
zip -qry "Myapp-Simulator-app.zip" "Myapp.app"
Client unzips, drags in Myapp.app, it installs. Progress! Then it opened… and froze.
🧊 Attempt 3 — Stuck on the Remote Config Loading Screen
The app booted to a spinner and never moved. The launch gate looked like this:
if (!firebaseRemoteConfigLoaded || !introLoaded) {
return <ActivityIndicator size="large" color={colors.primary} />;
}
firebaseRemoteConfigLoaded was flipped to true only on the success path of the bootstrap:
getappSetting: async () => {
set({ loading: true, firebaseRemoteConfigLoaded: false });
try {
await remoteConfig().setDefaults({ /* ... */ });
await remoteConfig().setConfigSettings({ /* ... */ });
const fetched = await remoteConfig().fetchAndActivate();
// ...read params, compute version...
set({ isUpdateAvailable: /* ... */, firebaseRemoteConfigLoaded: true });
} catch (err) {
console.log('getappSetting error:', err); // ← only logs, never releases the gate
}
set({ loading: false });
},
See it? If fetchAndActivate() throws, the catch logs the error and sets loading: false, but never sets firebaseRemoteConfigLoaded: true. The gate stays closed forever. Any hiccup in Remote Config = permanent spinner.
Interestingly, the sibling intro store already did this correctly — its catch sets loaded: true with a comment: "On failure we still mark loaded so the UI doesn't hang." The settings store just never got the same treatment.
The fix is to fail open — release the gate once the attempt finishes, no matter the outcome. Remote Config already has setDefaults, so falling back to defaults is exactly right:
} catch (err) {
console.log('getappSetting error:', err);
} finally {
// Always release the launch gate once the attempt finishes. Remote Config has
// defaults, so a fetch failure must fall back to them rather than hang forever.
set({ loading: false, firebaseRemoteConfigLoaded: true });
}
Rebuilt, and the app sailed past the spinner to the login screen. But that only stopped the hang — it didn't explain why Remote Config was failing in the first place. The client (rightly) said: "Remote Config should be working fine — find out why it isn't responding."
So I kept digging.
🔬 Attempt 4 — Is Remote Config Actually Broken?
First rule of debugging a "the backend is down" report: check the backend yourself. Remote Config needs a Firebase Installations token before it can fetch, so I tested both REST endpoints directly with the app's real credentials (the API key and app ID from GoogleService-Info.plist — both ship inside the app anyway, so they're not secrets).
1) Firebase Installations — create an installation:
curl -s -X POST \
"https://firebaseinstallations.googleapis.com/v1/projects/myapp/installations" \
-H "Content-Type: application/json" \
-H "X-Goog-Api-Key: <API_KEY>" \
-H "X-Ios-Bundle-Identifier: com.myapp.app" \
-d '{"appId":"<APP_ID>","authVersion":"FIS_v2","sdkVersion":"i:10.29.0"}'
# → HTTP 200, returns fid + authToken ✅
2) Remote Config — fetch using that token:
curl -s -X POST \
"https://firebaseremoteconfig.googleapis.com/v1/projects/<PROJECT_NUM>/namespaces/firebase:fetch?key=<API_KEY>" \
-H "Content-Type: application/json" \
-H "X-Goog-Firebase-Installations-Auth: <authToken>" \
-d '{"appInstanceId":"<fid>","appId":"<APP_ID>", ... }'
# → HTTP 200
# { "entries": { "LATEST_VERSION_IOS": "0.0.1", "UNDER_MAINTENANCE": "false", ... }, "state": "UPDATE" }
Both 200. The project, API key, and Remote Config template were all perfectly healthy. The problem was inside the app, not Firebase.
The frustrating part: a Release build hides console.log, so I couldn't read the JS error. So I ran a Debug build via npx react-native run-ios and instrumented the store with a temporary on-screen alert (Fast Refresh made this instant):
const params = remoteConfig().getAll();
Alert.alert('RC OK @ post-fetch', JSON.stringify(Object.keys(params))); // TEMP
Result: "RC OK @ post-fetch" with all three keys. In the Debug build, Remote Config worked flawlessly. 🤔
So I rebuilt the Release simulator app with the same alert and relaunched. This time:
RC FAIL @ fetchAndActivate
[remoteConfig/unknown] Failed to get installations token. Error Domain=com.firebase.installations Code=0 "Underlying error: The operation couldn't be completed. SecItemCopyMatching (-34018)" ... Error Domain=com.gul.keychain.ErrorDomain ... "SecItemCopyMatching (-34018)"
There it was.
🔑 The Root Cause: -34018 (errSecMissingEntitlement)
SecItemCopyMatching (-34018) is errSecMissingEntitlement — the app tried to read the Keychain and iOS refused because it has no keychain entitlement.
The dependency chain that explains everything:
- Remote Config needs a Firebase Installations token.
- Installations stores that token in the iOS Keychain.
- Keychain access needs the app to have an
application-identifier(its default access group). application-identifieronly exists when the app carries a code signature.- My simulator build used
CODE_SIGNING_ALLOWED=NO→ completely unsigned → noapplication-identifier→ keychain returns-34018→ Installations fails →fetchAndActivate()rejects → (before the fail-open fix) the app hangs forever.
That's why the Debug build worked: react-native run-ios ad-hoc signs it. My hand-rolled Release build didn't sign at all.
The surprising bit: it's not about entitlements content
I assumed I'd need a keychain-access-groups entitlement. I checked both builds:
codesign -d --entitlements :- Myapp.app
# Debug (works): <dict></dict> ← empty!
# Signed Release: <dict></dict> ← empty!
Both have empty entitlements. The differentiator isn't what's in the entitlements — it's simply whether the app is signed at all. An ad-hoc signature (codesign with -) is enough to give the app an identity and a default keychain access group on the simulator. No signature = no identity = -34018.
| Build | Signature | Keychain | Remote Config |
|---|---|---|---|
Debug (run-ios) | ad-hoc ✅ | works | OK |
Release .ipa (CODE_SIGNING_ALLOWED=NO) | none ❌ | -34018 | fails |
| Release, ad-hoc signed | ad-hoc ✅ | works | OK |
✅ The Fix: Sign the Simulator Build (Ad-Hoc)
Stop disabling code signing. Let Xcode ad-hoc sign the simulator build — "sign to run locally" — which is all it takes:
xcodebuild \
-workspace Myapp.xcworkspace \
-scheme Myapp \
-configuration Release \
-sdk iphonesimulator \
-derivedDataPath simulator-build \
CODE_SIGN_STYLE=Automatic \
DEVELOPMENT_TEAM=<YOUR_TEAM_ID> \
-allowProvisioningUpdates
For a simulator destination this doesn't need a provisioning profile or network round-trip — it resolves to an ad-hoc signature with injected base entitlements. Rebuild, reinstall, relaunch:
RC OK (signed) —
["UNDER_MAINTENANCE","LATEST_VERSION_ANDROID","LATEST_VERSION_IOS"]
Remote Config responds. The client gets a build that behaves exactly like one launched from Xcode.
⚠️ This bites more than Remote Config. Firebase Auth and anything using secure token storage also live in the Keychain. An unsigned simulator build silently breaks all of them with the same
-34018. If your app "works from Xcode but not from the shared build," suspect signing first.
🎯 Result
The final ios/build-sim-ipa.sh now:
- Builds
Myappfor the simulator ad-hoc signed (not unsigned). - Finds the built
.appwithout hardcoding its name. - Zips the
.app(never an.ipa) intoMyapp-Simulator-app.zip. - Prints the exact drag-in /
simctl installinstructions.
The client unzips one file, drags Myapp.app onto their simulator, and everything — Remote Config, Auth, the lot — just works.
🧠 Key Lessons
- The Simulator installs
.app, not.ipa. Ship a zip of the.app; an.ipaonly "saves to Files." - Never leave a launch gate fail-closed. If a bootstrap step can throw, release the gate in
finallyand fall back to defaults — never hang the UI on a network hiccup. - Test the backend from outside the app first. Two
curls proved Firebase was healthy and pointed me inward, saving hours. - Debug vs Release differ in more than logs. Release hides
console.log; a temporary on-screenAlert+ Fast Refresh is a great way to see errors in a release-like build. -34018=errSecMissingEntitlement= keychain access denied. On the simulator it almost always means the app is unsigned.- Ad-hoc signing is enough for the simulator. It's not about entitlement contents (both builds were empty) — it's about having a signature at all.
- Firebase Installations underpins Remote Config, Auth, Messaging. Break the keychain and you break all of them at once.
🏁 Closing Thoughts
What looked like "Remote Config is down" was really "your simulator build has no code signature." The fail-open fix stopped the hang; the ad-hoc signing fix restored the functionality. Two different bugs, one innocent request to "just send me a build."
The satisfying part is that the final workflow is boringly simple: run one script, send one zip, drag in one .app. All the sharp edges are now baked into the script and its comments so the next person (probably future me) doesn't rediscover them the hard way.
Written by @iamhusnain
— if you found this helpful, here's the device-build companion:
React Native iOS CI/CD with GitHub Actions + Fastlane → TestFlight .
Firebase acting up in your React Native app? See Firebase development or hire a React Native developer.
Let's bring your app idea to life
React Native apps for iOS & Android, from first commit to the store.
Share this article
Related Articles
Custom Fonts in React Native WebView: The Complete Fix
Struggling with custom fonts not rendering inside a React Native WebView? Learn why it happens and how to properly inject fonts using platform-specific asset paths and base64 embedding.
Debugging React Native Apps: Tools, Techniques, and Production Troubleshooting
Master debugging React Native applications. Learn to use React Native Debugger, Flipper, breakpoint debugging, network inspection, performance profiling, memory leak detection, and solve production issues effectively.
My Battle With Jest Mocks and Firebase Auth
Mocking @react-native-firebase/auth in Jest should be simple — until it isn’t. Here’s the real-world story of how I struggled with a mysterious jest.fn() bug, what I learned about shared references, and the best practices that finally fixed my test suite.