Description
Apps generated with create-better-t-stack crash on startup in production/TestFlight iOS builds while working fine in debug mode. The root cause is in the @vorbull/env/native.ts template (or equivalent env setup) where runtimeEnv: process.env is passed to @t3-oss/env-core.
Root Cause
In the generated packages/env/src/native.ts:
export const env = createEnv({
clientPrefix: "EXPO_PUBLIC_",
client: {
EXPO_PUBLIC_SERVER_URL: z.url().transform((url) => {
if (Platform.OS === "android") {
return url.replace("localhost", "10.0.2.2");
}
return url;
}),
},
runtimeEnv: process.env, // ← THIS IS THE PROBLEM
emptyStringAsUndefined: true,
});
Metro's transform for EXPO_PUBLIC_* environment variables only works on individual property accesses like process.env.EXPO_PUBLIC_SERVER_URL — it performs AST-level replacement. Passing process.env as a whole object to runtimeEnv results in an empty object in production Hermes bytecode bundles, because Metro doesn't replace the entire process.env reference.
This works in development because Metro dev server handles process.env differently, but in production (Release/TestFlight) builds where JS is pre-compiled to Hermes bytecode, the env vars are missing and Zod validation throws "Invalid environment variables".
The Crash Chain
On iOS 26+, this triggers a secondary React Native bug (facebook/react-native#54859):
- Env validation throws during module initialization
- A TurboModule catches the resulting
NSException
- React Native's
performVoidMethodInvocation tries to convert it to a JSError via convertNSExceptionToJSError from a background dispatch queue (not the JS thread)
- Accessing Hermes/JSI from the wrong thread causes heap corruption →
EXC_BAD_ACCESS (SIGSEGV) or SIGABRT
This makes debugging extremely difficult since the crash log points to Hermes internals rather than the actual env validation error.
Fix
Explicitly reference each env var so Metro can inline them:
runtimeEnv: {
- process.env,
+ EXPO_PUBLIC_SERVER_URL: process.env.EXPO_PUBLIC_SERVER_URL,
},
Environment
- Expo SDK 55
- React Native 0.83.2
- iOS 26.3.1 (iPhone 18,2)
- Hermes engine
@t3-oss/env-core with Zod validation
- Built via EAS Build (production profile)
Description
Apps generated with create-better-t-stack crash on startup in production/TestFlight iOS builds while working fine in debug mode. The root cause is in the
@vorbull/env/native.tstemplate (or equivalent env setup) whereruntimeEnv: process.envis passed to@t3-oss/env-core.Root Cause
In the generated
packages/env/src/native.ts:Metro's transform for
EXPO_PUBLIC_*environment variables only works on individual property accesses likeprocess.env.EXPO_PUBLIC_SERVER_URL— it performs AST-level replacement. Passingprocess.envas a whole object toruntimeEnvresults in an empty object in production Hermes bytecode bundles, because Metro doesn't replace the entireprocess.envreference.This works in development because Metro dev server handles
process.envdifferently, but in production (Release/TestFlight) builds where JS is pre-compiled to Hermes bytecode, the env vars are missing and Zod validation throws"Invalid environment variables".The Crash Chain
On iOS 26+, this triggers a secondary React Native bug (facebook/react-native#54859):
NSExceptionperformVoidMethodInvocationtries to convert it to a JSError viaconvertNSExceptionToJSErrorfrom a background dispatch queue (not the JS thread)EXC_BAD_ACCESS (SIGSEGV)orSIGABRTThis makes debugging extremely difficult since the crash log points to Hermes internals rather than the actual env validation error.
Fix
Explicitly reference each env var so Metro can inline them:
runtimeEnv: { - process.env, + EXPO_PUBLIC_SERVER_URL: process.env.EXPO_PUBLIC_SERVER_URL, },Environment
@t3-oss/env-corewith Zod validation