← Blog ·

Where NOT to Store API Keys in a React Native App (and Where You Should)

If you've ever written EXPO_PUBLIC_API_KEY or imported a .env file straight into a React Native component, you probably have a React Native security problem without realizing it. The reason is simple: anything compiled into your JavaScript bundle ships to the user's device, and that device is not a trusted environment.

This isn't theoretical. Anyone with your APK or IPA can unzip it and read the bundle as plain text. Let's go through exactly where developers get React Native API keys wrong, and what actually works instead.

The JavaScript bundle is not a vault

Metro and Hermes compile your code — they don't encrypt it. A .env loaded via react-native-dotenv or react-native-config gets substituted at build time with its literal value, which ends up embedded in the final bundle. It doesn't matter that your backend never sees the .env file — the value is already sitting inside the .apk you upload to Google Play.

Places you should never put an API key

1. Environment variables baked into the client

EXPO_PUBLIC_*, REACT_APP_*, or anything injected by react-native-config ends up in plain text inside the JS bundle. These are fine for public config (a feature flag, an API base URL) — never for secrets.

2. Hardcoded in source

const STRIPE_SECRET_KEY = "sk_live_51H...";

This is the first thing an AI coding assistant suggests when you ask it to "connect this to Stripe," and the first thing an attacker searches for with grep.

3. AsyncStorage or unencrypted filesystem storage

AsyncStorage persists data in SQLite (Android) or a plist (iOS) without real encryption. On a rooted or jailbroken device, it's readable directly.

4. Info.plist, strings.xml, or google-services.json

Developers often add third-party keys here because it "feels like native config." It's still the same binary distributed publicly.

5. Your git history

Even if you delete a key from the current file, it lives on in previous commits. git log -p | grep sk_ finds it in seconds.

Check it yourself

On a release build you've already generated:

# Android
unzip app-release.apk -d app-release
strings app-release/assets/index.android.bundle | grep -Ei "sk_live|AIza|api[_-]?key"

# iOS
strings MyApp.app/MyApp | grep -Ei "sk_live|AIza"

If anything shows up, that key is already compromised the moment you publish the app. Rotating it prevents further damage, but it doesn't erase the copies already downloaded to devices.

Where API keys should actually live

Public keys, client-side — that's fine

A Stripe publishable key, Firebase config, or an analytics ID are designed to be public. The client is exactly the right place for these.

Session tokens, in the Keychain/Keystore

For tokens issued after login (short-lived JWTs, refresh tokens), use react-native-keychain or the native Keystore. It's OS-level encryption built for precisely this.

Third-party secret keys: never on the phone

An OpenAI API key, a database password, or a Stripe secret key should never reach the device at all. If your app needs that service, your backend makes the call — not the client.

The right pattern: backend + secrets manager

Your React Native app calls your API; your API calls OpenAI, Stripe, or your database. The phone never sees those keys. What's left to solve is: where does your backend — and your CI/CD, and the AI assistant writing that backend code — store those keys without them ending up in a committed .env?

That's where Koove fits. Instead of pasting the key into a file, you store it once:

koove set OPENAI_API_KEY sk-proj-xxxx --env prod

The value is encrypted client-side with envelope encryption (X25519 + AES-256-GCM) before it ever leaves your machine; Koove's server only ever stores ciphertext, never the key in the clear. Your backend code drops the embedded secret and just references it by name:

import { koove } from "@koove/sdk";

const apiKey = await koove.get("OPENAI_API_KEY");
const client = new OpenAI({ apiKey });

The key is only decrypted for verified consumers — an authorized backend, or, when the use case genuinely requires it, a mobile consumer validated with App Attest / Play Integrity plus biometrics. Setup details live in /docs/sdk.

Being honest about the limits

Worth being direct here rather than salesy. Koove protects a secret up to the point of download and decryption — it can't make a secret already delivered to a compromised device disappear, and it doesn't make your app unhackable. What it does is dramatically shrink the exposure surface: the secret doesn't live in your repo, doesn't live in the bundle, and doesn't sit in plain text on any server — it only reaches a client that can prove it's legitimate. If your use case genuinely requires the secret to reach the mobile device itself (uncommon, but it happens), attestation raises the bar significantly against emulators or tampered app builds — but once it's decrypted in that device's memory, that device has it.

In the era of AI-generated code

When you ask Copilot or Cursor to "add Stripe integration," the most likely suggestion is a hardcoded key directly in the component — it's the pattern most represented in its training data. AI assistants can use the same CLI (koove set NAME value) to store the secret instead, so your generated code only ever references the name. We go deeper into this specific failure mode in /ai-security.

Quick checklist

  • Any sk_, AIza, Bearer, or password in your bundle? Check with strings before every release.
  • Do third-party keys ever pass through the client? They should only pass through your backend.
  • Are session tokens sitting in AsyncStorage? Move them to the Keychain/Keystore.
  • Is your .env in .gitignore now but was committed before? Check git log -p.
  • Does your AI assistant have free rein to generate code with plaintext secrets? Give it a safe way to store them instead.

If you'd rather stop deciding case-by-case where each key belongs and centralize this with end-to-end encryption, check out the plans at /plans or create your account at /register. For specific questions about how it fits your stack, /faq covers the most common cases.

AI-generated code

Your AI writes the code. Who guards the secrets?

The most common security failure in AI-generated apps is exposed credentials. With Koove, your own assistant stores every token from the CLI — encrypted on your machine, never in the code or the repo.