●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Never Embed Your Gemini API Key in a Mobile App: Complete Multi-Layer Security Architecture with Firebase App Check
A production-grade guide to securing Gemini API access in mobile apps. Covers Firebase App Check, Cloud Functions proxy, rate limiting, and anomaly detection — with complete iOS and Android code examples.
Shipping a mobile app that uses Gemini API is exciting — until you wake up to a billing alert from Google Cloud.
It happens more often than you might think. A developer embeds an API key directly in their Android APK or iOS IPA, someone extracts it using a decompiler or proxy tool, and within hours the key is making thousands of requests billed to the developer's account. I learned this lesson the hard way on a side project: a test key I left in a debug build ended up costing me $300 before I caught it.
This guide walks you through an architecture that eliminates that risk entirely. The approach: Firebase App Check as a device authenticator, a Cloud Functions backend as the only place your Gemini API key lives, and layered defense with rate limiting and anomaly detection. Your client apps never see the API key. Under any code path. Ever.
This is a premium guide, so we go deep: complete, production-ready implementations for both iOS (SwiftUI) and Android (Kotlin), not simplified pseudocode. By the end, you'll have everything you need to deploy and start protecting a real app.
Why Direct API Key Embedding Fails
Mobile apps are fundamentally more exposed than web backends. Three attack vectors matter in practice.
Binary decompilation. Android APKs are ZIP archives. Tools like apktool decompress them and extract resource strings and decompiled bytecode with minimal effort. iOS IPA files are similarly reversible — Mach-O binary analysis tools can locate string constants. If your API key exists anywhere as a literal in your app bundle, it can be found by anyone who downloads your app.
Network traffic interception. Running Charles Proxy or mitmproxy on the same network as an iOS simulator or an Android emulator with a user-installed CA certificate captures every HTTPS request your app makes — including the Authorization: Bearer YOUR_API_KEY header — unless you've implemented certificate pinning correctly. Most apps don't.
Repository accidents. Committing a config file containing a real API key to a public GitHub repository is still one of the most common exposure events. GitHub's secret scanning helps catch this, but the window between push and detection is long enough for automated scrapers to find and abuse the key.
The wrong fix is obfuscation — encoding your key in base64, splitting it across multiple strings, using native code to reconstruct it. All of these can be reversed. The right fix is not putting the key in the app at all.
Architecture Overview: Three Layers
The complete system uses three layers that work together.
Layer 1 — Firebase App Check (device authentication). App Check verifies that each request comes from a genuine, unmodified iOS or Android app running on a real device. It does this using platform-specific attestation APIs: Play Integrity on Android (which checks device integrity, app signing, and licensing) and DeviceCheck or App Attest on iOS (which uses hardware keys to prove device authenticity). Bots, emulators, and apps repackaged with modified code fail these checks.
Layer 2 — Cloud Functions proxy (API key isolation). Your Gemini API key exists only in Cloud Functions environment variables, stored encrypted in Google Secret Manager. The mobile app sends a prompt and an App Check token to the function. The function verifies the token, then calls Gemini API on the app's behalf. The key never travels to the client.
Layer 3 — Rate limiting + anomaly detection (cost control). Per-user request quotas stored in Firestore prevent any single account — even a legitimate one — from running up unexpected costs. An anomaly detector catches repeated identical prompts, which is a common pattern in scripted abuse.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You'll implement a Firebase App Check-backed architecture that keeps your Gemini API key off client devices entirely — protecting you from key theft and unexpected billing surprises
✦You'll learn how to block bots, emulators, and tampered apps using Google Play Integrity and Apple DeviceCheck, with copy-paste-ready Cloud Functions code for rate limiting and anomaly detection
✦You'll set up per-user usage tracking in Firestore and Google Cloud budget alerts, giving you full cost visibility and automatic protection before a billing surprise ruins your month
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Before implementation, pick the attestation provider that fits your deployment.
Android: Play Integrity API. The recommended provider for production Android apps. Verifies device integrity (is this a genuine Android device?), app integrity (has the APK been modified?), and account integrity (is this a licensed copy from Play Store?). Requires your app to be published to Google Play — not usable for sideloaded or enterprise-only apps.
Android alternative: Safety Net (legacy). Older provider, being deprecated in favor of Play Integrity. Don't use for new projects.
iOS: App Attest (recommended). Introduced in iOS 14, App Attest uses a hardware-backed key on the Secure Enclave to cryptographically prove that requests come from an unmodified app on a genuine Apple device. Stronger than DeviceCheck.
iOS fallback: DeviceCheck. Supported on iOS 11+. Uses a hardware key to identify the device but doesn't verify app integrity as strongly as App Attest. Use App Attest where possible; fall back to DeviceCheck for older iOS versions.
For most indie developers shipping a consumer app to both stores, the combination is: Play Integrity on Android + App Attest on iOS, with DeviceCheck as a fallback.
Step 1: Firebase Project Setup
Enable App Check in the Firebase console:
Open Firebase Console → App Check → Apps
Click on your iOS app → Register → Choose App Attest
Click on your Android app → Register → Choose Play Integrity
Set your Gemini API key as a Cloud Functions secret (this stores it encrypted in Google Secret Manager, not in your source code):
# Install Firebase CLI if needednpm install -g firebase-toolsfirebase login# Initialize Cloud Functions in your projectfirebase init functions # Select TypeScript or JavaScript# Set the Gemini API key as an encrypted secretfirebase functions:secrets:set GEMINI_API_KEY# You'll be prompted to enter the key value — it's never stored in plaintext
Install backend dependencies:
cd functionsnpm install @google/generative-ai firebase-admin firebase-functions
Register it in Firebase Console → App Check → [Your iOS App] → Debug tokens. Debug tokens only work in debug builds — production builds use the hardware-backed attestation provider.
Register this in Firebase Console → App Check → [Your Android App] → Debug tokens.
Step 5: Testing the Complete System
Test all three layers systematically before shipping.
Unit testing the Cloud Function locally:
cd functionsnpm install --save-dev @firebase/functions-test# Set a fake GEMINI_API_KEY for local testingexport GEMINI_API_KEY="test-key-not-real"# Start Functions emulatorfirebase emulators:start --only functions,firestore
Don't cache App Check tokens yourself. The Firebase SDK already caches tokens and refreshes them before expiry. If you store the token in a singleton or UserDefaults, you'll serve expired tokens and get 403s after an hour. Call token(forcingRefresh: false) fresh before every request.
Don't set minInstances: 0 for user-facing features. Cold starts (500ms–2s) are noticeable in chat interfaces. If budget allows, set minInstances: 1. The cost is roughly $10–15/month for one always-warm instance in Tokyo — often worth it for the UX improvement.
Don't use the same rate-limit collection for multiple functions. If you add more Cloud Functions later (image generation, speech-to-text, etc.), give each its own Firestore subcollection for rate-limit tracking. Sharing a single collection across functions makes it impossible to tune limits per capability.
Do verify debug vs. production providers at build time. Use #if DEBUG / BuildConfig.DEBUG rigorously. Shipping a production build with AppCheckDebugProviderFactory means any debug token can access your backend — completely defeating the purpose of App Check.
Optional Layer: Certificate Pinning
App Check handles device and app authenticity. A separate but complementary technique is certificate pinning, which prevents network interception even on devices where the user has installed a rogue CA certificate (the setup required for Charles Proxy or mitmproxy to intercept HTTPS).
Certificate pinning means your app only accepts HTTPS responses signed by a specific certificate or public key, not just any CA-trusted cert. If someone intercepts your traffic with a self-signed CA, the pinned check fails and the connection is dropped.
iOS (URLSession with pinning):
// CertificatePinningDelegate.swiftimport Foundationimport CryptoKitclass PinnedSessionDelegate: NSObject, URLSessionDelegate { // SHA-256 hash of your server's public key (get from: openssl s_client -connect // asia-northeast1-YOUR_PROJECT.cloudfunctions.net:443 | openssl x509 -pubkey -noout | // openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | base64) private let pinnedKeyHash = "YOUR_BASE64_SHA256_PUBLIC_KEY_HASH" func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let serverTrust = challenge.protectionSpace.serverTrust, let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0), let publicKey = SecCertificateCopyKey(serverCert), let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data? else { completionHandler(.cancelAuthenticationChallenge, nil) return } let hash = SHA256.hash(data: publicKeyData) let hashBase64 = Data(hash).base64EncodedString() if hashBase64 == pinnedKeyHash { completionHandler(.useCredential, URLCredential(trust: serverTrust)) } else { completionHandler(.cancelAuthenticationChallenge, nil) } }}// Usage in GeminiService:// let session = URLSession(configuration: .default, delegate: PinnedSessionDelegate(), delegateQueue: nil)// let (data, response) = try await session.data(for: request)
Certificate pinning is optional for most indie apps — App Check already closes the main attack vectors. Add pinning if your app handles particularly sensitive data or if you operate in an environment where users might deliberately run proxies (enterprise MDM, security research apps).
Scaling and Multi-Region Considerations
For apps with users across multiple continents, latency from a single Tokyo Cloud Functions region can be significant. Consider deploying the proxy to multiple regions and routing users to the nearest one.
On the client, detect the user's locale and select the nearest endpoint:
// Endpoint selection by locale (simplified)func backendURL() -> String { let languageCode = Locale.current.language.languageCode?.identifier ?? "en" let regionCode = Locale.current.region?.identifier ?? "" if ["ja", "ko", "zh"].contains(languageCode) || ["JP", "KR", "CN", "TW"].contains(regionCode) { return "https://asia-northeast1-YOUR_PROJECT.cloudfunctions.net/geminiProxyAsia" } else if Locale.current.region?.identifier.hasPrefix("US") == true { return "https://us-central1-YOUR_PROJECT.cloudfunctions.net/geminiProxyUS" } else { return "https://europe-west1-YOUR_PROJECT.cloudfunctions.net/geminiProxyEU" }}
Rate-limit state in Firestore is automatically available across all regions — no synchronization needed. The ratelimits and anomaly collections are globally consistent.
Next Steps
Start here: deploy the geminiProxy function with a test key, call it from Postman with a manually obtained debug token, and verify you get a 200 response. Then add Firebase to your iOS and Android apps, enable App Check, and verify that requests without a valid token are rejected.
The final step: remove any Gemini API key from your client code and search for strays:
Nothing found? You're done. Your key is server-side only.
For more on mobile Gemini integration, see the SwiftUI production app guide and the mobile AI features masterguide. The architecture in this guide applies to any sensitive API credential in a mobile app — once it's in place, protecting additional keys is straightforward.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.