Expo iOS Build Errors: prebuild, Config Plugins and Expo Pods
Expo iOS build errors often mean the ios folder was edited by hand, but prebuild regenerates it from app.json and config plugins. 12 fixes for SDK 58.
Expo iOS build errors usually come from one thing. The ios folder is generated by npx expo prebuild from app.json and config plugins, so manual edits get overwritten by the next prebuild --clean. The fix is to put usage descriptions, entitlements and the Bundle ID in your app config, then regenerate, run pod install again and do a full build. This post explains the CNG rules first, then walks through 12 common problems as symptom, cause and fix, and ends with what to know when building Expo with KXBuild. Version details follow the Expo docs. As of October 2026, the latest is SDK 58.
Which Expo SDK version are you on?
Look at the expo version in package.json. Here are the last three SDKs, from Expo’s version table.
| Expo SDK | Released | React Native | Minimum Node.js |
|---|---|---|---|
| 58 | October 2026 | 0.88 | 22.13 |
| 57 | June 2026 | 0.86 | 22.13 |
| 56 | May 2026 | 0.85 | 20.19 |
SDK 58 requires iOS 16.4 or later. Expo is React Native underneath, so issues with Pods, Hermes and the New Architecture are the same as in a plain RN project. See React Native New Architecture iOS issues. This post covers only what’s specific to Expo.
What are the CNG rules in Expo?
Expo calls this approach Continuous Native Generation (CNG). You maintain the definition of your native configuration, which is app.json (or app.config.js) plus config plugins, and npx expo prebuild generates the native project when you need it. Keep four rules in mind.
- The ios folder is build output. New Expo projects add ios and android to
.gitignoreautomatically. - Manual edits get lost. Edit Info.plist, AppDelegate or the Podfile inside ios, and the next
prebuild --cleanwipes them. - Native settings go in the app config. Usage descriptions, entitlements, the Bundle ID and the deployment target all have fields. For anything the fields don’t cover, use a config plugin.
- Regenerate after changes. Config plugins run only during prebuild. If you change app.json and don’t regenerate, nothing changes in the app.
| What you want to change | Where it goes |
|---|---|
| Bundle ID, version, build number | ios.bundleIdentifier, version, ios.buildNumber |
| Usage descriptions and other Info.plist keys | ios.infoPlist, or the library’s config plugin options |
| Capabilities (push, associated domains, App Groups and so on) | ios.entitlements, ios.associatedDomains |
| Minimum iOS version | ios.deploymentTarget (built in since SDK 56) |
| Privacy manifest | ios.privacyManifests |
| use_frameworks and extra Pods in the Podfile | The expo-build-properties plugin |
| Native changes none of the above cover | Write your own config plugin |
What causes most Expo iOS build errors?
| # | Symptom | Cause | Fix |
|---|---|---|---|
| 1 | You changed app.json but the app didn’t change | The ios folder wasn’t regenerated | prebuild --clean, then pod install and build again |
| 2 | Your manual native changes are gone | Overwritten by prebuild --clean |
Put the changes in the app config or a config plugin |
| 3 | A config plugin has no effect | Not listed in plugins, not regenerated, or app.config.js dropped the config | Check the resolved config with npx expo config |
| 4 | A usage description is missing from Info.plist and the app crashes | Wrong nesting level, or a library’s plugin option overrode it | Put it under expo.ios.infoPlist |
| 5 | Push, associated domains or another capability doesn’t work, signing reports an entitlements mismatch | Entitlements missing, or the profile doesn’t have the capability | ios.entitlements plus a regenerated provisioning profile |
| 6 | Expo Go reports Cannot find native module |
Expo Go doesn’t include the native library you installed | Use a development build with expo-dev-client |
| 7 | pod install fails | JS dependencies not installed, deployment target too low, leftover Pods | Run npm install first, check the deployment target, delete Pods and reinstall |
| 8 | The library is installed but its native part isn’t linked | The library isn’t in the autolinking results | Investigate with the expo-modules-autolinking commands |
| 9 | The Release build shows a blank screen or crashes | The JS bundle wasn’t included, or environment variables are missing | Check the bundling step in the build log and your .env |
| 10 | Signing says the Bundle ID doesn’t match the profile | The Bundle ID in the app config differs from the profile’s App ID | Use one Bundle ID and regenerate |
| 11 | Settings in eas.json don’t apply to local builds | Those settings are only for EAS | Use .env or shell variables locally |
| 12 | Lots of compile errors after upgrading the SDK | The ios folder still uses the old template | npx expo install --fix, then prebuild --clean |
How do you fix prebuild and config plugin problems?
1. You changed app.json but the app didn’t change
app.json is only the definition. What ends up in the app is the Info.plist, project file and Podfile generated in the ios folder. Regenerate whenever you change the Bundle ID, usage descriptions, icons or plugin options, or install a library with native code.
1npx expo prebuild --clean --platform ios
--clean deletes the existing native folder before generating. The Expo docs call this the safest way to run it. Without --clean, changes are layered on top of existing files, and some config plugins give inconsistent results when they run twice. After generating, run pod install again and do a full build, not just a JS refresh.
2. Your manual native changes are gone
Swift files you added under ios, and changes to AppDelegate or the Podfile, all disappear after --clean. If git has uncommitted changes, prebuild warns you first. Don’t just skip that warning.
Pick one of two ways to maintain the project and don’t mix them.
- Follow CNG. Keep the ios folder out of version control and put every native change in the app config or a config plugin.
- Maintain the ios folder yourself. Commit ios to version control, edit native files directly from then on, and stop running
prebuild --clean. Native fields in app.json no longer sync to the ios folder, so make those changes in the native files.
3. A config plugin has no effect
Config plugins go in the plugins array of the app config. Without options, list just the package name. With options, use a two-element array of package name and options object.
1{
2 "expo": {
3 "plugins": [
4 "expo-dev-client",
5 [
6 "expo-build-properties",
7 {
8 "ios": { "useFrameworks": "static" }
9 }
10 ]
11 ]
12 }
13}
If a plugin doesn’t take effect, check these four things in order.
- Did you regenerate? Plugin changes run only during the prebuild sync step. See item 1.
- Is it in plugins? Running npm install on a library doesn’t add its plugin to plugins. Add it as the library’s docs describe.
- Did app.config.js drop the contents of app.json? Expo reads app.json first and passes the result to app.config.js. Write dynamic config as
({ config }) => ({ ...config, ... })and modify the config you’re given. If you return a brand-new object, the plugins from app.json are lost. When both app.config.ts and app.config.js exist, the ts file is used. - Is the plugin version right? Install with
npx expo install <package>, which picks the version that matches your SDK.
Run the command below to see the final config as resolved by the Expo CLI, and confirm the plugin and fields are actually there.
1npx expo config
4. A usage description is missing from Info.plist
Usage descriptions go under expo.ios.infoPlist, with key names exactly as in Info.plist.
1{
2 "expo": {
3 "ios": {
4 "infoPlist": {
5 "NSCameraUsageDescription": "Used to scan the barcode on your delivery label",
6 "NSLocationWhenInUseUsageDescription": "Used to show the stores closest to you"
7 }
8 }
9 }
10}
The common mistakes are putting infoPlist directly under expo instead of inside ios, or misspelling a key. Many Expo libraries also write usage descriptions through their config plugins, with generic default text. The Expo docs point out that most of these defaults should be rewritten for your use case, which you do through plugin options, for example photosPermission for expo-media-library.
If the same key is set in both places, the generated Info.plist is what counts, so open ios/<App>/Info.plist after generating and check it.
Without a usage description, the system terminates the app when it accesses that data, and uploads get ITMS-90683. For how to write the text, see Info.plist usage descriptions.
5. Entitlements and capabilities
Capabilities such as push, App Groups, iCloud and Keychain sharing need two things. The app’s entitlements must contain the key, and the App ID behind the provisioning profile must have the capability enabled.
1{
2 "expo": {
3 "ios": {
4 "entitlements": {
5 "aps-environment": "development",
6 "com.apple.security.application-groups": ["group.com.example.myapp"]
7 },
8 "associatedDomains": ["applinks:www.example.com"]
9 }
10 }
11}
Many libraries, for push or Sign in with Apple for example, add entitlements through their own config plugins, so you only need to add the plugin to plugins. Regenerate after adding it, then enable the same capability for the App ID in the developer portal and regenerate and download the provisioning profile. If the profile doesn’t have a capability, signing reports that the entitlements don’t match the profile. For how capabilities relate to profiles, see How Bundle IDs, App IDs and provisioning profiles fit together.
Why use a development build instead of Expo Go?
6. Expo Go can’t find a native module
1Error: Cannot find native module 'ExpoXxx'
Expo Go is a general-purpose app with a fixed set of native libraries. If the library you installed isn’t in it, or you changed the native config, your project won’t run in Expo Go. In Expo’s words, a development build is your own version of Expo Go, which can use any native library and any native config.
Install expo-dev-client in the project, then build a development version yourself and install it on your phone.
1npx expo install expo-dev-client
2npx expo start
With expo-dev-client installed, npx expo start targets your development build by default instead of Expo Go.
| Expo Go | Development build | |
|---|---|---|
| Native libraries | Only those bundled in Expo Go | Everything installed in your project |
| Change native config | No | Yes, rebuild after changing |
| How you get it | Download from the App Store | Build, sign and install it yourself |
| Good for | Learning, JS-only prototypes | Real projects |
How do you debug pod install and autolinking failures?
7. pod install fails
The Podfile Expo generates calls node to find Expo’s autolinking script in node_modules. Handle the errors in three groups.
Cannot find module, or the autolinking script fails to load. JS dependencies aren’t installed yet. Runnpm installin the project root first, then pod install inside ios.required a higher minimum deployment target. A library needs a newer iOS version than the project. Raiseios.deploymentTargetin the app config, regenerate, then pod install. On SDK 55 and earlier, useios.deploymentTargetinexpo-build-properties.- Version conflicts after upgrading or switching branches. Delete the ios folder and regenerate, which also clears the old Pods and Podfile.lock.
For Pods problems shared with React Native, such as missing Codegen output or use_frameworks conflicts, see React Native New Architecture iOS issues.
8. The library is installed but its native part isn’t linked
Expo uses expo-modules-autolinking to link libraries’ native code during pod install, and since SDK 52 it also replaces the React Native community CLI autolinking. If a library isn’t linked, you also get a missing native module error at runtime.
Run these commands in the project root to investigate.
1npx expo-modules-autolinking search
2npx expo-modules-autolinking resolve --platform apple
3npx expo-modules-autolinking verify
search lists the Expo modules it found, resolve shows the podspec path each module uses, and verify checks for duplicate native modules.
According to the Expo docs, a package is linked only if its root has an expo-module.config.json whose platforms includes the platform. In a monorepo, add the folder of any local module outside node_modules to expo.autolinking.searchPaths in package.json. npx expo-doctor also lists duplicate native modules and where they’re installed.
How do you fix Release, signing and EAS errors?
9. The Release build shows a blank screen or crashes
A Release build doesn’t connect to Metro, so the JS has to be bundled into the app at build time. A blank screen usually means that step failed. In the build log, find the first error in the Bundle React Native code and images phase. Causes such as node not being found or SKIP_BUNDLING being set are the same as in RN. See React Native New Architecture iOS issues.
Expo projects have one more common cause, environment variables. When the Expo CLI bundles, it replaces process.env.EXPO_PUBLIC_XXX in your code with values from the .env file. If the build machine has no .env, those values are undefined, the API URL is empty and the app fails on launch.
Make sure the build machine has the right .env. Since SDK 58, custom-named files such as .env.staging are no longer loaded, only the standard .env files. Also keep secrets out of EXPO_PUBLIC_ variables, because they’re embedded in plain text in the JS bundle.
10. The Bundle ID doesn’t match the provisioning profile
If signing can’t find a matching profile, or the profile’s App ID doesn’t match the app, compare three places.
ios.bundleIdentifierin the app config.PRODUCT_BUNDLE_IDENTIFIERin the generated ios project. If you haven’t regenerated, it’s still the old value.- The App ID of the provisioning profile.
Be careful in projects that switch the Bundle ID per environment in app.config.js. Expo’s app variants guide uses an APP_VARIANT environment variable to tell development and production apart, for example com.example.myapp.dev for development. That variable is set in eas.json, so a local build doesn’t have it and generates a different Bundle ID. Set the same variable in your terminal before generating, then regenerate.
For general signing troubleshooting, see 16 common iOS code signing and .ipa install errors, and how to fix them.
11. EAS-only settings don’t apply to local builds
These settings only work on EAS Build. Local builds can’t see them.
| Setting | On EAS | For local builds |
|---|---|---|
env in an eas.json build profile |
Injected at build time | Set variables with the same names in your terminal, or put them in .env |
| Environment variables stored on EAS | Available at build time | Depending on visibility, pull them locally with eas env:pull |
| Certificates and profiles managed by EAS | Signed automatically | Provide your own .p12 and .mobileprovision |
| Automatic prebuild when there’s no ios folder | Generated before every build | Generate it yourself, see item 1 |
One more point that’s easy to mix up. According to the Expo docs, EAS Build doesn’t run prebuild when the project already has an ios folder, so it won’t overwrite your changes. Once you commit the ios folder, EAS stops syncing native fields from app.json too.
12. Compile errors after upgrading the SDK
To upgrade, update the expo package first, let Expo align the other dependencies to that SDK, then regenerate from the new template.
1npm install expo@^58.0.0
2npx expo install --fix
3npx expo prebuild --clean --platform ios
--clean generates the ios folder from the new SDK’s template, so differences left over in AppDelegate and the Podfile from the old template go away. Projects that maintain the ios folder themselves can’t do this, and have to update native files by hand following Expo’s upgrade notes. After upgrading, run npx expo-doctor to check dependency versions and duplicate modules.
How do you build the iOS side of Expo with KXBuild?
When you build the iOS side of an Expo app with KXBuild on Windows or Linux, keep four points in mind.
- Run doctor first. Run
kxbuild doctor D:\work\expohellowith the project path, or run KXBuild: Doctor (fix environment) from the KXBuild view. It checks the Expo environment and generates the ios folder. Then runpod installin the ios folder and build the.xcworkspace. - Regenerate. Doctor generates the ios folder only when it doesn’t exist. After changing app.json or installing a new native library, delete the ios folder, run doctor again, then pod install.
- Debug. Run
npx expo startin another terminal, select your device and press F5. Set breakpoints in native code in VS Code, and debug JS with Expo’s own debugger. - Build an .ipa. Run
kxbuild build .\ios\expohello.xcworkspace -c Release --export app-store, with your certificate and provisioning profile in thekeychainfolder.
For the full Expo iOS build steps, see Build Expo iOS apps locally on Windows without EAS and the Expo stack page. If doctor fails, add -v to see the full log, and see Troubleshooting.
FAQ
Can I edit the ios folder in an Expo project by hand?
You can, but in a project that follows CNG, the next prebuild –clean overwrites your edits. Put native settings you want to keep in the ios fields of app.json or in a config plugin. If you really want to maintain the ios folder yourself, commit it and stop running prebuild –clean.
Why don’t my app.json changes show up on iOS?
app.json reaches the ios project only through prebuild. Run npx expo prebuild –clean –platform ios to regenerate, then pod install and do a full build. In KXBuild, delete the ios folder and run doctor again.
What’s the difference between Expo Go and expo-dev-client?
Expo Go is a general-purpose app with a fixed set of native libraries, and you can’t add your own. A development build you make with expo-dev-client is your own Expo Go, and it can use every native library and native setting in your project.
How do I debug a config plugin that doesn’t work?
Run npx expo config to check that the plugin is in the final config, then confirm you ran prebuild again after the change. With app.config.js, modify and return the config you’re given, or the plugins from app.json are lost.
What’s the minimum iOS version for Expo SDK 58?
According to Expo’s version table, SDK 58 requires iOS 16.4 or later and uses React Native 0.88. If a library needs a newer version, raise ios.deploymentTarget in the app config, regenerate, then run pod install.