Environment variables in React Native are easy to underestimate until a team adds staging, rotates an API key, or discovers that a value bundled into JavaScript is visible in the app package. This guide gives you a practical workflow for handling dev, staging, and production configuration across Expo and React Native projects, with a clear separation between build-time config, runtime settings, and true secrets. The goal is not a clever setup. It is a configuration system your team can understand, audit, and update without breaking builds or leaking sensitive values.
Overview
A good React Native environment strategy solves three problems at once: it tells the app which backend or feature set to use, it keeps sensitive values out of places they do not belong, and it reduces friction in local development and CI/CD. That makes this topic part of testing, debugging, and DevOps as much as application code.
The first principle is simple: not every configuration value is a secret. Teams often put all config into one bucket, then either overprotect harmless values or accidentally expose real credentials. In practice, you usually have three categories:
- Public build config: API base URLs, Sentry environment labels, feature flags that are acceptable to ship in the app bundle, app display names, bundle identifiers, and deep link schemes.
- Semi-sensitive operational config: keys that are technically public but still worth controlling carefully, such as analytics write keys or environment-specific project IDs.
- True secrets: signing credentials, server-side tokens, private service account files, database credentials, and anything that should never live in the client app.
The second principle is just as important: mobile apps are distributed artifacts. If a value is baked into the app and sent to user devices, assume it can eventually be inspected. That means true secrets belong on your server, not inside React Native code, not in an Expo public env variable, and not in a client-side config file that happens to be hidden in Git.
For most teams, the safest reusable model looks like this:
- Keep a small, typed set of environment variables for the app.
- Maintain separate values for development, staging, and production.
- Use build pipelines to inject environment-specific values.
- Expose only values that are safe for the client.
- Move sensitive operations behind your backend whenever possible.
If you are still deciding how to organize project structure around configuration, it helps to pair this guide with a broader architecture approach such as React Native App Architecture Guide: Feature Folders, Domain Layers, and Scaling Patterns.
Step-by-step workflow
Use this workflow as a baseline process your team can return to whenever environments change.
1. Define your environment matrix before choosing tools
Start with a short table, not code. List the environments you actually need and the values that differ between them. For many apps, the minimum set is:
- Development: local backend or shared dev API, debug logging on, test push credentials, relaxed feature flags.
- Staging: production-like backend, release-like build settings, QA integrations, test payment or notification providers.
- Production: live backend, stable analytics, production push credentials, minimal logging.
Then map every variable into one of these questions:
- Does this need to change per environment?
- Does the client app need to know it?
- Would exposure create real risk?
- Should this be set at build time, runtime, or on the server?
This step avoids the common mistake of storing everything in one .env file and hoping naming conventions will save you later.
2. Create a single config module in the app
Do not scatter process.env usage across screens, hooks, and services. Instead, create one central config file that reads environment variables once, validates them, and exports a typed object.
A simple example in TypeScript might look like this:
type AppConfig = {
appEnv: 'development' | 'staging' | 'production';
apiBaseUrl: string;
sentryDsn?: string;
enableVerboseLogs: boolean;
};
function requireEnv(value: string | undefined, name: string) {
if (!value) throw new Error(`Missing environment variable: ${name}`);
return value;
}
export const config: AppConfig = {
appEnv: requireEnv(process.env.EXPO_PUBLIC_APP_ENV, 'EXPO_PUBLIC_APP_ENV') as AppConfig['appEnv'],
apiBaseUrl: requireEnv(process.env.EXPO_PUBLIC_API_BASE_URL, 'EXPO_PUBLIC_API_BASE_URL'),
sentryDsn: process.env.EXPO_PUBLIC_SENTRY_DSN,
enableVerboseLogs: process.env.EXPO_PUBLIC_ENABLE_VERBOSE_LOGS === 'true',
};The exact prefixes and loading method may differ depending on Expo or bare React Native tooling, but the pattern stays useful: one entry point, explicit validation, and a small exported surface.
This improves debugging because missing config fails early. It also improves testing because you can mock one module instead of multiple globals.
3. Separate client config from secrets
This is the most important operational boundary. A React Native app can safely ship with:
- API hostnames
- environment labels
- public analytics identifiers
- flags controlling UI or non-sensitive behavior
It should not ship with:
- private API tokens with broad access
- database passwords
- service account credentials
- store signing material
- server-only webhook secrets
If your app currently needs a private third-party key to call a service directly, that is often a sign to add a backend proxy or serverless endpoint that performs the sensitive operation on behalf of the client. This is a better long-term secrets management pattern than trying to hide a credential inside a mobile build.
4. Choose the right loading mechanism for your stack
In Expo projects, environment handling is often centered around app config, EAS, and supported env variable patterns. In bare React Native projects, teams may use a combination of Metro-time replacement, native build settings, and dedicated config libraries.
The tool choice matters less than the behavior you want:
- Local developers can run the correct environment easily.
- CI can build dev, staging, and production deterministically.
- The app can show which environment it is using.
- Configuration errors fail the build or fail fast on app startup.
If your release process is still evolving, this fits naturally with a broader deployment pipeline documented in React Native CI/CD Guide: GitHub Actions, EAS Build, Fastlane, and Store Releases.
5. Keep environment files minimal and predictable
When teams do use env files, they tend to age badly because values pile up and names drift. A better pattern is:
- Maintain a checked-in example file such as
.env.example. - Keep only variables the app truly consumes.
- Use consistent names across all environments.
- Document which values are safe for the client.
- Do not store production secrets in developer machines unless absolutely necessary.
A small example naming scheme:
APP_ENV=development
API_BASE_URL=https://dev-api.example.com
ENABLE_VERBOSE_LOGS=true
SENTRY_DSN=
In Expo, if you expose values to the app bundle through public env conventions, label them clearly so no one mistakes them for secure storage. A naming scheme is not security, but it does reduce accidental misuse.
6. Add schema validation
Human-readable docs are helpful, but code-level validation catches more issues. Whether you use a light custom function or a schema library, validate:
- required values
- valid URLs
- allowed enum values such as development, staging, production
- boolean parsing
- optional values with sane defaults
This turns vague runtime bugs into clear setup errors. It is one of the highest-leverage improvements for teams dealing with fragmented documentation and environment drift.
7. Surface the active environment inside the app
Many debugging sessions begin with a false assumption: the tester thinks they are on staging while the app is pointed at production, or vice versa. Reduce that confusion by displaying environment information in a developer menu, settings screen, or debug overlay.
Useful fields include:
- app environment
- API base URL
- app version and build number
- feature flag summary
- commit SHA or build identifier, if available
This is a small change that pays off every time someone investigates a release issue.
8. Connect environment config to test flows
Your test strategy should know which environment it is exercising. Unit tests may mock config entirely, but integration and end-to-end flows should be explicit about target environment and data setup.
For example:
- Unit tests use a mocked config module.
- Integration tests point to predictable test fixtures.
- E2E runs use a staging backend with resettable data.
That keeps failures attributable. If you are refining this part of the workflow, see React Native Testing Strategy: Unit, Integration, and E2E Tools Compared and Detox vs Maestro vs Appium for React Native E2E Testing.
9. Put secret injection in CI/CD, not in source control
Build systems should provide environment-specific values at build time. This applies to app config, native signing, store credentials, notification certificates, and any server-side tokens used during release automation.
The practical rule is:
- Store non-secret examples in the repo.
- Store real secrets in your CI/CD secret manager or platform secret store.
- Limit who can change production values.
- Audit and rotate credentials on a schedule.
For React Native app development, this is often the difference between a setup that works on one laptop and one that survives staff changes and emergency fixes.
Tools and handoffs
The exact tools may change over time, but the handoffs between people and systems are stable. That is where most environment mistakes happen.
Developers
Developers need a fast path for local setup and a safe path for changing config. Give them:
- a current
.env.exampleor equivalent config template - a short setup document
- one command per environment, if possible
- a typed config module with good error messages
They should not need to discover environment rules by reading native project files or old chat messages.
QA and product stakeholders
QA benefits from predictable staging builds, visible environment labels, and resettable test data. If a build can point to multiple services, expose that clearly. The less guesswork involved, the better your debugging signal will be.
DevOps or release owners
Release owners should control the pipeline that injects environment-specific values, signs builds, and promotes artifacts. Their responsibilities usually include:
- managing secret stores
- assigning build profiles or schemes
- verifying signing and release credentials
- ensuring production values are not reused in staging by accident
Configuration management is also tied to observability. If you use logging, crash reporting, or performance tracing, separate project IDs and environment tags make post-release debugging much easier. For adjacent troubleshooting practices, see React Native Debugging Toolkit: Flipper, React DevTools, Logs, and Network Inspectors and React Native Performance Checklist: What to Measure Before and After Every Release.
Backend team
The backend team often owns the real secrets and the environment boundaries that matter most. Mobile teams should coordinate with them on:
- separate API hosts or routing rules per environment
- test accounts and seed data
- token scopes and expiration
- feature flag ownership
- deprecation timelines for endpoints
This handoff is often overlooked. A clean React Native config layer cannot compensate for unstable or undocumented backend environments.
Quality checks
A configuration system is only complete when it has checks around it. These are the review points worth making standard.
Pre-commit and code review checks
- No hardcoded tokens in JavaScript, TypeScript, or native files.
- No new environment variable added without documentation.
- No direct
process.envaccess outside the config module. - No production credentials copied into local example files.
Build-time checks
- Required variables exist for the chosen environment.
- App identifiers, package names, and display names match the target environment.
- Signing and release credentials come from secure CI/CD storage.
- Staging and production builds do not share unintended endpoints.
Runtime checks
- The app can display its environment and API target.
- Feature flags resolve as expected.
- Logging verbosity changes by environment.
- Crash and analytics events include environment labels.
Security checks
- Anything truly secret is kept off the client.
- Rotated credentials are updated in CI/CD and server systems.
- Former team members no longer have access to production secret stores.
- Third-party integrations are reviewed for least-privilege access where possible.
It is also worth checking whether environment-specific behavior affects performance, memory, or app size. Verbose logging, debug-only instrumentation, and temporary feature flags can linger longer than expected. Related maintenance guides include React Native Memory Leak Guide: Common Causes, Detection Tools, and Fixes and How to Reduce React Native App Size on Android and iOS.
When to revisit
This setup should be reviewed whenever the inputs change, not only when something breaks. A useful rule is to revisit your React Native environment variables strategy when any of the following happens:
- You add a new deployment target such as staging, preview, or white-label builds.
- You migrate between Expo and a bare React Native workflow, or significantly change your build system.
- You introduce a new analytics, crash reporting, push notification, or authentication provider.
- You rotate credentials or change secret ownership.
- You split one backend into multiple services.
- You notice testers or developers repeatedly using the wrong environment.
- You onboard new team members and setup still depends on tribal knowledge.
To keep the process practical, end with a short maintenance checklist:
- Review your current variables and delete anything unused.
- Confirm each variable is categorized as public config or true secret.
- Verify your central config module still matches the real app behavior.
- Test one dev build, one staging build, and one production-like release build.
- Make sure CI/CD injects the right values and no secrets are committed.
- Update
.env.example, setup docs, and release runbooks. - Rotate sensitive credentials on your normal security schedule.
If your team treats environment management as living infrastructure instead of one-time setup, configuration becomes easier to reason about, easier to debug, and safer to operate. That is the real goal. A solid React Native config workflow should not feel clever. It should feel boring, visible, and reliable every time your app moves from development to staging to production.