Mobile Security: Why Secrets Don’t Belong on Devices
Mobile binaries can be decompiled. Any credential embedded in the app is effectively public. Production mobile security means the device proves identity through cryptographic challenges, not stored secrets.
Why can’t mobile apps keep secrets?
Mobile apps can’t keep secrets. Unlike server-side applications, a mobile binary can be decompiled, reverse-engineered, and instrumented. Any client secret, API key, or credential embedded in the app is effectively public to anyone willing to spend 15 minutes with a decompilation tool.
This isn’t a theoretical concern — it’s a structural reality of distributing code to devices you don’t control. The solution isn’t better obfuscation (attackers are better at deobfuscation). It’s eliminating secrets from the device entirely and replacing static credentials with one-time cryptographic proofs.
PKCE — Proof Key for Code Exchange — replaces the client secret with a cryptographic challenge that’s generated fresh for each authorization attempt. The app proves it initiated the request by presenting a value that only it could know, because it generated it moments ago. A stolen authorization code is useless without the original proof that never left the device.
Where does the device-server trust boundary belong?
The most important security decision in mobile architecture is drawing the trust boundary: what lives on the device, and what lives on the server.
In our architecture, exactly three things never touch the device: the OAuth client secret, AI service API keys, and any service account credentials. The mobile app only holds tokens (stored in the platform’s hardware-backed secure storage) and public-facing client identifiers. All credential-dependent operations route through server-side edge functions that add the secrets the device doesn’t have. (The same boundary, stated as doctrine: security as architecture.)
We enforce this with a compile-time assertion: if a server-only credential appears in a release build, the app crashes on startup with a clear error. CI/CD configurations change. Developers make mistakes. Feature branches merge with test values. The assertion catches what process discipline misses.
Warning: Never trust that 'we just won’t set that variable in release builds.' Add a compile-time or startup assertion that fails loudly if a server-only secret appears in a client build. It will catch a misconfiguration that process discipline won’t.
How do you manage tokens across concurrent operations?
Tokens expire. In a mobile app, expiration during an active operation creates a jarring experience — and if the refresh fails, the user is logged out mid-task. We refresh proactively before expiration, not reactively after it. The user never sees an expired token, and we have a buffer window to handle refresh failures gracefully.
Concurrent operations create a subtler problem. Multiple API calls in flight simultaneously may all detect an expiring token at the same time. Without coordination, each would trigger its own refresh — and most OAuth providers invalidate the first refresh token when the second is used, causing a cascade of failures. We use a coordination mechanism where the first caller to detect expiration starts the refresh, and all subsequent callers wait for the same result.
All tokens and encryption keys are stored in the platform’s native secure storage — iOS Keychain and Android Keystore — which are hardware-backed on modern devices. The encryption key for the local database is generated once per installation using the platform’s secure random generator. If the app is uninstalled, the key is deleted and the encrypted database is unrecoverable. That’s the correct behavior for PII on a mobile device. The database that key protects is described in the offline-first architecture.
App Development
Focused applications, fixed scope, handed over clean — code, docs, infra all yours.
See how engagements workTalk2CRM — Voice-to-CRM Mobile App
Mobile app that turns sales calls into CRM records. AI extracts contacts, deal values, and commitments from the call so reps stop typing afterward — with two-layer PII redaction before anything is processed. In beta now.
Want to see more patterns from production?
See the past work where these patterns run in production, or browse the rest of the library.