You cannot securely store a privileged API secret inside a React Native app that ships it, so the secure approach is to keep those secrets on a backend and let your app call the backend instead. A tool like react-native-dotenv does not hide anything, because the values are bundled into your JavaScript and can be read from the app. Encrypted storage such as the Keychain or Keystore is real and useful, but it protects secrets your app obtains at runtime, like a user's session token, not secrets you try to embed at build time. Only keys designed to be public, and constrained by a server-side rule, belong in the app.
Short answer
Store privileged secrets on a backend, not in the app, because anything shipped in a React Native bundle can be extracted. Environment tools like react-native-dotenv inline values into your JavaScript, so they are readable, not secret. Encrypted storage, through react-native-keychain or Expo SecureStore, protects runtime secrets at rest on the device, not a key you embed at build time. For anything privileged, use a backend proxy. Only public or restricted keys belong in the app, and per the OWASP MASVS, secrets must not live in the client. If a secret already shipped, rotate it immediately.
Which keys are safe versus unsafe to ship
The distinction is whether a key is a privileged secret or a public identifier. A privileged secret grants access to billed services or private data on its own, and it can never be safely stored in a shipped app. This includes an OpenAI or other third-party API secret, a Stripe secret key, cloud credentials, and a Supabase service_role key. If leaking the value lets someone spend your money or read your data, it is unsafe to embed no matter how you store it.
A public identifier is designed to be visible in a client and is safe to ship when it is properly restricted. A Supabase publishable or anon key is safe only when Row Level Security governs what it can reach, and a client key such as Google Maps is safe when restricted to your app. The test is what the key can do alone: if it is powerless without a server-side rule backing it, it can live in the app; if it carries privilege by itself, it belongs on a server. Everything below follows from that split.
Does react-native-dotenv secure keys?
No, react-native-dotenv does not secure keys, and treating it as a hiding place is a common and costly mistake. Environment tools like react-native-dotenv, react-native-config, and Expo's public environment variables exist to inject configuration at build time, and the values they inject are compiled into your JavaScript bundle. Because the bundle ships inside the app, anyone can extract it and read those values, so a secret placed in an env file is just as exposed as one written directly in code.
The confusion comes from the word environment, which implies server-side secrecy as it would in a backend. On the client there is no such secrecy, because the app has to carry the value to use it. So use env tools for what they are good at, non-secret configuration such as an API base URL or a feature flag, and never for privileged keys. If your keys are in a dotenv file today, moving them there did not protect them; the fix is to remove the privileged ones from the app entirely.
What encrypted storage is actually for
Encrypted storage is real and worth using, but it solves a different problem than embedding a build-time key. Libraries like react-native-keychain and Expo SecureStore store values in the platform's secure storage, the iOS Keychain and the Android Keystore-backed store, protecting them at rest on the device. That is the right tool for a secret your app receives at runtime, such as a session token or refresh token issued after the user logs in, which you then keep securely between launches.
What encrypted storage cannot do is let you ship a secret inside the app. To embed a key and decrypt it at runtime, the app needs the decryption key, which must also be in the app, so an attacker simply takes both and decrypts the value. Encrypting a build-time secret into your bundle moves the problem without solving it. The rule is clear: use encrypted storage for runtime-obtained secrets like tokens, and do not use it as a way to smuggle a privileged API key into the app, because that is not what it protects against.
The real fix: a backend proxy for privileged secrets
For any operation that needs a privileged secret, the secure design is a backend proxy rather than storing the secret in React Native. Your app sends its request to a server endpoint you control, that server holds the API secret and makes the privileged call to the third party, and it returns only the result to the app. The secret never ships in the bundle, so it cannot be extracted, and rotating the key becomes a server change instead of an app update.
Protect the proxy, since it is now internet-facing. Authenticate your users so it is not an open relay, add rate limiting so a leaked user credential cannot run up unlimited cost, and scope each request to what that user is allowed to do. This is more work than dropping a key into an env file, but it is the only approach that actually keeps the secret secret. It also centralizes usage monitoring and key rotation in one place you control, rather than scattering a secret across every installed copy of your app.
Row Level Security and auth boundaries
When your backend is a service like Supabase, the same principle applies through its keys and Row Level Security. The Supabase anon key is meant to ship in your app, but it is safe only because Row Level Security policies decide what any request using it can read or write, so enable Row Level Security on every table and write policies that tie access to the authenticated user. Without those policies, the public key can reach data it should not, which is how many exposed-key incidents actually happen.
The service_role key is the opposite and must live only on a server, never in your React Native app, because it bypasses Row Level Security entirely. If your app needs to perform a privileged Supabase operation, do it through a backend or an Edge Function using the service_role key there. The general lesson holds beyond Supabase: a client-safe key is safe only because a server-side boundary constrains it, so put the boundary in place and keep the privileged key server-side.
If you already shipped a secret: rotate now
If a privileged secret is already in a released version of your app, treat it as compromised and rotate it immediately, because a shipped bundle is public and cannot be recalled. Revoke the exposed key at the provider and issue a new one so the leaked value stops working, then update your server-side code to use the new key. Do this before re-architecting, since the old key stays dangerous for as long as it is valid, and decompiling or unpacking an app bundle is quick.
After rotating, remove the secret from the app and move its operations behind your backend proxy, because reissuing a key and shipping the new one the same way just exposes a fresh secret. Check the provider's usage and billing for signs the key was already found and used, and tighten spending limits and alerts. Rotation contains the immediate exposure, and the architectural move, secrets on the server, keys out of the bundle, is what keeps the next one safe.
Safe storage at a glance
Sorting each kind of secret to where it belongs makes the choices concrete. The table below compares them.
| Secret type | Where it belongs | Why |
|---|---|---|
| Privileged API secret, such as OpenAI or Stripe | Backend only, behind a proxy | Extractable from any shipped bundle |
| Public or restricted client key | In the app, with restrictions | Powerless without a server-side rule |
| Build-time env value, dotenv | Non-secret config only | Inlined into the JavaScript, readable |
| Runtime secret, such as a session token | Encrypted storage, Keychain or SecureStore | Obtained at runtime, protected at rest |
Read the table by origin: a build-time privileged secret has no safe home in the app, while a runtime token does, and public keys ship only when a rule constrains them.
Secure-storage checklist
Working through these steps puts each secret where it belongs. The checklist below covers them.
| Step | Action | Done? |
|---|---|---|
| Classify your keys | Only public or restricted keys in the app | [ ] |
| Move secrets to a backend | Proxy privileged calls server-side | [ ] |
| Use encrypted storage for tokens | react-native-keychain or Expo SecureStore | [ ] |
| Enable Row Level Security | Scope any Supabase anon key with policies | [ ] |
| Rotate exposed secrets | Revoke and reissue any shipped secret | [ ] |
| Verify the bundle | Inspect the JavaScript for privileged keys | [ ] |
The step that matters most is the second: moving privileged secrets to a backend, because no on-device storage method, encrypted or not, makes an embedded secret safe.
Scan the build for leaked keys
Because a leaked key is easy to miss, especially when an AI code generator wired up your API client or an env file quietly carried a secret into the bundle, checking what actually ships is the reliable way to know. Storing keys wrong is consistently one of the most common mobile app weaknesses, so verification is worth building into your process.
A scanner like PTKD.com analyzes your React Native app build and reports issues such as leaked keys and secrets, insecure data storage, and over-broad permissions by severity, mapped to OWASP MASVS, so a privileged key that slipped into the bundle is flagged before users can extract it. To be clear about the boundary: PTKD does not build your backend proxy or write your Row Level Security policies. It finds the exposed key in the app you ship so you know what to rotate and remove.
What to take away
- You cannot securely store a privileged API secret in a shipped React Native app, so keep those secrets on a backend and call it from the app.
- react-native-dotenv and similar tools do not hide keys; they inline the values into your JavaScript bundle, where they are readable.
- Encrypted storage like react-native-keychain and Expo SecureStore protects runtime secrets such as tokens at rest, not a key you embed at build time.
- Only public or restricted keys belong in the app, and a Supabase anon key is safe only with Row Level Security constraining it; keep the service_role key server-side.
- If a secret already shipped, rotate it immediately, move it behind a proxy, and scan the build with a tool like PTKD.com to confirm nothing privileged remains.



