Environments & EAS Environment Variables
Separate dev, staging, and production configuration without committing secrets - using local .env files for development and EAS environment variables for cloud builds, updates, and workflows.
Search across all documentation pages
Separate dev, staging, and production configuration without committing secrets - using local .env files for development and EAS environment variables for cloud builds, updates, and workflows.
Quick-reference recipe card - copy-paste ready.
// eas.json - wire each build profile to an EAS environment
{
"build": {
"development": {
"developmentClient": true,
"distribution": "internal",
"environment": "development"
},
"preview": {
"distribution": "internal",
"environment": "preview"
},
"production": {
"distribution": "store",
"environment": "production"
}
}
}# Create cloud variables (repeat per environment)
eas env:create --name EXPO_PUBLIC_API_URL \
--value https://api.staging.example.com \
--environment preview \
--visibility plaintext
# Pull preview vars locally (writes .env.local - keep gitignored)
eas env:pull --environment preview
# Publish an OTA update using the same environment as the build
eas update --environment production --message "Hotfix auth redirect"// src/config.ts - only EXPO_PUBLIC_ vars are inlined into the bundle
export const config = {
apiUrl: process.env.EXPO_PUBLIC_API_URL ?? "http://localhost:3000",
appEnv: process.env.EXPO_PUBLIC_APP_ENV ?? "development",
} as const;When to reach for this:
.env files on remote runners.EXPO_PUBLIC_* values.// app.config.ts - branch native config per environment without hardcoding secrets
import type { ExpoConfig } from "expo/config";
const APP_ENV = process.env.EXPO_PUBLIC_APP_ENV ?? "development";
const bundleIds: Record<string, string> = {
development: "com.example.myapp.dev",
preview: "com.example.myapp.staging",
production: "com.example.myapp",
};
const apiUrls: Record<string, string> = {
development: "http://localhost:3000",
preview: "https://api.staging.example.com",
production: "https://api.example.com",
};
const config: ExpoConfig = {
name: APP_ENV === "production" ? "My App" : `My App (${APP_ENV})`,
slug: "my-app",
version: "1.0.0",
ios: {
bundleIdentifier: bundleIds[APP_ENV] ?? bundleIds.development,
},
android: {
package: bundleIds[APP_ENV] ?? bundleIds.development,
},
extra: {
appEnv: APP_ENV,
apiUrl: process.env.EXPO_PUBLIC_API_URL ?? apiUrls[APP_ENV],
eas: {
projectId: "00000000-0000-0000-0000-000000000000",
},
},
};
export default config;// App.tsx - read runtime config from inlined public vars and app.config extra
import Constants from "expo-constants";
import { StyleSheet, Text, View } from "react-native";
export default function App() {
const apiUrl =
process.env.EXPO_PUBLIC_API_URL ??
(Constants.expoConfig?.extra?.apiUrl as string | undefined) ??
"not configured";
const appEnv =
process.env.EXPO_PUBLIC_APP_ENV ??
(Constants.expoConfig?.extra?.appEnv as string | undefined) ??
"unknown";
return (
<View style={styles.container}>
<Text style={styles.title}>Environment: {appEnv}</Text>
<Text style={styles.body}>API: {apiUrl}</Text>
<Text style={styles.hint}>
EAS builds inject EXPO_PUBLIC_* at bundle time. Secrets never appear here.
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, justifyContent: "center", padding: 24, gap: 8 },
title: { fontSize: 20, fontWeight: "700" },
body: { fontSize: 15, color: "#374151" },
hint: { fontSize: 13, color: "#6b7280", marginTop: 12 },
});# .gitignore - never commit local overrides or pulled secrets
.env*.localWhat this demonstrates:
development, preview, production) to eas.json build profiles.EXPO_PUBLIC_* for client-safe values that Metro inlines at bundle time.app.config.ts..env, .env.local, and other standard dotenv files. Variables prefixed with EXPO_PUBLIC_ are statically replaced in your source during bundling.environment is set on a build profile, EAS injects that environment's variables into the build worker. Plaintext and sensitive variables are available when resolving app.config.ts; secret variables are available only on the server during the job.eas update --environment <name> is required. Only variables from that EAS environment are used - local .env files are ignored, keeping OTA bundles aligned with store builds.| EAS environment | Typical use | eas.json signal (if environment omitted) |
|---|---|---|
development | Dev client, local API, debug tooling | developmentClient: true |
preview | Internal QA, staging API, TestFlight/Play internal | Everything else (default fallback) |
production | App Store / Play Store releases | distribution: "store" |
Explicit environment fields are recommended - auto-detection is convenient but easy to misconfigure when profiles share similar flags.
| Visibility | Readable in dashboard/CLI | In build logs | Client bundle |
|---|---|---|---|
| Plain text | Yes | Yes | Only if referenced as EXPO_PUBLIC_* |
| Sensitive | Toggle on dashboard | Obfuscated | Only if referenced as EXPO_PUBLIC_* |
| Secret | No (server-only) | Obfuscated | Never - build-time tooling only |
Anything embedded in client JavaScript is extractable from the compiled app. Use secrets for
NPM_TOKEN,SENTRY_AUTH_TOKEN, and similar build-time values - not for "private" API keys shipped to users.
These are set automatically on EAS Build workers (not part of your project environments):
| Variable | Value |
|---|---|
EAS_BUILD | true |
EAS_BUILD_PROFILE | Profile name from eas.json |
EAS_BUILD_PLATFORM | android or ios |
EAS_BUILD_ID | Unique build UUID |
EAS_BUILD_PROJECT_ID | Linked EAS project ID |
Use EAS_BUILD_PROFILE in app.config.ts when you need profile-specific logic beyond the three standard environments.
// env.d.ts - document expected public variables for the team
declare namespace NodeJS {
interface ProcessEnv {
EXPO_PUBLIC_API_URL: string;
EXPO_PUBLIC_APP_ENV: "development" | "preview" | "production";
}
}
// Static access only - Metro will NOT inline bracket notation or destructuring
const url = process.env.EXPO_PUBLIC_API_URL; // ✓ inlined
// const url = process.env["EXPO_PUBLIC_API_URL"]; // ✗ not inlinedEXPO_PUBLIC_* - Any value inlined into JavaScript is visible in the app binary. Fix: Keep secrets server-side; use short-lived tokens from your backend.--environment on eas update (SDK 55+) - Updates may not match the variables used in your production build. Fix: Always pass --environment matching the target build profile.NODE_ENV to switch .env files - npx expo export forces NODE_ENV=production, so test/staging files won't load as expected. Fix: Use eas env:pull --environment <name> or explicit EXPO_PUBLIC_APP_ENV.process.env['EXPO_PUBLIC_KEY'] is not statically analyzable and won't be inlined. Fix: Use dot notation: process.env.EXPO_PUBLIC_KEY.app.config.ts locally - Secret-type variables are unreadable outside EAS servers. Fix: Use sensitive/plaintext for values needed during local config resolution, or provide dev defaults..env.local after eas env:pull - Pulled files often contain staging/production values. Fix: Add .env*.local to .gitignore and use separate pull commands per developer machine.production paired with a preview build causes wrong secrets. Fix: Set jobs.<id>.environment to match the build profile's environment in EAS Workflows.| Alternative | Use When | Don't Use When |
|---|---|---|
| EAS environment variables | Cloud builds, updates, team-wide config | You need zero Expo account dependency |
Local .env files only | Solo dev, no EAS, open-source samples | Remote CI/CD or EAS Build - files won't be on the runner |
eas env:pull + .env.local | Local dev parity with cloud environments | You want fully offline config with no Expo CLI |
app.config.ts + extra field | Values needed before JS bundle (icons, bundle ID) | Simple runtime flags - prefer EXPO_PUBLIC_* |
react-native-config | Legacy bare RN apps not on Expo env vars | New Expo SDK 57 projects - migrate to EXPO_PUBLIC_* |
| Runtime remote config (Firebase, LaunchDarkly) | Feature flags that change without a new build | Static API URLs known at build time |
development, preview, and production. Each is an independent set of variables. Custom environment names are available on Enterprise and Production EAS plans.
Add the environment field to the profile:
{
"build": {
"production": {
"environment": "production"
}
}
}All variables assigned to that environment are injected during the build.
Only variables prefixed with EXPO_PUBLIC_. They are inlined into the bundle at build time and are extractable from the compiled app. Never prefix private keys or admin tokens with EXPO_PUBLIC_.
eas env:pull --environment developmentThis writes a .env.local file. Restart Metro or reload the app to pick up changes. Keep .env.local gitignored.
To guarantee OTA bundles use the same EAS environment variables as cloud builds - not whatever .env file happens to exist on the machine running the update command.
Expo's Metro config only inlines static dot notation (process.env.EXPO_PUBLIC_API_URL). Bracket access and destructuring are not supported. Use dot notation everywhere.
Build-time values like NPM_TOKEN or private registry credentials. They are unreadable outside EAS servers and are not available during local app.config.ts evaluation. They do not protect values you embed in client code.
Automatically: production when distribution is store, development when developmentClient is true, and preview for everything else. Explicit configuration is safer for teams with many profiles.
Yes. Build jobs inherit environment from the matching eas.json profile. Other job types (update, fingerprint, Maestro) accept jobs.<id>.environment. Keep environments consistent across jobs in the same workflow.
Upload a file (for example google-services.json) as an environment variable. EAS exposes it as a file path on the build runner. Useful for credentials that must exist as files during native compilation.
Commit a .env with safe defaults or placeholder EXPO_PUBLIC_* values if helpful for onboarding. Never commit .env.local or files containing real secrets. Use EAS for production values.
eas env:exec --environment production 'npx sentry-expo-upload-sourcemaps dist'eas env:exec loads the environment before executing the shell command.
Node evaluates app.config.ts on the build worker. Plaintext and sensitive EAS variables are available as process.env.*. Use them to set bundleIdentifier, name, plugins, and extra fields per environment.
They are independent concepts that you align by convention. EXPO_PUBLIC_APP_ENV is a variable you define. EAS development/preview/production are containers for variables. Set EXPO_PUBLIC_APP_ENV=preview inside the preview EAS environment for consistency.
APP_VARIANT branching and EAS project linkingStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026