Rork Max + EAS Build Custom Native Module Errors: Complete Fix Guide
Rork Max works great for the vast majority of apps — but when you need something outside the Expo SDK (Bluetooth hardware, custom SDKs, native camera libraries), you're in EAS Build territory with custom native modules.
The first time through, it's a maze of Podfile conflicts, Execution failed for task ':app:merge...', and "the build passes but crashes immediately at launch." This guide maps out all the major failure modes with concrete fixes.
The Foundation: How EAS Build and Config Plugins Work
To add native code to a Managed Workflow Expo project (what Rork Max uses internally), you need Config Plugins. These are Node.js scripts that modify the native iOS and Android project files during npx expo prebuild — which EAS Build runs automatically before compiling.
Many libraries ship their own Config Plugin (like @config-plugins/react-native-ble-plx). For libraries that don't, you'll need to write one.
app.config.js plugins array
↓
npx expo prebuild (runs automatically in EAS Build)
↓
ios/ and android/ directories are generated
↓
iOS: pod install → Xcode build
Android: Gradle build
↓
Binary output
Error #1: Podfile Version Conflicts
Symptom: EAS Build fails with:
[!] CocoaPods could not find compatible versions for pod "SomeLibrary":
In snapshot (Podfile.lock):
SomeLibrary (= 1.2.0)
In Podfile:
SomeLibrary (~> 2.0)
// config-plugins/force-pod-version.js
const { withPodfile } = require('@expo/config-plugins');
module.exports = (config) => {
return withPodfile(config, (modConfig) => {
const { modResults } = modConfig;
modResults.contents = modResults.contents.replace(
/# Add any custom podfile content here/,
`# Add any custom podfile content here
# Force version to resolve dependency conflict
pod 'SomeLibrary', '~> 2.0.0'`
);
return modConfig;
});
};// app.config.js
export default {
expo: {
plugins: ['./config-plugins/force-pod-version']
}
};Resetting the Podfile.lock:
npx expo prebuild --clean # regenerate ios/ and android/ from scratch
cd ios && rm Podfile.lock # remove the lock file
pod install --repo-update # reinstall with updated pod specs
cd ..Error #2: Config Plugin Not Applied — Module Crashes at Runtime
Symptom: Build succeeds, but app crashes with Native module cannot be null or Cannot read property of undefined.
This means the Config Plugin didn't write the native initialization code into the project. Verify your app.config.js plugins array:
export default {
expo: {
plugins: [
// Wrong: library may not have a config plugin under this name
'react-native-ble-plx',
// Correct: check for @config-plugins namespace
'@config-plugins/react-native-ble-plx',
// Correct with options: pass config as array second element
['@config-plugins/react-native-ble-plx', {
bluetoothAlwaysPermission: "$(PRODUCT_NAME) uses Bluetooth to connect to devices",
bluetoothPeripheralPermission: "$(PRODUCT_NAME) uses Bluetooth to connect to peripheral devices"
}],
]
}
};Verify Config Plugin application before building:
# Run prebuild to generate native files locally
npx expo prebuild --clean
# Check if iOS permissions were added
cat ios/MyApp/Info.plist | grep -A 2 "NSBluetooth"
# Check if Android permissions were added
cat android/app/src/main/AndroidManifest.xml | grep "BLUETOOTH"
# If these entries are missing, the Config Plugin didn't apply correctlyError #3: Android Gradle Build Failures
Symptom: iOS builds fine but Android fails with errors like Execution failed for task ':app:mergeReleaseJavaResource' or Manifest merger failed.
// config-plugins/fix-android-build.js
const { withAppBuildGradle, withProjectBuildGradle } = require('@expo/config-plugins');
// Fix minSdkVersion when a library requires a higher minimum
const fixMinSdkVersion = (config) => {
return withAppBuildGradle(config, (modConfig) => {
modConfig.modResults.contents = modConfig.modResults.contents.replace(
/minSdkVersion = \d+/,
'minSdkVersion = 24'
);
return modConfig;
});
};
// Fix Kotlin version conflicts
const fixKotlinVersion = (config) => {
return withProjectBuildGradle(config, (modConfig) => {
modConfig.modResults.contents = modConfig.modResults.contents.replace(
/kotlinVersion = '.*'/,
"kotlinVersion = '1.9.0'"
);
return modConfig;
});
};
module.exports = (config) => {
config = fixMinSdkVersion(config);
config = fixKotlinVersion(config);
return config;
};// app.config.js
export default {
expo: {
plugins: ['./config-plugins/fix-android-build']
}
};Error #4: Hermes and New Architecture Incompatibility
Symptom: Module works in Expo Go (uses JSC) but crashes with Hermes, or Calling synchronous methods on native modules is not supported.
// app.config.js — control Hermes and New Architecture settings
export default {
expo: {
jsEngine: "hermes", // default, keep this
// If a module is incompatible with New Architecture, disable it here
// (this is a temporary measure — prefer updating the library)
newArchEnabled: false,
ios: {
newArchEnabled: false,
},
android: {
newArchEnabled: false,
}
}
};To check if a specific library supports Hermes and New Architecture, look for a react-native.config.js or check the library's GitHub issues for "Hermes" or "New Architecture" mentions.
Error #5: Missing Native Initialization Code
Symptom: Module is installed and Config Plugin exists, but at runtime you still get "module not found" errors.
Older React Native libraries that predate Config Plugins sometimes require manual entries in MainApplication.java (Android) and AppDelegate.mm (iOS). Config Plugins let you automate this:
// config-plugins/add-native-init.js
const { withMainApplication, withAppDelegate } = require('@expo/config-plugins');
// Android: add module to MainApplication.java
const withAndroidModule = (config) => {
return withMainApplication(config, (modConfig) => {
const { modResults } = modConfig;
if (!modResults.contents.includes('import com.example.CustomModule;')) {
// Add import
modResults.contents = modResults.contents.replace(
'import com.facebook.react.ReactApplication;',
`import com.facebook.react.ReactApplication;
import com.example.CustomModule;`
);
// Add to packages list
modResults.contents = modResults.contents.replace(
'packages.add(new MainReactPackage());',
`packages.add(new MainReactPackage());
packages.add(new CustomModule());`
);
}
return modConfig;
});
};
// iOS: add initialization to AppDelegate.mm
const withIOSInit = (config) => {
return withAppDelegate(config, (modConfig) => {
const { modResults } = modConfig;
if (!modResults.contents.includes('#import <CustomSDK/CustomSDK.h>')) {
modResults.contents = modResults.contents.replace(
'#import <React/RCTBridge.h>',
`#import <React/RCTBridge.h>
#import <CustomSDK/CustomSDK.h>`
);
}
return modConfig;
});
};
module.exports = (config) => {
config = withAndroidModule(config);
config = withIOSInit(config);
return config;
};Efficient Debug Strategy for EAS Build
EAS Build cycles are expensive (10–15 minutes per build). Minimize them with local verification first:
# Step 1: Verify prebuild output before EAS Build
npx expo prebuild --clean
# Inspect ios/ and android/ — check all expected changes are present
# Step 2: Test a local release build (requires macOS + Xcode for iOS)
npx expo run:ios --configuration Release
# or
npx expo run:android --variant release
# Step 3: Only after local validation, submit to EAS Build
eas build --profile preview --platform ios
# View build logs
eas build:list
eas build:view [BUILD_ID]Multi-profile eas.json design:
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"ios": { "buildConfiguration": "Debug" }
},
"preview": {
"distribution": "internal",
"ios": { "buildConfiguration": "Release" },
"env": { "API_URL": "https://staging.example.com" },
"cache": {
"key": "preview-cache-v1"
}
},
"production": {
"distribution": "store",
"ios": { "buildConfiguration": "Release" },
"env": { "API_URL": "https://api.example.com" }
}
}
}Managing secrets securely:
# Store sensitive values as EAS Secrets (never commit these to git)
eas secret:create --scope project --name STRIPE_SECRET_KEY --value "sk_live_xxxxx"
eas secret:create --scope project --name FIREBASE_SERVICE_ACCOUNT --value "$(cat service-account.json)"
# List configured secrets
eas secret:listLooking back
EAS Build custom native module errors fall into five main patterns:
- Podfile conflicts → Use
withPodfileConfig Plugin to pin conflicting versions - Config Plugin not applied → Verify with
npx expo prebuild --clean, check@config-plugins/namespace - Android Gradle errors → Use
withAppBuildGradle/withProjectBuildGradleto patchminSdkVersionand Kotlin version - Hermes incompatibility → Set
newArchEnabled: falseas a temporary workaround while waiting for library updates - Missing native initialization → Write custom
withMainApplication/withAppDelegateplugins
The core diagnostic practice is: always run npx expo prebuild --clean and inspect the generated native files before submitting to EAS Build. Most issues are visible in the generated files, saving you multiple 15-minute build cycles.