Mobile App Development

Capacitor as a bridge between our PWA and native app stores

APP

A client's progressive web app had strong usage, a loyal base of users who'd installed it to their home screen and used it regularly, but they wanted a presence in the actual app stores, both for discoverability and because a segment of their user base simply distrusts installing anything that isn't found through an app store listing. Funding a full native or React Native rebuild wasn't in the budget for what was fundamentally a distribution problem rather than a product one, so Capacitor let us wrap the existing PWA with native shell functionality instead of rebuilding the product from scratch.

What Capacitor actually is

Capacitor takes an existing web app and wraps it in a thin native shell, essentially a native container hosting a web view, while exposing native device APIs to the web code through a plugin bridge. That distinction matters for understanding both what you get and what you don't: the UI is still the same web app rendering in a web view, so there's no separate native UI layer to build and maintain, but the shell can request native permissions, invoke native APIs, and get listed in app stores as a genuine native app rather than a bookmarked website.

Access to native APIs like push notifications and biometric login came through Capacitor plugins with minimal changes to the existing web codebase. The push notification plugin, for instance, handles registering the device token with Apple's and Google's respective push services and exposes a simple JavaScript API to the existing app code for handling incoming notifications, work that would otherwise have required native code the team didn't have in-house. Biometric login similarly exposed Face ID and fingerprint APIs through a JavaScript call, which the existing login flow could adopt with a conditional branch rather than a parallel native implementation.

  • Access to native APIs like push notifications and biometric login came through Capacitor plugins with minimal changes to the existing web codebase.
  • Performance for anything animation-heavy still lagged behind a true native or React Native build, which is the tradeoff we made clear to the client upfront.

Where the wrapper shows its seams

Performance for anything animation-heavy still lagged behind a true native or React Native build, which is the tradeoff we made clear to the client upfront rather than letting them discover it after launch. A web view, however capable, is still rendering through a browser engine rather than compiling to native UI components, and for a list with complex scroll-linked animations, the difference was visible, not broken, but noticeably less fluid than a comparable native or React Native screen would have been. We flagged this specifically for any future feature involving heavy animation or gesture-driven interaction, since that's where the wrapper approach costs the most relative to a native rebuild.

We also had to budget time for platform-specific quirks that don't come up in normal web development: iOS's stricter rules around background push notification handling, Android's fragmented behavior around biometric API availability across device manufacturers, and app store review requirements that a pure web app never has to think about, like Apple's guidelines around what counts as a sufficiently "native-feeling" experience for a wrapped web app to pass review. None of these were dealbreakers, but they were real work that a team unfamiliar with either app store's review process would likely underestimate.

When we'd recommend this path

App store review considerations we hadn't fully anticipated

Getting the wrapped app through app store review took more back-and-forth than a typical native app submission, largely because both Apple and Google have review guidelines written with an assumption of substantial native functionality that a wrapped web app has to demonstrate it meets even when its actual implementation is a web view. Apple in particular scrutinizes whether a submission is "just a website in a wrapper," and we had to make sure the app meaningfully used native capabilities, push notifications, biometric login, offline caching, rather than just rendering the same web pages a browser bookmark would show, in order to pass review on the first attempt rather than receiving a rejection citing insufficient native functionality.

Offline behavior needed its own design pass

Offline support was one area where the wrapped approach required real additional engineering rather than coming for free. The existing PWA already had a service worker caching key assets, but Capacitor's native shell introduces its own network-state handling that doesn't automatically defer to the web app's existing offline logic, so we had to build an explicit bridge letting the web code query the native layer's connectivity status directly rather than relying solely on the browser's standard online and offline events, which behaved inconsistently across different Android device manufacturers' network stacks in our testing.

Update strategy differs meaningfully from a pure web app

One tradeoff we hadn't fully appreciated going in was how app updates work differently once a web app is wrapped for app store distribution. Because Capacitor bundles a version of the web assets into the native binary at build time rather than always fetching fresh from a server the way the original PWA did in a browser tab, a change to the app's JavaScript doesn't reach users automatically; it requires either a live-update mechanism syncing new web assets at runtime, or a full new app store submission and review cycle for anything more substantial. We set up a live-update pipeline for minor content and bug-fix changes, keeping app store submissions reserved for changes to native functionality or app store metadata, which mostly preserved the fast iteration speed the client's team was used to from the pure web version.

Testing across real devices mattered more than emulators suggested

Emulator testing gave us a false sense of confidence early in the project; several of the platform-specific quirks we eventually had to address, particularly around biometric API availability on certain Android manufacturers' custom Android builds, simply don't reproduce reliably in an emulator and only surfaced once we tested against a small physical device lab covering a spread of real hardware and OS versions. We'd budget for physical device testing much earlier in any future Capacitor project rather than treating it as a pre-launch checklist item, since several of the issues we found late would have been cheaper to catch and fix earlier in the build.

Revisiting the decision a year later

A year after launch, we revisited the original Capacitor-versus-React-native decision with the client to see whether it still held up given how the app had actually been used. Usage patterns confirmed the original call: the animation-heavy screens we'd flagged as a risk upfront saw relatively light usage compared to the core account-management flows, which perform well in the wrapped web view, and the client's app store presence goal had been fully achieved without the cost of a native rebuild. We'd make the same recommendation again for a similar client profile, though we'd flag the update-cadence tradeoff even earlier in the conversation than we did the first time.

Support costs after launch were lower than either the client or our team initially expected, which we attribute mostly to how little new surface area the wrapper actually introduced. Because the core application logic remained the same web codebase already running in production as a PWA, most bug reports traced back to pre-existing web issues rather than anything specific to the native wrapper, and the wrapper-specific issues we did see clustered almost entirely around the handful of native plugin integrations rather than being spread unpredictably across the whole app.

Capacitor is a strong option when app store presence matters more than native-grade performance and there's already a solid, actively used PWA to build from; wrapping something that already works well as a web app is a fundamentally lower-risk project than building a new native experience from a blank slate. It's not a replacement for React Native when performance is the priority, particularly for anything gesture-heavy or animation-heavy, and we're upfront with clients about that distinction before the project starts rather than after they've noticed the difference in a shipped build. For this client specifically, app store presence and push notifications were the actual goals, not matching native performance exactly, which made Capacitor the right call rather than a compromise they'd regret. Framing the decision around the client's actual goal, distribution rather than raw performance, is ultimately what made the wrapper approach the right call rather than a compromise anyone had to talk themselves into after the fact.

← Back to the journal

Have a project in mind?
Let’s talk.

Tell us where you are and where you want to go. We'll map the fastest route between the two.

Currently accepting new clients