◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-06-02Intermediate

Fixing 'No bundle URL present' on a Release Build to a Real iOS Device

How to read the 'No bundle URL present' error that shows up when you build a Rork-exported app locally in Xcode. We separate the Metro-connection case from the missing-embedded-bundle case, and walk through generating an offline bundle by hand with working commands.

Rork585Expo219iOS116Metro4Error Handling8

I wanted to add a WidgetKit extension to one of my wallpaper apps, so I opened the Expo project that Rork had exported in Xcode and installed a Release build onto my phone. The moment the splash screen disappeared, the screen filled with red text:

No bundle URL present.

Make sure you're running a packager server or have included a .jsbundle file in your application bundle.

It ran fine in the development build, so at first I blamed my own code. The real cause was earlier than that: the JavaScript simply wasn't anywhere the app could find it. You may have seen the same red screen in a Debug build, but Debug and Release fail for completely different reasons. Mix them up and you lose half a day. I did.

What the error is actually telling you

No bundle URL present means the native side of the iOS app has no idea where to load JavaScript from at launch. A React Native / Expo app runs a JavaScript bundle inside a native shell, and that bundle can live in one of two places.

  • Debug build: it connects to the Metro bundler running on your Mac at http://<your-mac-ip>:8081 and loads the JS over the network
  • Release build: it loads from a main.jsbundle file baked into the app itself

The red screen means neither of those was found. So the very first thing to determine is whether it happened in Debug or Release.

If it happened in a Debug build — Metro isn't reachable

If your scheme was still set to Debug when you installed onto the device, this is almost always a Metro connection problem. In order of how often I hit each:

  1. Metro isn't running. Forgetting to start npx expo start in a terminal is the most common trap of all.
  2. The Mac and the iPhone are on different Wi-Fi networks. A physical device can't reach your Mac via localhost. Same network is the baseline requirement.
  3. The router blocks device-to-device traffic. Guest SSIDs and "client isolation" settings stop the connection even when both devices show the same network name.

The quick check: open http://<your-mac-ip>:8081/status in Safari on the iPhone. If it returns packager-status:running, the route is alive and the problem is elsewhere. If nothing comes back, it's a network issue.

The most reliable path is to connect the device over USB and reinstall with:

# With the device connected over USB, this forwards the port and launches it
npx expo run:ios --device

expo run:ios resolves the device and handles the Metro port for you. On flaky Wi-Fi this is by far the least painful option.

If it happened in a Release build — the bundle wasn't baked in

This is the real subject. A Release configuration never talks to Metro. The app must contain a main.jsbundle, and without it you get the red screen. This is exactly where people who start editing a Rork-exported project locally in Xcode get stuck.

There are two main causes.

1. The bundling build phase didn't run

A React Native iOS project has a shell-script build phase called "Bundle React Native code and images" that packages the JS at build time. A clean project from Expo prebuild includes it, but while hand-editing native code you can accidentally set it to skip, or the script's reference paths can drift, and then no bundle is produced even in Release.

In Xcode, open the target → Build Phases and confirm the Bundle React Native code and images phase exists and that Run script only when installing is unchecked. When it's checked, the script runs only during Archive, not on a normal Run — which produces the confusing symptom "red screen on device, but Archive works fine."

2. You aren't using expo run:ios --configuration Release

Rather than hitting raw xcodebuild or Xcode's Run in Release, it's more reliable to let Expo handle the Release build.

# Let Expo do the Release build, bundle embedding included
npx expo run:ios --configuration Release --device

This command generates the bundle and embeds it before installing to the device, so you spend far less time fighting manual build-phase settings.

A stopgap — building the offline bundle by hand

When you just want it running on a device right now, before touching build-phase settings, you can generate the bundle manually and drop it into the project.

# Generate main.jsbundle and assets under the ios directory
npx react-native bundle \
  --platform ios \
  --dev false \
  --entry-file index.js \
  --bundle-output ios/main.jsbundle \
  --assets-dest ios

On projects using Expo Router, --entry-file may need to be node_modules/expo-router/entry.js. If it complains it can't find the entry, check the main field in package.json. Add the generated main.jsbundle to the target in Xcode (not as a folder reference), and Release builds will read from it.

But treat this strictly as a stopgap. A hand-placed bundle doesn't auto-update when you change code, so once you've fixed the real cause (the build phase), remove the manual bundle. I stepped on this trap a few times and caused a secondary mess — "it worked!" while an old JS bundle was still baked in — so I'll say it plainly.

The order I work through it (when in doubt, follow this)

When I see the red screen, I always knock it down in this order.

  1. Is the configuration Debug or Release? (check the scheme at the top-left of Xcode)
  2. If Debug: is Metro running, and are the device and Mac on the same Wi-Fi?
  3. If Release: does Build Phases include the bundling phase, and is only when installing unchecked?
  4. Still failing? Rebuild with expo run:ios --configuration Release --device
  5. If you need a working build faster than a root-cause fix, generate the offline bundle by hand

Following that order alone keeps you from the detour of distrusting your code and reading logs endlessly.

Prevention — so you never lose half a day again

I've run apps with over 50 million cumulative downloads as a solo developer since 2014, and I often split the work: scaffold quickly in Rork, then add only the native pieces — WidgetKit, Live Activities — in Xcode. Once I made it a habit to verify on a real device in Release every time, this error stopped stopping me. Don't conclude "it works" from the dev build alone; install the store configuration (Release) onto a real device at least once. That one habit prevents the panic moments right before review.

Try installing your current project onto a device with npx expo run:ios --configuration Release --device. If no red screen appears there, your distribution build is carrying its bundle correctly. I hope this helps anyone stuck at the same spot.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-09-21
A build that stops with exit code 0 — the flag that silenced the log, and the check that let an empty file through
Two reports landed in the same week: eas build --local stopping with exit code 0 but produced no further output, and a zero-byte stub that never got embedded. Both came down to settings that reduce output. Here is how to audit your own scripts.
⬡ Dev Tools2026-09-05
Your Widget Extension Can Submit a BGTaskScheduler Request. It Just Cannot Register the Handler
Calling BGTaskScheduler from a widget extension compiles, submits, and returns true — and then nothing runs. Here is why registration belongs to the host app only, and how I moved refresh ownership back where it belongs across my wallpaper apps.
⬡ Dev Tools2026-07-27
When to Raise Your Minimum iOS Version — Count Leftover Branches, Not User Percentages
Judging a minimum OS bump by usage share produces the same answer every year, so the decision never happens. Here is the annotation convention, the sweep script that counts how many iOS and Android branches each candidate floor would retire, and what to watch for 30 days after.