DEV Community

Shehryar Nasir
Shehryar Nasir

Posted on

AI gave you a React Native app with API keys in the code. Here's how to fix it

Ask an AI to add an API call to your React Native app and it will often do this:

const response = await fetch('https://api.openai.com/v1/chat/completions', {
  headers: {
    Authorization: 'Bearer sk-proj-abc123yourrealkeyhere',
  },
});
Enter fullscreen mode Exit fullscreen mode

The key is in the source code. It ships in your APK. Anyone who decompiles your app — and plenty of tools do this in seconds — can read it, use it, and run up your bill.

Here's the right way to handle API keys in React Native, and the prompt that gets AI to do it correctly from the start.

Why .env alone isn't enough

The usual advice is "put it in .env". That's correct, but it only solves half the problem.

OPENAI_API_KEY=sk-proj-abc123
Enter fullscreen mode Exit fullscreen mode

With react-native-dotenv or expo-constants, your app reads the key at build time. It's out of your source code — good. But it's still bundled into the JavaScript that ships with your app. The .env file keeps secrets out of git; it doesn't keep them out of the bundle.

For a production app, the key should never reach the device at all.

The correct pattern: a backend proxy

Device → Your backend → OpenAI
Enter fullscreen mode Exit fullscreen mode

Your server holds the key. The app calls your backend. Your backend calls OpenAI. The device never sees the key.

On the device (React Native):

// No API key here — ever
const response = await fetch('https://your-backend.com/api/chat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ message: userMessage }),
});
const data = await response.json();
Enter fullscreen mode Exit fullscreen mode

On the server (Node/Express example):

// This is the only place the key lives
const OPENAI_KEY = process.env.OPENAI_API_KEY;

app.post('/api/chat', async (req, res) => {
  const response = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${OPENAI_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      model: 'gpt-4o',
      messages: [{ role: 'user', content: req.body.message }],
    }),
  });
  const data = await response.json();
  res.json(data);
});
Enter fullscreen mode Exit fullscreen mode

The API key stays on the server. It never touches the bundle.

When you genuinely need a key on the device

Some SDKs (maps, analytics, push notifications) require a key on the device. These are usually low-risk public keys — they identify your app, not authenticate a paying account.

For those, react-native-config or Expo's app.config.js with environment variables at build time is the right approach:

// app.config.js
export default {
  extra: {
    googleMapsKey: process.env.GOOGLE_MAPS_KEY,
  },
};
Enter fullscreen mode Exit fullscreen mode
import Constants from 'expo-constants';
const mapsKey = Constants.expoConfig?.extra?.googleMapsKey;
Enter fullscreen mode Exit fullscreen mode

Still keep these out of git with .env and .gitignore. But know that any key you ship in a bundle can be extracted — only put keys here that you're comfortable being public.

Check your existing app right now

# Build your JS bundle and search it
npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output /tmp/check.bundle
grep -i "sk-|api_key|apikey|secret" /tmp/check.bundle
Enter fullscreen mode Exit fullscreen mode

If you see your keys in that output, they're in your shipped app.

The prompt

Paste this before describing your API integration:

I need to add [describe the API call] to my React Native app.
Rules:
- No API keys in the React Native code or bundle
- If this requires a secret key, give me a minimal Node/Express proxy endpoint
- If the key must be on the device (public SDK key), use react-native-config
- Show me the .gitignore entries for any .env files
Explain where each key lives and why.
Enter fullscreen mode Exit fullscreen mode

The "explain where each key lives" line is the important part. It forces the AI to think through the data flow, and you can catch a wrong call before it ships.

One more thing

Add this to your .gitignore and never remove it:

.env
.env.local
.env.production
*.keystore
google-services.json
GoogleService-Info.plist
Enter fullscreen mode Exit fullscreen mode

A leaked key that's been in git history once is a leaked key. You have to rotate it even after you remove the file.


If you want a full set of prompts that handle security, navigation, offline sync and Play Store submission — I collected the ones I use on every project.

Daily tips: @rn_aibuilder

Top comments (0)