You shipped a prototype with Rork, and then production goals start showing up: "I need the full Stripe Mobile SDK," "I need to talk to a custom BLE device," or "We need to integrate our internal SDK." That is the moment Expo Prebuild enters the picture.
This guide walks through how to safely add native code to a Rork-generated Expo Managed Workflow app using Prebuild. Drawing from the times I have crossed this boundary while shipping personal projects, I will also share the gotchas that tend to consume the most hours.
Recapping the Rork–Expo Relationship
Rork outputs an Expo Managed Workflow project — app.json and package.json at the center, and no ios/ or android/ directories. Native builds happen on EAS (Expo Application Services), which is wonderful for the first few weeks but becomes restrictive once you outgrow the expo- library catalog.
When that happens, you have three escape hatches:
- Stay declarative with a Config Plugin for cases where only native configuration changes (permission strings,
Info.plist,AndroidManifest) - Prebuild and add a third-party native module distributed through npm
- Prebuild and write your own native module in Swift or Kotlin
In my experience, jumping straight to native code is rarely the fastest path. Confirming whether a Config Plugin would already be enough almost always pays off.
Things to Check Before Reaching for Prebuild
Once you commit to native code, going back is possible but expensive in maintenance time. Walk through this short checklist first:
- Is the feature already covered by an Expo first-party library (
expo-camera,expo-notifications, etc.)? - Is there a third-party library shipped through the Expo Modules ecosystem that solves it?
- If the change is only native configuration, can a Config Plugin do the job?
- Is native code truly required, or can a different architecture (WebView, server-side processing) deliver the same outcome?
When the honest answer is "yes, native is required," that is when Prebuild becomes the right tool.
Core Prebuild Commands
Prebuild generates ios/ and android/ directories from app.json. Rather than a one-time eject, it embraces "Continuous Native Generation" — your native projects are reproduced from configuration on every run.
# 1. Snapshot the project (important)
git add . && git commit -m "Snapshot before prebuild"
# 2. Align SDK versions if needed
npx expo install --check
# 3. Run prebuild (clean rebuild recommended)
npx expo prebuild --clean
# 4. Install iOS dependencies
cd ios && pod install && cd ..
# 5. Verify locally
npx expo run:ios # or npx expo run:androidThe --clean flag deletes existing ios/ and android/ before regenerating, which means any hand edits you made to native files will be wiped. That is the single most common pitfall — see the next section for how to make changes survive.
A Concrete Example: Adding a Native Module
Imagine adding a library that requires native build steps, such as react-native-mlkit-ocr:
# 1. Install the library
npx expo install react-native-mlkit-ocr
# 2. Re-run prebuild so native dependencies are picked up
npx expo prebuild --clean
# 3. Refresh CocoaPods
cd ios && pod install && cd ..
# 4. Verify the build
npx expo run:iosThe non-obvious bit is step 2: skipping prebuild --clean after installing the library means the Podfile never picks up the new dependency, and you end up staring at a "Native module cannot be found" error. I lost about half an hour to this once before it became muscle memory.
Two Ways to Make Native Changes Persistent
Because prebuild --clean regenerates the native directories, anything you edit by hand in Info.plist or AppDelegate.swift will vanish. Two strategies handle this cleanly.
Strategy 1: Manage Changes Declaratively with a Config Plugin
A Config Plugin rewrites native files based on app.json. To add a camera usage description to Info.plist, write the plugin like this:
// plugins/with-camera-usage.js
const { withInfoPlist } = require('@expo/config-plugins');
module.exports = function withCameraUsage(config) {
return withInfoPlist(config, (cfg) => {
cfg.modResults.NSCameraUsageDescription =
'We use the camera to scan business cards.';
return cfg;
});
};Register it in app.json:
{
"expo": {
"plugins": ["./plugins/with-camera-usage"]
}
}Now prebuild --clean will produce the same configuration every single time.
Strategy 2: Commit to Bare Workflow
If your native changes are extensive, you can stop running prebuild, commit ios/ and android/ to your repo, and operate as a Bare Workflow project. The cost is real merge work whenever you upgrade Expo SDK, so personally I push to keep everything inside Config Plugins for as long as possible.
Pairing Prebuild with EAS Build
After prebuilding, you can build locally — but EAS Build typically removes more friction. A starting eas.json looks like this:
{
"cli": { "version": ">= 5.0.0" },
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"ios": { "simulator": true }
},
"production": {
"autoIncrement": true
}
}
}Build commands:
# Development build (for on-device testing)
eas build --profile development --platform ios
# Production build with TestFlight upload
eas build --profile production --platform ios --auto-submitEAS Build runs Prebuild itself before the actual build, so you do not have to prebuild locally just for CI. You only need to prebuild on your machine when you want to use expo run:ios directly.
Common Pitfalls and How to Handle Them
Three issues I keep seeing in practice:
1. pod install fails because of an Xcode version mismatch
CocoaPods is sensitive to your Xcode version and deployment target. When the platform :ios, '15.1' line in Podfile and the minimum iOS each library demands disagree, you get an install failure. Setting image: latest on EAS Build keeps the cloud environment on the most recent Xcode, which minimizes the local–CI gap.
2. expo-router stops working after Prebuild
Rork-generated apps lean heavily on expo-router, and rewriting AppDelegate during Prebuild can change the initialization order, leaving you with a blank screen. The fix is usually to make sure expo-router is in the plugins array of app.json, which lets @expo/config-plugins apply the proper patches automatically.
3. Android complains it cannot find com.facebook.react:react-native
This usually means android/build.gradle is missing maven { url 'https://www.jitpack.io' } under allprojects.repositories. Either add it via a Config Plugin (withProjectBuildGradle) or follow the library's setup instructions to fix the repository list.
Reading the Generated AppDelegate Helps More Than You Think
One habit I have picked up: the first time I prebuild a project, I open ios/{ProjectName}/AppDelegate.swift and just read it. Modern Expo Prebuild generates a fairly compact AppDelegate that delegates most work to ExpoAppDelegate. Spending five minutes there clarifies several things at once:
- Where push notification tokens are forwarded
- How URL handlers route between expo-linking and other plugins
- What happens during background fetch entry points
- How
ExpoReactNativeFactoryinitializes the bridge
When something later goes wrong — push notifications never reaching the JS layer, or a deep link not opening the right screen — you already have a mental map of where to put a breakpoint. Without that mental map, debugging native issues from inside a Rork-style workflow can feel like guessing.
The same logic applies to android/app/src/main/java/.../MainApplication.kt. The generated code is not magic; once you have read it once, the rest of the project stops feeling opaque.
Keeping Prebuild Reproducible Across Machines
A practical detail that comes up the moment you collaborate or move CI: native build outputs depend on the exact versions of Xcode, Node, CocoaPods, and Ruby installed locally. Pinning these makes Prebuild reliable.
# .nvmrc
20.11.1
# .ruby-version
3.2.2
# In Podfile (already set by Expo, but verify)
platform :ios, podfile_properties['ios.deploymentTarget'] || '15.1'For the Xcode version on EAS Build, set it in eas.json:
{
"build": {
"production": {
"ios": { "image": "latest" }
}
}
}Once these versions are stable, "works on my machine but not on CI" stops happening as often. That alone has saved me more debugging time than any other configuration choice.
Going Deeper
When you hit specific errors while wiring up native modules, Rork Max + EAS Build Custom Native Module Errors: Complete Resolution Guide catalogs the common patterns and how to recover from each.
A Suggested Next Step
Expo Prebuild is the most important bridge between "a prototype that runs in Rork" and "a native app ready for production." Rather than starting with a large SDK integration, I recommend just running expo prebuild --clean and reading through the generated ios/ and android/ folders. That single act makes the difference between Managed and Bare Workflow tangible in a way no documentation quite manages.
From there, the habit to build is treating every native change as a Config Plugin contribution. Do that consistently and your Rork app gains a foundation that will keep working as the project grows.