The app opens fine in the simulator and in Expo Go. Then you build it in Release for submission, tap the icon on an iOS device, and it is gone before a single screen appears. With nothing on screen, there is no hint about where to start.
A report filed on Expo's GitHub in September 2026 has exactly this shape: in an expo-router app, if app.json has no scheme, only the iOS Release build crashes right at launch. If you export a Rork project and build it on your own Mac, it is worth opening your app.json once to check.
What the report describes, and why no diagnostic flags it
The minimal reproduction used this combination:
- expo 57.0.24, expo-router 57.0.22, expo-linking 57.0.10, react-native 0.86.3
- No
schemein app.json - The iOS Release build aborts with SIGABRT right after launch
- The same app starts normally in iOS Debug, and in Android Debug and Release
One detail is easy to miss. In the reporter's environment, expo-doctor passed all 21 checks. This is not the kind of failure a green diagnostic rules out, and as an indie developer who tends to relax once the checks are green, I wanted that written down.
Why Debug and Android survive
The explanation from Expo's verification bot and the reporter's own trace agree, so I'll follow them in order.
On iOS, expo-router calls Linking.createURL('/') synchronously at launch to build the root URL. Inside it, expo-linking resolves the scheme. When there is none, the behavior depends on the mode:
- In Debug (
__DEV__is true), it only prints a warning and carries on - In Release, it throws
Cannot make a deep link into a standalone app with no custom scheme defined
On iOS, that exception is not caught during startup and is reported as a fatal JS error. Release has no red screen, so it becomes a real native crash. On Android the same call sits inside a Promise then, so the exception turns into a rejected Promise and the app stays up. Same logic, but only iOS calls it synchronously — that is the whole "only iOS Release" puzzle.
Two ways to fix it
1. Add a scheme
This is the most dependable fix and leaves the rest of the app alone.
{
"expo": {
"name": "my-app",
"slug": "my-app",
"scheme": "myapp"
}
}In the bot's verification, the build with a scheme opened on 3 of 3 cold launches. Use lowercase letters and digits, and something unlikely to collide with another app.
2. Move to a fixed expo-router
The fix landed in expo-router. Its CHANGELOG lists, under 58.0.11, "Fix a crash on launch in production when the app has no scheme." The change is small: it catches the error from createURL and falls back to '/'. Apps that have a scheme take the same path as before.
One caveat: I could only confirm the 58.0.11 entry. I have not confirmed whether a release on the SDK 57 line carries the same fix. If you are shipping on SDK 57, adding the scheme from option 1 is the safer move while you wait.
When leaving the scheme out is deliberate
The reporter did not omit the scheme by accident. Without a custom URL scheme registered with the OS, other apps have fewer ways to open yours with myapp://, and that was the point.
It helps to separate two things here. As the bot's note put it, a scheme only lets other apps open your app by URL. What protects a sensitive screen is the authentication check on that screen, not the presence or absence of a scheme. Removing the scheme is not a reason to skip auth, and adding one does not weaken it.
For a wallpaper-style app I run, there are few screens that deep links could open to anyone's harm. For an app with purchases or anything close to personal data, the practical order is: add the scheme, then confirm that opening those screens directly still hits the auth check.
The scheme is the number of doors; authentication is the lock. Decide how many doors to have and whether to lock them as two separate decisions.
Two checks before you submit
This kind of bug never shows up in hours of Debug testing. I'm adding two checks to my own pre-submission routine.
Check 1: does the resolved config have a scheme?
To catch apps that build their config in app.config.js as well as plain app.json, look at the resolved config.
npx expo config --json | node -e '
let s = "";
process.stdin.on("data", d => s += d).on("end", () => {
const j = JSON.parse(s);
const c = j.exp ?? j; // tolerate either output shape
const sc = [].concat(c.scheme ?? []).filter(Boolean);
console.log(sc.length ? "scheme: " + sc.join(", ") : "NO SCHEME");
process.exit(sc.length ? 0 : 1); // lets CI or a submit script fail
});'If it prints NO SCHEME, ask yourself once whether that was intentional. If it was, confirm you are on a fixed expo-router; if not, add the scheme.
Check 2: cold-launch the Release configuration
Run the Release configuration, fully quit the app, and open it again:
npx expo run:ios --configuration ReleaseThe bot's procedure was to terminate the app, cold-launch it, wait 10 seconds, and record the state, three times. A launch crash can slip past once by chance, so I look three times too.
Pitfalls
- A passing
expo-doctordoes not catch this - Debug working, or Android Release working, is not evidence that iOS Release works
- If you add a scheme, make sure you are not reusing the same string in another app of your own
Other causes produce the same "only Release dies" symptom. I wrote up a resource looked up by name and removed by shrinking in debug worked, release failed with "not found" at launch, and a flag that silenced the log and hid the real failure in a build that stops with exit code 0. How far a device check gets you toward the store build is in a screen that worked in Rork Companion is not finished yet.
Before your next build, run Check 1 once. It prints a single line, but it is a line that spares you a panicked submission night — or so it feels to me now.