Hide AI API Keys in Mobile and Web Apps: How We Do It

To hide AI API keys in a mobile app, keep them off the device. How NoteTube keeps OpenAI, Gemini and Claude keys in one FastAPI service across five surfaces.

A smartphone displaying a security lock icon on a wooden desk with a succulent
Photo by Dan Nelson / Unsplash

You can't hide AI API keys in a mobile app or a web bundle, because anything shipped to a device can be read by its owner. The fix is to keep the keys off the device entirely: put every model call behind a small server you control, give clients a short-lived user token, and let that server hold the keys.

TL;DR: Keys compiled into client code can be extracted, as Google's own Gemini docs warn. Route every AI request through a backend service that holds the provider keys, checks a revocable user token, applies plan limits, and records cost. NoteTube does this with one FastAPI service shared by web, iOS, Android, a Chrome extension, and an MCP server.

This post is the pattern we use and why each piece is there, with NoteTube as the worked example. NoteTube runs on five surfaces (web, iOS, Android, a Chrome extension, and a remote MCP server) and lets each conversation pick a model from five providers: OpenAI, Gemini, Claude, Llama, and Mistral. The keys for all of them live in one separate FastAPI service. No client ever sees them.

Why can't you hide an AI API key in a mobile app?

A mobile or web app can't keep a secret because the user holds the whole program. Web code is sent to every visitor, and a mobile binary can be downloaded and unpacked. Whatever key the app needs to call a provider directly is sitting in that code, waiting for someone to look.

The vendors say this plainly. Google's Gemini documentation tells developers not to hardcode keys in web or mobile apps, because keys compiled into client-side code can be extracted by users, and it recommends a backend proxy instead (Gemini API docs). OWASP's mobile Top 10 rates hardcoded credentials as easy to detect with publicly available tools (OWASP Mobile Top 10, M1).

Build tooling won't save you either. Next.js inlines any NEXT_PUBLIC_ variable into the JavaScript sent to the browser at build time (Next.js docs). Expo says EXPO_PUBLIC_ variables are visible in plain text in the compiled app and should never hold secrets (Expo docs). Environment variables keep keys out of your repository. They don't keep them out of the binary.

What actually goes wrong when a key leaks?

Three things, and the third one bites even if nobody steals anything:

  1. Someone else runs their workload on your account, and you find out from the invoice.
  2. You can't revoke the key without breaking every installed copy of the app that still carries it, because mobile users don't all update on the same day.
  3. You have no per-user control. The provider sees one key, so it can't tell a paying customer from a stranger, and you can't say "free users get ten questions a day".

Point two is the one teams underestimate. On the web you can rotate a key and redeploy in minutes. On mobile, the old key lives on in every phone that hasn't updated, so rotating it means choosing between an open leak and a broken app.

The pattern: a thin AI service that owns the AI API keys

The fix is boring on purpose. Clients talk to your backend. Your backend talks to the model providers. Nothing else changes for the user.

Layer Holds Does
Web, mobile, extension, MCP A user session token Sends the question, shows the streamed answer
AI service (FastAPI) Provider keys, retrieval index access Checks the token, applies plan limits, builds the prompt, calls the model, streams back
Model providers Nothing of yours beyond the request Return completions

In NoteTube the AI service is its own FastAPI deployment, separate from the rest of the backend. It runs retrieval over the student's own material in Qdrant, scoped per user and per session, then streams the answer back. Because it's the only thing that can reach the providers, it's also the only place that needs to know about them. Adding a provider or swapping a model is a change to one service, not a release of four clients. That's paid off more than the security did, honestly.

Why FastAPI for this piece? The work is mostly waiting: on the vector store, on the model, on an open stream. Async request handling suits that, and Python is where the AI libraries live. We lay out the trade-offs against a Node service on the FastAPI vs Node.js page.

What should the client send instead of a key?

The client should send a user token issued by your own auth system, never a provider credential. The AI service verifies that token on every request, so access is tied to one account, expires on its own, and can be revoked from the server without shipping an app update.

On each request the AI service does six things, in this order:

  1. Verifies the token's signature and expiry.
  2. Looks up the user's plan and current usage.
  3. Rejects the request with a clear error if they're over the limit.
  4. Scopes retrieval to that user's data only.
  5. Calls the model with the server-side key.
  6. Records tokens and cost against the user.

A stolen user token is a much smaller problem than a stolen provider key. It expires. It's tied to one account, so the damage stops at that account's plan. And you can kill it server-side today.

