If an API key is exposed in your Android app, the first move is to treat it as compromised and rotate it immediately, because anyone can decompile an APK and read the value. There is no safe place to hide a real secret inside the app: putting it in BuildConfig, strings.xml, or gradle.properties still compiles the value into the APK, where it is trivially extracted. The durable fix is to remove the secret from the app entirely and route the calls through a backend proxy you control, so the key lives on your server and never ships to the device. Keys designed to be public, paired with server-side rules like Supabase Row Level Security, are the only ones that belong in the app.
Short answer
Rotate the exposed key now, then move it off the device. Per Google's API key best practices, you should not embed keys in code, because a client binary can be inspected; an Android APK decompiles easily, so BuildConfig, strings.xml, and gradle-injected values are all readable. The fix is a backend proxy: your app calls your server, and your server holds the secret and calls the third-party API. Per the OWASP MASVS, secrets must not be stored in the client. Only keys meant to be public belong in the app, and only when a server-side boundary such as Supabase Row Level Security limits what they can do.
Which keys are safe versus unsafe in an Android app
The safe-versus-unsafe distinction comes down to whether a key is a privileged secret or a public identifier. A privileged secret grants access to billed services or private data on its own, and it must never ship in the app. Examples include an OpenAI or other third-party API secret, a Stripe secret key, cloud provider credentials, and a Supabase service_role or secret key that bypasses your database rules. If leaking the key lets someone spend your money or read your data, it is unsafe to embed.
A public identifier is designed to be visible in a client and is safe to ship when it is properly restricted. A Supabase publishable or anon key is meant to be public, but only safe when Row Level Security governs what it can reach, and a client key like Google Maps is safe when restricted to your app's signature and package. The test is what the key can do by itself: if it is powerless without a server-side rule or restriction backing it, it can live in the app; if it carries privilege on its own, it cannot.
Immediate step: rotate the exposed key
The first action is containment through rotation, because an exposed key in a shipped APK is public and cannot be un-shipped. Revoke the compromised key at the provider and issue a new one, so the leaked value stops working even though it remains readable in copies of your app that are already out. Do this before anything else, since the app remains vulnerable for as long as the old key is valid, and decompiling a published APK takes minutes.
Alongside rotating, check for abuse. Review the provider's usage and billing dashboards for unexpected activity that suggests the key was already found and used, and set or tighten spending limits and alerts so a future leak is capped. Rotation stops the bleeding, but it is only the first half: if you reissue a secret and put the new one back into the app the same way, you have simply exposed a fresh key. Rotation must be paired with actually moving the secret off the device, which is the architectural fix below.
Why BuildConfig and strings.xml will not hide it
A common mistake is believing that moving a key out of source code and into BuildConfig, strings.xml, or gradle.properties protects it, but it does not, because all of these end up in the APK. BuildConfig fields are compiled into the app as constants, string resources are packaged in the APK, and values injected at build time from gradle.properties or local.properties are baked into the compiled output. Any of them can be recovered by decompiling the APK with common tools, so the key is readable to anyone who looks.
Obfuscation does not save you either. Code shrinkers and obfuscators rename symbols but do not reliably hide string literals, so an obfuscated APK still yields the key with a string search. Even native code in the NDK only raises the effort slightly, since the value can be extracted from the compiled library or observed at runtime. The honest conclusion is that there is no location inside the app where a real secret is safe, because everything the app can read at runtime, an attacker with the APK can read too. This is why the fix is architectural, not a better hiding spot.
The fix: a backend proxy
The durable fix is to put the secret on a backend and have your app call that backend instead of the third-party API directly. In this pattern, your app sends its request to a small server you control, that server holds the API secret and makes the privileged call to the third party, and it returns only the result to the app. The secret never leaves your server, so decompiling the app reveals nothing useful, and rotating the key is a server change rather than an app update.
Build the proxy with its own protections, since it is now an internet-facing endpoint. Authenticate your app's users so the proxy is not an open relay anyone can call, add rate limiting so a leaked user credential cannot run up unlimited cost, and scope each request to what that user is allowed to do. This is more work than embedding a key, but it is the only approach that actually keeps the secret secret, and it gives you a single place to monitor usage, enforce limits, and rotate keys without shipping a new build.
Supabase and the Row Level Security boundary
Supabase is a common case in AI-built apps, and it illustrates the safe-versus-unsafe boundary clearly. The publishable or anon key is meant to be included in your app, but it is only safe because Row Level Security policies on your tables decide what any request using that key can read or write. Without Row Level Security enabled and written correctly, the anon key can reach data it should not, which is how many exposed-Supabase incidents happen: the key was public by design, but the database had no server-side rules limiting it.
The service_role or secret key is the opposite. It bypasses Row Level Security entirely and has full access, so it must live only on a server, never in your Android app. If a service_role key is in your app, treat it as a serious exposure: rotate it immediately and move it to your backend. The general lesson applies beyond Supabase: a client-safe key is safe only because a server-side boundary constrains it, so enable and verify that boundary, and keep the privileged key server-side.
Verify your APK is clean
After rotating and re-architecting, verify rather than assume, because a stray copy of the key can remain. Build the release APK and inspect it the way an attacker would: decompile it with a tool such as jadx or apktool and search the output for the key patterns, your provider prefixes, and terms like secret or token. Also scan your source repository and its history with a secret scanner, since a key removed from the current code may still sit in an old commit that anyone with repo access can read.
Make this a repeatable check rather than a one-time cleanup. Run a secret scan in your CI pipeline so a newly introduced key is caught before it ships, and re-inspect the APK when you change how configuration is injected. The goal is to confirm that the build you distribute contains no privileged secret and that only properly restricted public keys remain. Verification closes the loop: rotation stops the old key, the backend keeps new secrets off the device, and scanning proves the result.
Safe versus unsafe keys
Sorting your keys into the two categories tells you what to move. The table below compares them.
| Key type | Ship in the app? | Why |
|---|---|---|
| Supabase publishable or anon key | Yes, only with Row Level Security | A public identifier limited by server-side rules |
| Restricted client key, such as Maps | Yes, when restricted to your app | Scoped to your signature and package |
| OpenAI or third-party API secret | No | Grants billed, privileged access on its own |
| Stripe secret key | No | Full account access |
| Supabase service_role or secret key | No | Bypasses Row Level Security, server-only |
Read the table by what a key can do alone: if it is powerless without a server-side rule, it can ship; if it carries privilege by itself, it must not.
Remediation checklist
Working through the steps in order contains the exposure and prevents the next one. The checklist below covers them.
| Step | Action | Done? |
|---|---|---|
| Rotate now | Revoke the exposed key and issue a new one | [ ] |
| Check abuse | Review usage and billing, set limits | [ ] |
| Remove from app | Delete it from BuildConfig, strings.xml, gradle | [ ] |
| Add a backend proxy | Route the calls through your server | [ ] |
| Restrict remaining keys | Scope client keys, enable Row Level Security | [ ] |
| Verify | Decompile the APK and scan the repo for secrets | [ ] |
The step people skip is the fourth: without a backend proxy, rotating just exposes a new key, so moving the secret off the device is what actually fixes it.
Scan your build for exposed keys
Because a leaked key is often hard to spot by eye, especially when a build tool or an AI code generator injected it, scanning the build is the reliable way to know what your app actually contains. AI app builders in particular have a habit of hardcoding provider keys directly into the client, so the exposure may not be something you wrote.
A scanner like PTKD.com analyzes your build and reports issues such as leaked keys and secrets, insecure data storage, and over-broad permissions by severity, mapped to OWASP MASVS, so you can find an embedded key before it reaches users or, after a leak, confirm your rebuilt app is clean. To be clear about the boundary: PTKD does not rotate your keys at the provider or build your backend proxy. It finds the exposure so you know what to rotate and remove.
What to take away
- Treat an exposed API key as compromised and rotate it immediately, because a published APK is public and decompiles in minutes.
- There is no safe place to hide a real secret in the app: BuildConfig, strings.xml, gradle-injected values, obfuscation, and even native code are all recoverable.
- Fix it architecturally with a backend proxy, so the secret lives on your server, your app calls the server, and rotating a key is a server change.
- Only keys designed to be public belong in the app, and only when a server-side boundary such as Supabase Row Level Security limits what they can do.
- Verify the result by decompiling your APK and scanning your repo, and use a tool like PTKD.com to catch embedded keys before they ship.



