You want to keep an API key out of reach in your Expo managed app, and the honest starting point is that you cannot hide a true secret inside any client app. Anything shipped in the bundle can be read by anyone who downloads it. What you can do is move the real secret to a server and keep only safe values on the device. Here is how to tell them apart and where each one belongs.
Short answer
You cannot hide a runtime secret in an Expo managed app, because everything in the bundle is readable. Expo's own environment variables guide warns that values prefixed with EXPO_PUBLIC_ are inlined in plain text into your compiled app and must never hold sensitive secrets. The only reliable way to protect a key like a Stripe secret key or an OpenAI key is to keep it on a server, such as an Expo Router API route or your own backend, and have the app call that endpoint. Publishable or restricted keys can safely stay in the client.
What you should know
- Client bundles are readable: anything you embed in the app, including environment variables, can be extracted by a user.
- EXPO_PUBLIC_ is not secret: those variables are inlined in plain text, by Expo's own description.
- EAS secrets are build-time: they protect values in your build pipeline, not values you embed in the app.
- Real secrets belong on a server: a backend or an API route holds the key and the app calls it.
- Some keys are meant to be public: publishable and restricted keys are safe in the client when you lock them down provider-side.
Why can't you hide a key in an Expo app at all?
Because the JavaScript bundle and assets ship to the device, and anyone can read them. When Expo builds your app, it inlines EXPO_PUBLIC_ environment variables directly into the bundle as plain text, which Expo states explicitly: do not store sensitive info such as private keys in EXPO_PUBLIC_ variables, because they are visible in plain text in the compiled application. The same is true for values placed in app.config.js under extra and read through Constants. There is no client-side hiding place that survives a determined look at the binary, so the question is not how to hide the key but where the key should live.
What about EAS Secrets, do they protect my key?
Only in the build pipeline, not in the shipped app. EAS environment variables and secrets keep a value out of build logs and let a build job use it, for example an npm token to install a private package or a token to upload source maps. Expo is direct that secrets do not provide any additional security for values you end up embedding in your application itself. So an EAS secret that you bake into the app at build time is still in the binary. EAS visibility settings, secret, sensitive, and plain text, govern who can see the value inside EAS, not whether it is exposed once shipped.
What is the right way to handle a real secret?
Keep it on a server and never send it to the device. The pattern is the same across stacks: the secret lives in a backend, the app makes a request to your endpoint, and the server uses the key and returns only the result. With Expo, Expo Router API routes (files ending in +api.ts) run on the server, so a key read there is never visible to the app user. A standalone backend or a serverless function works the same way. The table sorts where each kind of key belongs.
| Key type | Safe in the client bundle? | Where it belongs |
|---|---|---|
| True secret (Stripe secret key, OpenAI key, database password) | No | Server: a backend or an API route |
| EXPO_PUBLIC_ variable | No, inlined in plain text | Non-sensitive configuration only |
| Publishable or restricted key (Firebase config, Stripe publishable, Maps key with restrictions) | Yes, by design | Client, with provider-side restrictions |
| EAS secret used during the build | Protected in EAS, not in the app | Build and CI only |
Which keys are safe to keep in the client?
The ones designed to be public. A Firebase web config, a Stripe publishable key, or a Google Maps key are meant to live in client apps, because their power is limited and they are protected by server-side rules and provider restrictions rather than by secrecy. The job there is to restrict them: lock a Maps key to your bundle identifier, rely on Firebase Security Rules and App Check, and keep the Stripe secret key on your server while only the publishable key reaches the device. A key is safe in the client when exposure does not let someone abuse it, not when you have hidden it well.
How do you verify before submitting?
Confirm the secret is actually gone from the build, not just from your source. It is easy to move a key to a server and leave an old copy in an EXPO_PUBLIC_ variable, a committed .env, or a hardcoded string. A pre-submission scan such as PTKD.com (https://ptkd.com) reads the compiled APK, AAB, or IPA for hardcoded secrets against OWASP MASVS, so you see what an attacker would see after extracting your bundle. Rotate any key that was ever shipped in a client build, because once it has left in a binary you cannot assume it is private again.
What to take away
- You cannot hide a runtime secret in an Expo managed app; everything in the bundle is readable, and EXPO_PUBLIC_ values are inlined in plain text.
- EAS secrets protect values in your build pipeline, not values you embed in the app, so a secret baked into the build still ships in the binary.
- Keep real secrets on a server, such as an Expo Router API route or a backend, and have the app call your endpoint.
- Restrict the keys that are meant to be public, and confirm the build is clean with a pre-submission scan such as PTKD.com before you submit.