If one feature needs an even narrower credential, mint a token scoped to that action that expires in minutes. OpenAI's Realtime API works this way: your server creates an ephemeral client secret, and the browser or mobile client uses that instead of your API key (OpenAI Realtime docs). NoteTube's MCP server follows the same idea with OAuth and audience-checked tokens, which we cover in building a remote MCP server.

Does a server hop make streaming slower?

Not in a way users notice, in our experience. The AI service opens a streaming request to the provider and forwards each chunk as it arrives, so the first words show up about when they would from a direct call. The extra hop is one server-to-server call inside a request that spends most of its time waiting on the model.

We haven't published a latency measurement for that hop, so we won't quote one. What we can say is that the things that make streaming feel slow are almost always elsewhere: a proxy that buffers the whole response, or a stream nobody closes. NoteTube uses server-sent events with inactivity timeouts so dead connections don't hold resources. The full setup, with code, is in streaming AI chat with server-sent events in FastAPI.

Cost control comes with the server

Once every call passes through one service, cost becomes something you can see and limit. The same service that holds the keys can record tokens per user and per feature, enforce plan limits, and pick the model on the server, so a client can't quietly ask for the most expensive one.

That last point matters in NoteTube, where the conversation's model choice is enforced server-side across all five providers. None of this is possible when the client calls a provider directly. How to turn those records into a lower bill is its own topic, covered in how to control LLM costs.

How do you fix an app that already ships a key?

Fix the server first, then the clients, then the key. If you rotate the key before new clients exist, you break everyone at once. If you never rotate it, the leaked key stays live forever. Here's the order I'd follow on an existing app:

  1. Add the server route. Build the AI endpoint on your backend and point a new app version at it.
  2. Create a new provider key that only the server knows. Don't reuse the leaked one.
  3. Release the new client on the web straight away and through the stores for mobile.
  4. Cap the old key. Set a spending limit or tight restrictions on it while old app versions die off.
  5. Revoke the old key once traffic from old versions is low enough to accept the breakage. Show those users an "update the app" screen, not a silent failure.
  6. Search repositories and build logs for the old key, and rotate anything stored next to it.

For mobile, a forced-update screen driven by your backend is worth building on day one, before you need it. It turns "we have to wait for users to update" into "we can require it". Without one, step five above can take months.

Are API key restrictions enough on their own?

No. Restrictions help, but they aren't the protection. Google's docs say adding restrictions to a key minimizes the damage if it's compromised (Gemini API docs). That's damage limitation. An app restriction checks values a client sends, and anything a client sends can be copied by someone who has the app.

Treat restrictions as a second layer, useful on the server key too. They don't replace keeping the key off the device.

On-device models: when the problem goes away

Some features can run on the phone with a small local model, and then there's no key at all. That's a real option for narrow tasks like short classification or simple text cleanup. For anything that needs a large hosted model, retrieval over a big index, or a provider you pay per token, the server pattern above is still the answer.

Where does the AI service fit in a larger backend?

It's usually one piece of a mobile app backend that also handles accounts, billing, and push. Keeping the AI part separate has a practical benefit beyond security: it scales and deploys on its own schedule, and a slow model call can't tie up the endpoints that log people in.

That separation is the decision we'd repeat first. When a provider has a bad hour, NoteTube's chat slows down, and sign-in and billing carry on as normal.

FAQ

Can obfuscation hide an API key in an Android or iOS app?

Obfuscation slows a person down but doesn't stop them. The app still has to turn the key back into plain text to send it, and at that moment it can be read from memory or from the network traffic. Anyone patient enough will get it, so obfuscation buys time, not safety.

Is a Firebase or Supabase client key the same kind of secret?

No. Those public client keys are designed to ship in apps, and security comes from rules or row-level policies on the server. A model provider key is different: it's a billing credential with no per-user rules behind it. Never treat a provider key like a public client key just because both are called keys.

What should the AI service log for each request?

Log who asked, which model, how many tokens, the cost, and how long it took. Avoid storing full prompts and answers unless you have a clear reason and a retention rule, because they often contain personal data your users didn't expect you to keep.

How fast should we rotate a provider key after a leak?

Cap it right away with a spending limit, then revoke it as soon as new clients are live. On the web that can happen the same day. On mobile, revoke once traffic from old app versions is low enough that a forced-update screen is an acceptable experience for the users still on them.

Building an AI feature into a web or mobile app and not sure where the keys should live? See how we structure a FastAPI backend for AI apps, then book a 30-minute architecture call and bring your current setup.