Every web-to-mobile project I've been on had the same scoping meeting. Someone opens with "we already have the API, so mobile is mostly UI," the room nods, and a quarter gets pencilled in.
That sentence hides four things: the read contract your web app grew up with, the auth model that lives in a cookie, the OS plumbing a browser gave you for free, and a release cycle where "ship the fix" meant a redeploy, not a store review.
Most of the work lands behind the screens. The mobile client is the cheap part; what costs money is everything the web app was allowed to assume. This is about re-negotiating those assumptions, with shipped projects as evidence.
Decide what parity means before you scope
"Parity" is the default nobody chooses. It arrives because nobody wrote down a narrower target, and it quietly becomes the acceptance criterion.
There are three defensible targets, and you should pick one out loud:
- Companion app. Read everything, do one core action (approve a payment, sign a transaction, check a balance). Cheapest, and often the right first release.
- Functional parity. Every flow on web works on mobile. This is what most stakeholders mean when they say "the app."
- Mobile-first rebuild. Mobile is a net-new surface with its own flows, and web follows later.
Two projects show the ends of that spectrum. On Moneygate, a payment provider regulated by the Central Bank of Cyprus, the brief was explicitly a mobile app replicating all the functionality of the web platform: payments, multi-currency, fraud prevention, reporting, analytics.
Parity was a stated requirement, and a five-person team delivered it: two React Native devs, a QA engineer, a designer and a business analyst. The BA mattered; the case page counts more than 22 business flows and features that had to be documented before they could be scoped.
Ctrl, the wallet formerly known as XDEFI, went the other way. The product existed only as a web version and a browser extension, and "no mobile access" was the first problem on the list. Mobile there was new surface, not a port.
One limitation to accept up front: parity is measured in flows, not features. A web screen with six optional filters and a CSV export is not one mobile screen; it's three, plus a decision about which filter combinations deserve a bottom sheet.
Your web API is the first thing to break
Web APIs evolve under assumptions mobile invalidates:
- Many small round trips are fine, because the tab sits on Wi-Fi.
- Large payloads are fine, because the page renders whatever arrives.
- Offline is a non-case; the user reloads.
- Old API versions die the moment you redeploy, because every client reloads on the next visit.
Mobile invalidates all four, and the last one hurts most. The moment you ship a binary, you own a population of clients you cannot force-upgrade.
In June 2025 Apple's own numbers put iOS 18 on 82% of all active iPhones, nine months after release: one in five devices still on an older OS. Android is worse; April 2026 Statcounter data compiled by apilevels shows Android 11 or newer on 86.9% of devices, with the newest release at 22.3%.
Your app's update curve follows the same shape, so a /v1 endpoint you'd love to delete will get traffic for a year.
Chatty endpoints, cold starts and the unified read
The practical fix is server-side aggregation and deliberate versioning. Android vitals flags a cold start of 5 seconds or longer as excessive, and a home screen that fans out to eight REST calls on a cellular connection eats that budget before the first balance renders.
DEFI Wallet, a non-custodial multichain wallet spanning 20 blockchains, is the clearest example of building the read contract once. Its backend is a set of microservices behind a GraphQL router that combines their schemas: blockchain indexers for real-time transactions, a Python service for token metadata, and Node.js/Nest.js Asset and NFT balance services.
That single graph serves the browser extensions (Chrome, Edge, Opera, Brave), the web app and the iOS/Android apps. Mode, a UK-listed crypto company with GBP, EUR and BTC accounts, used microservices for the same reason across its mobile backend and admin portal, exposing GraphQL to both.
What that looks like from the client side is one query per screen, not one per widget:
One round trip for the portfolio screen; the router fans out server-side
query PortfolioScreen($account: ID!, $fiat: Currency!) {
account(id: $account) {
balances(fiat: $fiat) { asset { symbol chain } amount fiatValue }
nfts(first: 20) { edges { node { id name imageUrl } } }
recentTransactions(first: 10) { hash status timestamp }
}
}
The thing to look at is the first: arguments. Pagination limits that were optional on web become mandatory on mobile, because the alternative is a 4 MB response on a train.
The limitation is real, though. A unified API is a coupling decision: one aggregation layer serving three clients becomes the bottleneck every team queues behind, and a schema change for the extension now has a mobile review attached to it.
In the 2024 State of React Native survey, 59.3% of respondents use TanStack Query and 34.2% use Apollo Client. Whichever you pick, the server contract decides how many requests a screen makes, not the client library.
State management doesn't survive the trip
Redux-shaped web state is tuned for a tab that reloads on demand. A mobile app backgrounds, loses the network in a lift, and resumes four hours later with a store full of stale balances and an expired token. The state model has to separate two things the web app blurred:
- Server state: cache, refetch, invalidation, background sync. Belongs to a query library.
- Client state: navigation, form input, UI toggles. Belongs to a small store, persisted deliberately.
Moneygate used React Query for server state under a server-state pattern, with React Hook Form for input. Ctrl runs Redux Toolkit with Redux Persist in the extension but Zustand in the React Native app. DEFI Wallet made the same split: Redux on web, Zustand on mobile.
That is three teams independently accepting two state libraries in one product. They're not alone: Redux is still used by 71% of React Native respondents and Zustand by 46.7%, with Redux complexity the third most-cited pain point. Two stores is a maintenance tax; the case evidence shows teams paying it, not that it's free.
The part that bit us was hydration ordering. A persisted store that rehydrates after the first render shows a logged-out screen for one frame, then flips, and on a payments app that flash is a support ticket. Gate rendering on hydration explicitly:
// Zustand + persist: don't render auth-dependent screens until the store is back
export const useSession = create()(
persist(
(set) => ({ token: null, hydrated: false, setToken: (t) => set({ token: t }) }),
{
name: 'session',
storage: createJSONStorage(() => secureStorage), // not AsyncStorage for tokens
onRehydrateStorage: () => (state) => state && (state.hydrated = true),
},
),
);
The comment on storage is the point. AsyncStorage is used by 83.8% of respondents in the same survey, and it is plaintext. Session material goes in the keychain or keystore; everything else can go in AsyncStorage or MMKV.
Platform plumbing is the backlog nobody writes down
A browser handed the web app downloads, PDF viewing, printing and camera access, and none of it appeared in a ticket. On mobile each is an integration with its own permission model and OS-version behaviour. Budget them as features, not tasks.
Files, PDFs and the Android storage rules
Moneygate's challenge list included secure file downloads on Android and PDF preview with actions. Neither is exotic on web. On Android, apps targeting Android 11 or later run under scoped storage; the system ignores the requestLegacyExternalStorage flag, WRITE_EXTERNAL_STORAGE grants nothing extra, the Storage Access Framework refuses ACTION_OPEN_DOCUMENT_TREE on the Download directory, and declaring All Files Access "can affect your ability to publish and update your app on Google Play."
So the web app's became, on Moneygate:
- expo-file-system with the Storage Access Framework for user-directed saves.
- rn-pdf-reader-js for in-app PDF preview with actions.
- react-native-gesture-handler plus bottom-sheet for full-screen modals replacing traditional buttons.
- Reanimated for animation performance, because a statement list that stutters reads as a broken app.
Soapbox, a fulfilment-tech company's Android warehouse app, hit the other classic: React Native library compatibility problems implementing barcode scanning, settled with vision-camera-code-scanner and react-native-print.
Every one of those libraries carries native code, narrows your upgrade path and becomes a dependency you own through every SDK bump. The 2024 survey put New Architecture adoption at almost 50% with library compatibility the standing concern; the PDF viewer you picked in year one is exactly what blocks you in year two.
Biometrics are an unlock, not an authentication
This is the argument I have most often with product owners. Face ID does not log a user in. It gates something on the device, and the server should never learn or care that a biometric happened.
Treat it as a local factor:
- The server issues a session or refresh token after real authentication (password, passkey, OTP, whatever your risk model requires).
- The token sits in the keychain or keystore, wrapped by a key that requires user presence.
- Biometrics unlock that key. Server-side authorization is unchanged and still expires and revokes on its own schedule.
Android's docs separate BIOMETRIC_STRONG (Class 3) from BIOMETRIC_WEAK (Class 2) and provide CryptoObject so a successful prompt unlocks a Cipher or Signature rather than returning a boolean. OWASP lists cryptographic binding as a best practice for exactly that reason: a boolean callback can be bypassed or replayed; a keystore key with setUserAuthenticationRequired(true) cannot be used without the prompt succeeding.
Moneygate used expo-local-authentication for biometric authorization. That library gives you the prompt; the key-wrapping around it is your design.
Users want this. In the FIDO Alliance's 2024 barometer, biometrics ranked as the login method consumers consider both the best experience and the most secure for the second year running, and 42% had abandoned a purchase in the previous month over a forgotten password. That demand is exactly why teams are tempted to let the prompt stand in for auth.
Custody and secrets change shape on mobile
Custodial fintech has the easy version: the secret is a session token, and the worst case is re-login. Non-custodial products have the hard version. The key material lives on a device you don't control, next to a screenshot tool, a clipboard and whatever the user side-loaded.
Ctrl and DEFI Wallet are both non-custodial. DEFI Wallet's case page states it does not store users' seed phrases or passwords, and it ships the same key operations across extensions, web and mobile, with Ledger hardware-wallet support as an alternative to on-device keys.
What I can offer as engineering practice, kept separate from what those clients published:
- Generate and use keys inside the platform's hardware-backed store; export only for recovery flows, never for convenience.
- Bind signing to Class 3 biometrics or a device credential, with explicit confirmation per transaction, not per session.
- Invalidate biometric-bound keys when enrolment changes, another OWASP best practice for the same reason.
- Design recovery as a first-class flow. A wallet whose recovery path is "the user wrote the phrase down" has outsourced its most dangerous UX to a napkin.
No source describes Ctrl's or DEFI Wallet's key storage, recovery or attestation design, so don't read that list as their architecture. It's the checklist the shipped surface implies you'll need.
Reuse what's shared, fork what isn't
The shared-code fantasy: one monorepo, one business-logic package, two thin UI shells. In practice the reuse band is narrower:
- Design tokens. Colours, spacing, typography. Cheap to share, high payoff.
- Components. Worth sharing when the primitive is the same on both platforms; the layout around it usually isn't.
- Validation schemas. Zod or equivalent; IBAN rules don't change with screen size.
- Business logic. Partial at best. The moment a flow touches storage, auth or navigation, it diverges.
Ctrl's problem list named component inconsistency across platforms as something slowing development, and the answer was a unified component library for web and mobile, with the extension on Tamagui and Zod. DEFI Wallet listed accelerating web development through component reusability as an explicit challenge.
Both teams treated the component layer as the reuse target. Neither claims they shared the business logic.
The cost is coupling. A shared library forces one release cadence on two platforms with different constraints: the extension can ship a fix the same day; the app cannot. That's the price of consistency, and it's worth paying only if you also set up the release side.
Shipping to a store is not shipping to a URL
This is the shift teams underrate. Store review, staged rollout and users on months-old binaries change how you branch, feature-flag and hotfix.
The numbers are better than they were, but they're not a redeploy:
- Apple says 90% of submissions are reviewed in under 24 hours. The other 10% is where your Friday incident lives.
- Google Play's staged rollouts let you halt a release, and the percentage never increases on its own; you go up, you don't go down.
- Apple's guideline 2.5.2 says apps may not download or execute code that introduces or changes features. OTA JavaScript updates live inside the interpreted-code exception and the platforms' content rules; Expo's own docs are blunt that EAS Update handles non-native pieces such as JS, styling and images and anything native, any permission change, any SDK bump needs a new build.
Soapbox is what it looks like when none of this is set up. The app arrived with no cloud-based JS update system, no automated code review or deployment, no separation between development, staging and production, frequent push conflicts, and manual Play Store updates required from users.
The fix was environment stratification via GitLab and CodePush, ESLint, Datadog and full-cycle testing, followed by more than 15 successful releases. Fifteen is a count, not an improvement measure, but it's fifteen more than a team with no pipeline ships. Mode's case page lists 23.
Two more things that shape release engineering, both easy to miss at scoping:
- Your OTA vendor is a dependency. Microsoft retired App Center and CodePush on March 31, 2025 and published a standalone server to self-host. Adopt CodePush in 2023, inherit a migration in 2025. In the 2025 State of React Native survey, EAS Build is at 68.2% and GitHub Actions at 61.9%; the ecosystem consolidated, and that's a bet on someone else's roadmap.
- The web side doesn't escape platform-mandated rewrites. Ctrl's extension had to migrate from Manifest V2 to V3. Chrome disabled MV2 for all users on all channels by March 2025, removed the re-enable switch with Chrome 138, and deletes the remaining MV2 extensions from the Web Store on August 31, 2026. An extension is a store-distributed client with its own review; teams that file it under "web" get surprised.
Set up environments and OTA before the first store submission, not after the first incident. After the first incident you'll do it anyway, under worse conditions.
What the move actually costs
Stated plainly, going web-to-mobile buys you:
- Two more runtimes with their own crash signatures.
- Two more release channels, each with review and rollout mechanics.
- A device and OS-version test matrix that grows every September.
- Permanent version skew, so every API change is a compatibility decision.
- A native-dependency surface (files, PDF, camera, biometrics) you upgrade by hand.
None of that is a reason not to do it. Moneygate's five-person team and Ctrl's twelve-person dev and QA team show the headcount range between parity and net-new surface. Ctrl reports a user base expansion of 5–7% and a revenue increase of 10–25% as ranges on its case page, but that engagement also covered the extension redesign, the MV3 migration and backend work, so none of it isolates the mobile app.
The counter-case: if the mobile use case is read-heavy and occasional, a responsive web app plus one narrow native capability (push, or a biometric-gated approval companion) is often the better trade. Apple's guideline 4.2 rejects apps that don't rise above a repackaged website anyway.
Sequencing it
If I were scoping this again tomorrow:
- Pick a parity target and write it down in flows, not features.
- Fix the read contract first: aggregate server-side, paginate everything, version deliberately, and plan to keep old versions alive for a year.
- Decide the auth and custody model before the first screen. Biometrics unlock a local key; the server never trusts them.
- Budget the platform plumbing (files, PDFs, camera, permissions) as features with owners.
- Set up dev/staging/prod separation and OTA delivery before the first store submission.
- Share tokens, components and schemas. Fork the logic and stop feeling bad about it.
The uncomfortable part is the last one. Once the app is live, the web product has a permanent second client that can't be hotfixed, running on binaries you shipped months ago, against an API you can no longer change on a whim. That constraint outlives the launch, and it's the real thing you signed up for in that scoping meeting.
Top comments (0)