In a React Native app, why is an API key read from a .env file or an EXPO_PUBLIC_ variable not actually secret?
answer
- resolved at build time, not on the phone
- value copied into the shipped build
- APK and IPA are downloadable archives
- Expo docs: visible in plain text
- real secrets stay server-side
basics
~20 sThe value is copied into the build at build time, and that build ships inside every APK and IPA, so anyone who downloads the app can extract it. Real secrets must stay on a server the app calls.
solid answer
~50 sA `.env` file in a React Native project is read on the developer's machine or the build server, never on the phone. Expo CLI replaces each statically written `process.env.EXPO_PUBLIC_*` reference with the literal value inside the JavaScript bundle, and libraries such as `react-native-config` bake values into the native build; either way the value ends up inside the APK or IPA that every user downloads. Unzipping the package and searching the bundle finds it, and Hermes bytecode keeps string literals readable. So anything embedded in the app is configuration — an API base URL, a key the provider designed to be public — never a secret. Anything that grants spending or data access belongs on a backend: the app calls your server with the user's session, and the server attaches the key when it calls the third party.
code
typescript · 20 lines// Anti-pattern: Expo CLI inlines this value into the shipped bundle
export async function translateDirect(text: string) {
const key = process.env.EXPO_PUBLIC_TRANSLATE_KEY;
return fetch(`https://translate.provider.example/v1?key=${key}`, {
method: 'POST',
body: JSON.stringify({ text }),
});
}
// Instead: call your own backend; the provider key never leaves the server
export async function translateViaBackend(text: string, sessionToken: string) {
return fetch('https://api.myapp.example/translate', {
method: 'POST',
headers: {
Authorization: `Bearer ${sessionToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ text }),
});
}go deeper
Recall the core rule: a value in the app is readable by anyone who installs it, so .env and EXPO_PUBLIC_ are for configuration like an API URL, never for secret keys.
Explain the mechanism: values are resolved at build time and inlined into the bundle inside the APK or IPA, and name what may ship versus what must stay on a backend.
Show the design: a backend that holds provider keys, authenticates users, rate-limits per account and returns only needed data, plus a review step that treats every embedded value as published.
Frame it as classification policy across teams: which keys are public by design, who approves adding one to the client, and how you detect and rotate one that slipped into a release.
## What an environment variable means in a mobile build On a server, an **environment variable** is read by the running process at start-up, on a machine the attacker does not control. A React Native app has no such process on your side of the wire. The code runs on the user's phone, so any value the code needs has to travel inside the app package — the **APK** or **AAB** on Android, the **IPA** on iOS. A `.env` file in a React Native project is therefore read at **build time**, on a developer laptop or a CI worker, and its values are copied into the build output. The name "environment variable" survives, but the security property of a server environment variable does not. ## Where the value actually ends up | How the value gets in | When it is resolved | Where it lands | Readable by a user? | |---|---|---|---| | `process.env.EXPO_PUBLIC_X` in an Expo project | when Metro bundles | literal text in the JavaScript bundle | yes | | `react-native-config` or `react-native-dotenv` | at build time | the native build or the JavaScript bundle | yes | | an EAS environment variable with **secret** visibility that the app code reads | in the EAS build job | wherever the app code embeds it | yes | | a variable on your own backend | at server runtime | never leaves the server | no | The Expo documentation says it plainly: do not store sensitive info in `EXPO_PUBLIC_` variables, because they are visible in plain text in the compiled application. It also warns that EAS **secret** visibility only hides a value on the EAS website, in the CLI and in build logs — it gives no extra protection to a value the app itself embeds. The React Native security guide adds that `react-native-config` and `react-native-dotenv` are for environment-specific settings such as API endpoints, not secrets. ## Why the device is the attacker's machine - An APK and an IPA are **zip archives**; the JavaScript bundle is a file inside them. - With **Hermes**, the bundle is bytecode, but string literals sit in a readable string table, and decompilers exist. - A value the app sends over the network can also be read by the device's owner with an intercepting proxy, whatever the bundle looks like. - The user does not need a jailbroken or rooted phone to download a package and unzip it. The conclusion is not "hide it better". It is **classification**: every value in the app is public, so decide which values are allowed to be public. ## What may ship and what may not Values that are fine in the bundle: - your own API's **base URL** and environment name; - **publishable** keys a provider designs to be public, restricted to your app and scoped to low-risk calls; - feature flags and other non-sensitive configuration. Values that must never ship: - secret keys for payments, messaging, email or AI providers — anything billed per call; - database credentials, admin tokens, cloud access keys; - a signing key or an encryption key meant to protect data from the user. ## Where the secret goes instead 1. Keep the third-party key in your **backend's** own secret storage. 2. The app authenticates the user and calls **your** endpoint with the user's session token. 3. The backend checks the user, applies per-account rate limits, validates the input and calls the third party with the key. 4. The backend returns only the data the screen needs. A leaked user token then exposes one user's quota for a limited time; a leaked provider key exposes your whole bill until you rotate it. ## Traps interviewers listen for - **"The .env file is gitignored."** That keeps the value out of the repository, not out of the app. - **"`process.env.MY_KEY` is undefined, so I renamed it to `EXPO_PUBLIC_MY_KEY`."** Expo inlines only statically referenced `EXPO_PUBLIC_` names, so the rename is exactly the step that publishes the secret. - **OTA updates.** An update ships a new JavaScript bundle, so an embedded value also reaches every device through updates. The one-line interview answer: **a React Native app cannot keep a secret from its own user; keep secrets on a server and ship only values you are willing to publish.**
- In an Expo project, process.env.PAYMENT_KEY is undefined in the app. Why, and why is renaming it to EXPO_PUBLIC_PAYMENT_KEY the wrong fix?Expo CLI only replaces statically written `process.env.EXPO_PUBLIC_*` references; other names are never inlined into the client bundle, so they read as undefined. That is the guard working. Renaming the variable makes Expo copy the value into the bundle in plain text, which publishes the key. The fix is to move the call that needs the key to your backend.
- Is a key the provider calls publishable fine to embed in a React Native app?Usually, if the provider designed it to be public: it can only perform low-risk client operations, and the provider lets you restrict it to your app and cap its usage. It is still extractable, so apply those restrictions and watch usage. What never ships is the matching secret key that can charge, refund or read other users' data.
- Does storing the value as an EAS environment variable with secret visibility change the picture?No. Secret visibility keeps the value unreadable on the EAS website and in EAS CLI and masks it in job logs, which suits build-time credentials such as a private registry token. The Expo docs state that it adds no protection to a value the app embeds; once the app code reads it, it is in the package.
Embedding a key in the bundle is like printing the safe combination inside the instruction booklet that ships in every box: hiding it on a later page does not help, because every buyer owns the booklet.
saying these in an interview costs you the question
- A gitignored .env file keeps the key out of the shipped app.
- EAS secret visibility hides the value inside the compiled app.
- Hermes bytecode makes embedded string literals unreadable.
- Only rooted or jailbroken phones can read the app's bundle.
- The EXPO_PUBLIC_ prefix only exposes a variable to the app, not to users.