← Home

Research · October 2026

Your AI API key ships inside your Expo app

The AI feature is the newest part of many React Native and Expo apps, and the easiest place to leak a key. We scanned 869 open-source apps: 12 compile an OpenAI, Gemini, Anthropic or other AI provider key into the app they ship, and every one of the 12 calls the provider straight from the phone.

869open-source React Native and Expo apps scanned
12ship an AI provider key inside the app
8of them through an EXPO_PUBLIC_ variable
0of them had the key in the repository

Why .gitignore does not save you

Every one of these apps did the thing tutorials tell you to do: the key lives in .env, and .env is not committed. The code reads it like this:

const res = await fetch('https://api.groq.com/openai/v1/chat/completions', {
  headers: { Authorization: `Bearer ${process.env.EXPO_PUBLIC_GROQ_API_KEY}` },
  ...
});

Expo replaces process.env.EXPO_PUBLIC_* with the value at build time. The key is not in your repository, but it is a plain string in the JavaScript bundle of every build you ship. Expo's own documentation says not to put secrets in these variables. react-native-config and react-native-dotenv work the same way: they exist to put values into the app.

What someone needs: the APK or IPA (free from the store), a zip tool, and a text search for sk- or AIza. No reverse engineering skills. Automated scrapers do this at scale, and AI keys are what they look for, because a working key turns straight into free compute on your bill.

What we found

ProviderApps
Google Gemini6
OpenAI5
Anthropic2
Groq2
OpenRouter, xAI, Replicate, fal.ai1 each

Several apps ship more than one key. Eight use EXPO_PUBLIC_ variables, three react-native-dotenv, one react-native-config. One of the 12 is published on the app stores. They are mostly recent: chat assistants, journaling with AI summaries, photo and voice features added in the last year.

While checking these results we also found, in a different app, a server credential committed to the repository itself. We told the owner without publishing any details, and we name no app here.

The fix

The key has to live somewhere the user's phone cannot read:

  1. Move the call behind your own endpoint. An Expo API route (app/api/chat+api.ts), a Firebase or Supabase function, or any small server. It holds the key, the app calls it.
  2. Check who is calling. Require your user's session token, and add per-user rate limits. Otherwise your endpoint becomes the free proxy instead of the key.
  3. Revoke the key that shipped. Old builds stay on phones and in APK mirrors. Moving the code does not un-leak the old key.
  4. Set a spending limit at the provider, so a leak costs you a bounded amount.

If users bring their own key, store it with expo-secure-store or the Keychain/Keystore, and do not fall back to a built-in key when theirs is missing. We saw that fallback too.

Find it in your app

Since version 0.1.47, NativeKeel reports this as a high finding. It reads your code, not your secrets: the variable name is enough to know the value is compiled in. Server folders and Expo API routes are left out, because code there never reaches the phone.

▲ HIGH  Groq API key ships inside the app (utils/ai/client.ts:3)
        The app reads EXPO_PUBLIC_GROQ_API_KEY, and EXPO_PUBLIC_ variables are compiled
        into the JavaScript bundle; the app also calls Groq directly. ...

Run it on your app

One command, no signup, nothing leaves your machine:

npx nativekeel
See a sample reportWhat else it checks