Brian Marvin
Published September 13, 2026

You Shipped Your Web App as a Mobile App. Now Secure It Like One
A clip making the rounds this week put a common mobile failure bluntly. Capacitor wraps your web app. It does not secure it. Here is the fix list I use with clients.
A short video put the whole problem in about thirty seconds. You ship your web app as a mobile app. Every API key is visible in local storage. Capacitor wraps your web app but does not secure it. Secrets go in the native keychain. Certificate pinning on every call. Deep link validation on every callback. Your web app had a browser protecting it. Your mobile app does not. It is worth repeating because it matches what I see in rescue work.
That maps exactly to what I see in rescue work. A team vibecodes a clean web app, wraps it with Capacitor, ships to both stores in a week, then learns the hard way that the phone is a hostile environment. If you want the longer version of that rescue pattern, read what vibecoding rescue actually fixes and the true cost of vibecoding. This post is the mobile specific checklist.
Capacitor is a wrapper, not a vault
Capacitor puts your HTML, CSS, and JavaScript inside a native shell with a WebView. That shell gives you an icon, push, camera, and a store listing. It does not change what your code is. Your bundle ships with the app. Anyone can unzip it, read it, and grep it.
Local storage, session storage, and anything you hard code in JavaScript are readable on a rooted or jailbroken device, and often without that. OWASP treats this as baseline mobile verification under MASVS. Their testing guide assumes the attacker has the binary. Plan like they do.
I tell clients this plainly. If a secret lives in the web layer, it is public. Price your architecture from that fact.
Rule 1: no long lived secrets in the web layer
The most common find is a Stripe publishable key, a Firebase key, a Maps key, or worse, a private admin key sitting in local storage or in a config file inside the bundle. On web, the browser at least gave you origin isolation and HttpOnly cookies. On device, your JavaScript and its storage travel with the app.
Do this instead:
- Move every privileged call behind your own backend. The app talks to your API with a short lived user token. Your server holds the real keys.
- Store tokens and refresh tokens in native secure storage only. That means iOS Keychain and Android Keystore, through a plugin, not local storage. The Capacitor docs point here directly, and for enterprise cases their Identity Vault product wraps the same native APIs with biometric controls. Community options like the secure storage plugin do the same job with AES in GCM mode backed by the Keystore.
- Scope tokens tight. Short expiry, rotate on refresh, revoke on logout. If a token leaks in a log, it should already be dead.
- Scan the bundle before every release. Grep the built output for anything starting with sk_, service account JSON, and private endpoints. I have caught live admin keys this way more than once.
In my experience, the backend proxy step costs $2k to $6k on an existing API and removes 90 percent of the mobile secret risk in one move.
Rule 2: pin every connection
On web, TLS plus the browser certificate store did quiet work for you. On mobile, a user on hostile WiFi with an installed profile can proxy your traffic unless you explicitly refuse unknown certificates. That is a textbook man in the middle, and pen testers flag it under MSTG NETWORK 3 when your app does not verify the remote X.509 certificate.
Certificate pinning means your app only accepts known server certificates or public keys. Do it natively on both platforms, client and server side. On Android that usually means a network security config with pins. On iOS it means runtime validation plus Info.plist settings. Capacitor projects typically do this through an HTTP plugin that supports pinning, such as the advanced HTTP plugin or the Capacitor SSL pinning plugin, layered on top of the Capacitor HTTP stack.
Two details teams miss:
- Pin a backup key, not just the live one. Certificates rotate. Without a backup pin, a routine renewal bricks your app until the next store release.
- Automate pin updates in CI. When server certificates change, the app code should change in the same pipeline, not as a manual step someone forgets on a Friday.
Treat pinning as one layer of the onion, alongside encryption, input validation, and secure coding. It is not the whole plan. It is the layer that stops the coffee shop attack.
Rule 3: validate every deep link callback
Mobile auth lives and dies on deep links. OAuth callbacks, password resets, invite links, and payment returns all arrive as URLs your app claims. If you accept any URL with your scheme, an attacker can hand you a malicious one.
Validate all three parts on every callback:
- Scheme and host against an explicit allowlist. Reject everything else before you parse anything.
- Path and parameters against what you actually issued. Check state, nonce, and PKCE code verifier on OAuth flows. Reject reused or expired codes.
- Origin of the request where the platform gives you one. Universal Links and App Links with an associated domains file beat custom schemes alone, because the OS verifies ownership.
Log rejected callbacks. In rescue audits, a spike in rejected deep links is often the first sign someone is probing the auth flow.
Your browser is gone, so you become the browser
The video line that sticks with me is the last one. Your web app had a browser protecting it. Your mobile app does not. Here is what you lost and what replaces each piece:
| Browser gave you | Phone gives you | You add |
|---|---|---|
| Origin isolation | One WebView with your files inside | Backend proxy, tight CORS, no cross origin secrets in the bundle |
| HttpOnly, Secure, SameSite cookies | JavaScript readable storage by default | Native keychain storage, short lived tokens |
| HSTS and managed cert store | Any installed profile can intercept | Certificate pinning on every API call |
| Content Security Policy | Injected scripts run if you let them | Lock down allowed sources, review every plugin |
None of this is exotic. It is the OWASP MASVS baseline applied to a Capacitor app instead of a fully native one. The mistake is assuming the wrapper upgrades your web security. It does not. It removes it.
A one sprint shipping checklist
When a client asks me to harden a Capacitor ship, I run this list. It usually fits in one focused sprint, $4k to $10k depending on backend state:
- Move all privileged keys to your backend. App holds a short lived user token only.
- Move token storage to Keychain and Keystore through a secure storage plugin. Delete every auth value from local storage.
- Turn on certificate pinning on every API host, with a backup pin and CI driven rotation.
- Allowlist deep link schemes, hosts, and paths. Enforce state, nonce, and PKCE on auth callbacks.
- Set up Universal Links and App Links with associated domains instead of relying on custom schemes alone.
- Strip secrets from the bundle. Grep the production build for keys, endpoints, and service account blobs.
- Review plugins. Each Capacitor plugin is native code with full device access. Remove what you do not use.
- Run a MASVS pass on a real release build on both platforms before store submission, not on a debug build on a simulator.
That list is also how I quote the work. Storage and proxy changes are days. Pinning plus deep link validation is days. The audit pass at the end is what lets you tell the App Store reviewer and your first enterprise customer the same story with a straight face.
About the Author
I'm Brian Marvin, an AI-native Fractional CTO with 15+ years in technical leadership. At empowered.guru, I help startups build MVPs, shape roadmaps, and make AI-powered technology decisions that scale.
