When Environment Variables Return Undefined in Rork Apps
The first time I wired Supabase into a Rork-generated app for one of my own indie projects, the URL I had carefully placed in .env came back as undefined at runtime. The code was correct. The file existed. I spent the better part of an hour ruling things out before finding the culprit: Metro's bundler cache.
Expo and React Native handle environment variables in a way that surprises most developers — values are statically embedded at build time, not read at runtime. If you bring server-side or web-development instincts to this, you will trip somewhere. The good news is that once you internalize this one mechanism, every undefined error narrows down to a handful of causes.
Common Causes of Environment Variable Errors
There are five main reasons environment variables fail to load in Rork apps built on Expo and React Native.
1. Missing EXPO_PUBLIC_ Prefix
Since Expo SDK 49, any environment variable you want to access on the client side must start with EXPO_PUBLIC_. Variables without this prefix are intentionally excluded from the app bundle for security reasons. In my experience, this single issue accounts for more than half of all undefined errors.
2. Incorrect .env File Location
The .env file needs to be in the project root directory. Placing it in a subdirectory like src/ or config/ means it won't be picked up by the bundler.
3. Stale Metro Bundler Cache
Metro caches environment variable values at build time. Changing your .env file without clearing the cache means the app continues using the old values. This was my mistake in the story above — if a fix you know is correct refuses to take effect, suspect the cache first.
4. Confusing Build-time and Runtime Variables
EAS Build environment variables (set in eas.json or via EAS's environment variable management) and local .env files operate through different mechanisms. Understanding when each is used prevents a whole class of configuration bugs where the build succeeds but the app can't read its variables.
5. EAS Secrets Misconfiguration
Variables registered with EAS may not be injected into the build process if they're assigned to the wrong environment (development / preview / production) or named incorrectly.
Step-by-Step Fix
Step 1: Verify Your .env File
Start by confirming your .env file is in the right place with the correct format.
# Make sure you're in the project root
ls -la .env*
# Check the contents
cat .envHere's what a properly formatted .env file looks like:
# ✅ Correct: client-side variables with EXPO_PUBLIC_ prefix
EXPO_PUBLIC_API_URL=https://api.example.com
EXPO_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
EXPO_PUBLIC_SUPABASE_ANON_KEY=your_anon_key_here
# ✅ Correct: server-side / build-time only variables
SENTRY_AUTH_TOKEN=your_sentry_token
EAS_PROJECT_ID=your_project_idAnd here are the most common formatting mistakes:
# ❌ Wrong: missing the EXPO_PUBLIC_ prefix
API_URL=https://api.example.com
SUPABASE_URL=https://your-project.supabase.co
# ❌ Wrong: quoting the value
EXPO_PUBLIC_API_URL="https://api.example.com"
# ❌ Wrong: spaces around the equals sign
EXPO_PUBLIC_API_URL = https://api.example.comStep 2: Check How You Reference Variables in Code
Expo exposes environment variables through process.env.
// ✅ Correct way to reference variables
const apiUrl = process.env.EXPO_PUBLIC_API_URL;
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL;
// Debugging: confirm the values are loaded
console.log('API URL:', process.env.EXPO_PUBLIC_API_URL);
console.log('Supabase URL:', process.env.EXPO_PUBLIC_SUPABASE_URL);One subtle but important detail: dynamic key access like process.env[name] does not work. The build-time static analysis looks for the literal expression process.env.EXPO_PUBLIC_API_URL and replaces it with the value, so you must use direct dot notation.
You may also see older approaches using react-native-dotenv or expo-constants, but since Expo SDK 49 the process.env approach is the recommended one.
// ⚠️ Legacy approach (still works, not recommended)
import Constants from 'expo-constants';
const apiUrl = Constants.expoConfig?.extra?.apiUrl;
// ✅ Modern approach (recommended)
const apiUrl = process.env.EXPO_PUBLIC_API_URL;Step 3: Clear the Metro Cache
After changing your .env file, always clear Metro's cache before restarting.
# Clear the Metro cache and restart
npx expo start --clear
# For a more thorough reset
rm -rf node_modules/.cache
npx expo start --clearRunning plain npx expo start after editing .env will keep serving the old values, because environment variables are baked in at build time. I rewrote my .env several times before learning this, convinced the file itself was wrong. Make it muscle memory: touch .env, run --clear.
Step 4: Configure Environment Variables for EAS Build
For cloud builds with EAS Build, environment variables must be declared explicitly — for example in eas.json.
{
"build": {
"development": {
"env": {
"EXPO_PUBLIC_API_URL": "https://dev-api.example.com",
"EXPO_PUBLIC_SUPABASE_URL": "https://your-project.supabase.co"
}
},
"preview": {
"env": {
"EXPO_PUBLIC_API_URL": "https://staging-api.example.com",
"EXPO_PUBLIC_SUPABASE_URL": "https://your-project.supabase.co"
}
},
"production": {
"env": {
"EXPO_PUBLIC_API_URL": "https://api.example.com",
"EXPO_PUBLIC_SUPABASE_URL": "https://your-project.supabase.co"
}
}
}
}Separating development, staging, and production endpoints structurally prevents the classic accident of test data landing in your production database. As an indie developer shipping apps on my own, I know how tempting it is to skip environment separation, but once real user data starts flowing in after launch it's too late to untangle — set it up with your very first build.
Step 5: Managing Secrets — from eas secret to EAS Environment Variables
Sensitive values like API keys should never be written directly into eas.json. They belong in EAS's managed storage. Older guides use the eas secret:create command, but current versions of the EAS CLI have consolidated this into per-environment "EAS environment variables" (eas env:* commands), and the eas secret family is now considered deprecated.
# ✅ Current approach: EAS environment variables (registered per environment)
eas env:create --name EXPO_PUBLIC_SUPABASE_URL \
--value "https://your-project.supabase.co" \
--environment production --visibility plaintext
# Sensitive values should use secret visibility (unreadable after creation)
eas env:create --name SENTRY_AUTH_TOKEN \
--value "your_sentry_token" \
--environment production --visibility secret
# List registered variables
eas env:list --environment production
# Pull development variables into a local .env file (plaintext values only)
eas env:pull --environment developmentThe big improvement over the old Secrets system is eas env:pull, which lets you synchronize local and cloud values — making it far easier to catch the classic mismatch where the app works locally but the cloud build is missing a variable. If you're setting things up fresh, go straight to the eas env commands even if older documentation shows eas secret.
Verifying Your Configuration
Once you've applied the fixes, verify that the variables actually load.
// app/debug-env.tsx (temporary debugging file)
import { View, Text, ScrollView } from 'react-native';
export default function DebugEnvScreen() {
const envVars = {
API_URL: process.env.EXPO_PUBLIC_API_URL,
SUPABASE_URL: process.env.EXPO_PUBLIC_SUPABASE_URL,
SUPABASE_ANON_KEY: process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY
? '✅ Set (hidden)'
: '❌ Not set',
};
return (
<ScrollView style={{ padding: 20 }}>
<Text style={{ fontSize: 20, fontWeight: 'bold', marginBottom: 16 }}>
Environment Variables Debug
</Text>
{Object.entries(envVars).map(([key, value]) => (
<View key={key} style={{ marginBottom: 12 }}>
<Text style={{ fontWeight: 'bold' }}>{key}</Text>
<Text style={{ color: value ? '#22c55e' : '#ef4444' }}>
{value || 'undefined ❌'}
</Text>
</View>
))}
</ScrollView>
);
}Delete this debug screen once you've confirmed everything works. Keeping it out of production builds is essential to avoid leaking configuration details.
Best Practices to Prevent Recurrence
Adopt these rules in your project so environment variable issues don't come back.
Never put real secrets behind EXPO_PUBLIC_
This is the most important rule, and the one most often missed. Variables with the EXPO_PUBLIC_ prefix are embedded in your JS bundle in plain text. Anyone who downloads your app from the store can extract them, so only values that are safe to publish belong there. A Supabase anon key is fine — it's designed to be public, with Row Level Security as the real gatekeeper. But the moment you put a service_role key or a payment provider's secret key behind EXPO_PUBLIC_, you have effectively shipped it to every user. Keep those values server-side only, for example in Supabase Edge Functions.
Include a .env.example in your repository
For your team — and your future self — keep a template of required variables in the repo.
# .env.example (values empty or dummy)
EXPO_PUBLIC_API_URL=
EXPO_PUBLIC_SUPABASE_URL=
EXPO_PUBLIC_SUPABASE_ANON_KEY=Add .env to .gitignore
Never commit actual .env files with real credentials:
# .gitignore
.env
.env.local
.env.productionCreate a Type-safe Environment Helper
In TypeScript projects, wrap your environment access in a utility that warns you about missing values early:
// utils/env.ts
function getEnvVar(key: string): string {
const value = process.env[key];
if (!value) {
console.warn(`⚠️ Environment variable ${key} is not set`);
}
return value ?? '';
}
export const ENV = {
API_URL: getEnvVar('EXPO_PUBLIC_API_URL'),
SUPABASE_URL: getEnvVar('EXPO_PUBLIC_SUPABASE_URL'),
SUPABASE_ANON_KEY: getEnvVar('EXPO_PUBLIC_SUPABASE_ANON_KEY'),
} as const;Note that the dynamic lookup inside getEnvVar works here only because the static references inside the ENV object are what cause the values to be embedded at build time, as covered in Step 2. Using this ENV object throughout your app prevents typos in variable names and surfaces missing configuration as a warning right at startup.
Looking Back
When an environment variable comes back undefined, the checking order is always the same: ① the EXPO_PUBLIC_ prefix → ② the .env file's location and format → ③ npx expo start --clear → ④ the EAS-side environment configuration. In my experience, the vast majority of cases resolve at ① or ③.
As a next step, add a .env.example and the type-safe ENV helper to your project today. Troubleshooting isn't finished until you've built the guardrails that keep the same error from costing you an hour twice.