Brownfield Troubleshooting
Cross-cutting issues that apply to both the isolated and integrated approaches. For approach-specific setup, see ./brownfield-isolated.md or ./brownfield-integrated.md.
Build failures
Symptom: Gradle or Xcode build fails after a config change, dependency upgrade, or Expo SDK bump.
First inspect the failing build step and the dependency/configuration diff.
- Integrated approach: keep the hand-maintained host intact. Run
npx expo install --checkin the JS project, apply SDK-matched native template changes selectively, then runbundle exec pod install(orpod installwithout Bundler) in the host's Podfile directory. Open the.xcworkspace. Neitherprebuildnorprebuild --cleanis a recovery step for this host: clean prebuild deletes native directories. - Isolated approach: rebuild the affected platform in the separate Expo producer, then replace the consumer's artifact and its accompanying dependencies. CNG regeneration belongs only in that producer, after checking that its native files are generated and reproducible.
- For stale iOS build products, clean the affected target's build folder/DerivedData. Preserve
Podfile.lock; deleting it changes dependency resolution and can hide the cause. Reinstall pods only when the error points to the pod installation. - For Android, clean the affected project's build outputs with its Gradle wrapper. Inspect publication coordinates and dependency resolution before removing a specific stale local Maven artifact; do not clear unrelated caches.
Missing autolinked Expo modules
Symptom: Compilation succeeds but a module throws "Native module cannot be null" / "Cannot find native module 'X'" at runtime.
- Install with
npx expo install <package>rather than plainyarn add—expo installpicks the version compatible with the current SDK. - After installing a new module, rebuild the native app. Autolinking runs at native build time, not at JS bundle time.
- For the isolated approach, you must re-run
npx expo-brownfield build:android|iosafter adding a module, and republish/re-embed the new artifact.
Metro connection
Symptom: "Could not connect to development server" / red screen on launch in debug.
- Ensure the device or emulator can reach the dev machine. The Android emulator can talk to the host via
10.0.2.2; physical devices need a reachable LAN IP. - For physical Android devices on USB:
adb reverse tcp:8081 tcp:8081. - Confirm Metro is actually running:
npx expo startfrom the Expo project (oryarn startfrom the workspace root). - Verify the debug
AndroidManifest.xmlenables cleartext traffic — Android 9+ blocks HTTP by default. The debug variant should includeandroid:usesCleartextTraffic="true"on<application>, or anetwork_security_configallowing the dev server. - iOS simulator: Metro should be reachable at
localhost:8081. If it is not, check that ATS exceptions are still in place inInfo.plistforlocalhost(the Expo template ships this by default).
iOS XCFramework signing (isolated approach)
Symptom: App launches but immediately crashes with "Library not loaded" or codesign errors during archive.
- Inspect the actual output and generated
Package.swift; the framework set depends on package version and source/prebuilt settings, not only the SDK major. Link all required binaries and embed/sign dynamic frameworks. Do not apply Embed & Sign to static binaries. See the artifact instructions. - The frameworks must be added to the app target, not a framework or extension target.
- With Swift Package output (
build:ios --package), inspect the manifest and link all required products. Precompiled builds on SDK 57 expose an aggregate product; other configurations and older packages can expose separate products. See version compatibility.
iOS architecture / simulator mismatch
Symptom: "Building for iOS Simulator, but the linked library was built for iOS" or "Undefined symbols for architecture arm64".
- The XCFramework includes both device and simulator slices. If a slice is missing, rebuild on the missing platform. The
expo-brownfield build:ioscommand produces both by default. - On Apple Silicon simulators, do not set
EXCLUDED_ARCHS = arm64for the simulator configuration — Apple Silicon simulators requirearm64. The classic Rosetta-only exclusion is no longer correct.
Android mavenLocal() not found (isolated approach)
Symptom: Gradle reports "Could not find com.example:mybrownfield:1.0.0" even after a successful expo-brownfield build:android.
mavenLocal()must be declared underdependencyResolutionManagement { repositories { ... } }insettings.gradle.kts, not the deprecated top-levelallprojects { repositories { ... } }block. The deprecated form is silently ignored whendependencyResolutionManagementis present.- Confirm the artifact actually landed in
~/.m2:find ~/.m2/repository -name "mybrownfield*" - Verify the
groupandlibraryNamein the consumer's dependency line match what the plugin config emitted.
Module name mismatch
Symptom: The native view loads but renders a blank screen, with "Application 'X' has not been registered" in the JS logs.
- The
moduleNamepassed toReactNativeViewController(moduleName: "main")(iOS) or returned fromgetMainComponentName()(Android) must equal the name passed toAppRegistry.registerComponent("main", () => App)in the JS entry point. - The default Expo template registers
"main". If you changed the registration, update every native call site.
Monorepo: autolinking can't find the Expo project
Symptom: Gradle or CocoaPods fails resolving Expo modules even though they are installed.
- Android (integrated): set
root = file("../../my-project")(or the correct relative path) inside thereact { ... }block inapp/build.gradle, and explicitly set the project root insettings.gradlebeforeexpoAutolinking.useExpoModules(). - iOS (integrated): set
:app_pathinuse_react_native!to the absolute path of the Expo project root. Optionally passEXPO_PROJECT_ROOT=/abs/pathtopod install. - Confirm
node_modules/is installed at the workspace root (yarn installfrom the monorepo root, not from the Expo project subdirectory).
After upgrading Expo SDK
First check the selected SDK's Node, Xcode, and minimum OS requirements in version compatibility. An unsupported compiler or older deployment target is not repaired by clearing caches. If the brownfield setup stops building after an SDK upgrade:
- Re-run
npx expo install --fixin the Expo project to align native module versions. - Isolated: regenerate only the CNG-owned producer if needed, rebuild its artifact, and update the host dependency. Integrated: apply native upgrade diffs to the existing host and reinstall pods; preserve its source files and project configuration.
- Compare the new
templates/expo-template-bare-minimumfor the target SDK against your customized native files — Expo occasionally changes Gradle plugin names, Podfile helpers, or AppDelegate entry points across SDKs.
Result missing, duplicate callbacks, or a sheet that will not close
- Check the module registration and per-presentation request ID. Root props need an explicit JS entry point; do not assume a Router route receives native
initialPropsdirectly. - Attach the host listener before mounting RN and supply required startup data through initial props. For live updates, verify subscription readiness and use acknowledgements where delivery matters; messages are not a durable queue.
- Remove only this feature's listeners on completion, cancellation, and host dismissal. Dispatch UI changes to the main thread.
popToNative()depends on the native wrapper: the SDK 55 UIKit controller pops navigation, and its SwiftUI wrapper separately callsdismiss(). Custom integrated containers need their own handler. Check which wrapper is actually mounted, or let the host close it on a result/close message. See feature integration.