Skip to content

Expo native app crashes on iOS production builds due to process.env not being inlined by Metro #987

@shkumbinhasani

Description

@shkumbinhasani

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):

  1. Env validation throws during module initialization
  2. A TurboModule catches the resulting NSException
  3. React Native's performVoidMethodInvocation tries to convert it to a JSError via convertNSExceptionToJSError from a background dispatch queue (not the JS thread)
  4. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions