Engineering
Migrating from Cordova to Capacitor: A Cleaner Native Layer for Our Hybrid App

Last year, we migrated our hybrid mobile app’s native layer from Cordova to Capacitor. While our frontend is still web-based, we managed to simultaneously improve our customer experience from changes in the native side of the stack, while migrating with zero-downtime.

This post outlines why this work was worth doing, how we approached the migration, and what changed under the hood.

Why We Migrated

Our app was originally built with Apache Cordova. Cordova is an open-source mobile development framework that enables developers to build mobile applications using standard web technologies. By leveraging a web view within a native container, Cordova allows these web-based apps to access device features (such as camera, GPS, and contacts) through a set of APIs. It provides a unified platform for building cross-platform applications for iOS, Android, and other mobile operating systems, eliminating the need for separate codebases for each platform.

It gave us a quick way to package a web frontend with access to native APIs, which worked well in the early stages of Found as a company. But over time, we hit growing pains:

  • Unsupported or slowly updated plugins after Android/iOS updates. 

    1. While Cordova is still alive and well, we found that many of the plugins in the ecosystem didn’t receive timely updates to new OS versions – often leading to incompatibility or crashes on beta versions.

  • App crashes often lacked context the help developers debug the issues

  • Cordova’s build process did not work consistently with native IDEs particularly when engineers don’t work with this code often

  • Building custom plugins in Cordova was a tedious and error-prone process. Writing native code typically followed a fragile and manual workflow that looked something like this:

    1. Use Cordova to generate a native project.

    2. Build and run the app to confirm the base project works.

    3. Start writing native code directly in the generated project until your feature is complete.

    4. Copy and paste that native code into the actual plugin source so it’s preserved across builds.

    5. Manually delete the generated project files you edited.

    6. Triple-check that you copied every line of code correctly into the plugin directories—missing a file or line (which was easy to do) could break your next code generation.

    7. Re-run the code generation.

    8. Rebuild and test the app again to ensure everything still works.

      • Note that, your build caches are gone, so the rebuild takes much longer than usual—just to verify that you didn’t miss anything in the shuffle.

While fully rewriting the app in a native language was tempting, as an organization we didn’t want to invest the resources in a full rewrite, or switch to a cross-platform framework like Flutter or React Native due to the steep learning curve for most engineers at the company (we typically hire full stack developers, so building the iOS/Android competencies from scratch would be a significant effort). Based on the composition of our team and level of investment we wanted to make in this effort, we decided that something where for the vast majority of use cases developers can effectively ignore the native layer would work best for us.

Our web-based frontend was also working well, and was allowing us to make rapid changes and quickly iterate. We wanted to keep that velocity up going forwards, but we needed a better native wrapper—and that’s where Capacitor came in.

What is Capacitor?

Capacitor is a modern alternative to Cordova, developed by the Ionic team. It has a similar goal—bridging web apps with native functionality—but with a cleaner architecture and more maintainable native projects.

What stood out to us:

  • Native projects are real Android/iOS projects, no abstraction layer or code generation

    • You check in the full native project as the native IDEs expect. This allows easily setting breakpoints, easy build targeting and a consistent experience.

  • Plugins are built with native tooling (and easy to write ourselves)

    • On Cordova we had to develop in the native IDE and then copy and paste the code into the Cordova plugin code gen layer. This was confusing for new developers, and time consuming.

  • Much better debugging experience and developer ergonomics

    • Because the native project isn’t regenerated on every build, native IDEs can cache builds. Using the native IDEs also allows for a more consistent experience.

It works very similarly to Cordova, but with a few design decisions that seemed to make it much more pleasant to work with (as well as a generally more active community in 2024)

Capacitor architecture, from How Capacitor Works - Ionic Blog 

Key Differences We Noticed

Feature

Cordova

Capacitor

Native project structure

Generated / abstracted

Standard Android Studio & Xcode setup

Plugin ecosystem

Legacy, often outdated

Smaller, more modern

Debugging native code

Friction-heavy

Direct access to logs, code, breakpoints

SDK integration

Often fragile

Same as any other native app

How We Migrated

We didn’t need to change our frontend much except to change the javascript <-> native API, but the native side got a full refresh.

Step 1: Init Capacitor

Capacitor came with a built-in wizard for migrating projects, but after a first failed attempt (all of the known working versions for this migration are long outdated) we decided against using this tooling, and pretended we are starting from scratch.

npm install @capacitor/core @capacitor/cli

npx cap init

We kept our existing package name and app name for consistency.

Step 2: Add Native Projects

npx cap add android

npx cap add ios

This created actual Android and iOS projects we could open in Android Studio and Xcode. That was a big improvement right away—no more guessing what Cordova was doing under the hood.

Step 3: Install Plugins

We audited our Cordova plugin usage and decided on a path forwards: start off using all of our existing Cordova plugins that we could, except for the ones that were explicitly listed as having issues. Our migration plan looked something like this:

Cordova Plugin

Replaced With

cordova-plugin-fish

@capacitor/fish

cordova-plugin-dog

@capacitor/dog

Once we had a working app with most of the functionality from before, we would then slowly peel off old plugins and replace them (more to come on that). Where no Capacitor equivalent existed, we had two options:

  • Keep using the Cordova version (most still work)

  • Write our own small Capacitor-native plugin

    • This turned out to be straightforward following the documentation. Very simple plugins can be implemented in a single file on the native side.

We based our decision making process here on a few factors:

  • Does the plugin do something trivial?

  • Is the plugin abandoned?

  • Is this functionality critical to our business?

If any of these points were true, we biased our decision towards rewriting it ourselves. While any decision like this requires context about the company you work at, we think this is a helpful framework for deciding on rolling your own version.

For the plugins that we decided to keep the Cordova version running, we came up with a plan to eventually move them over to Capacitor after the initial release. This also helped us de-risk the rollout by only changing the smallest set functionality possible.

Step 4: Adjust javascript to support both platforms

Once we had all of the plugins set up and ready to go, we also needed to change how we interface with it on the web side. Our goal with this migration was not to provide new interfaces for all our product teams to migrate to. We wanted to switch over the implementations they worked with, while requiring minimal effort. 

We did this by wrapping all of our APIs and dynamically switching where they point to, depending on what native app the user is running. This let us roll over the underlying platform without requiring coordination across owning teams.

Step 5: The Rollout

Once we had all of our functionality ready to, and the ability to start releasing both, it was time to start sending this new app out into the world. We did this in a few steps:

  • Internal testing

    • We rolled internal users over to the new app, and were able to verify that everything was working as expected.

  • Stop building Cordova

    • We stopped building our old version, and rolled Capacitor onto our release tracks

  • Staged releases, health checks

    • We started a staged release, while monitoring for any performance regressions, crashes, or changes to business metrics.

  • Catching bugs!

    • During the staged release, we ended up having to hotfix one change on Android. You can see that in the chart below.

  • Full rollout

    • Once we were confident in the release, we were safe to turn things on to 100% of users!

iOS app versions rolling over:

Active android users (including the small hotfix)

A long tail of user adoption:

As this was rolling out, we noticed some improvements to cold start time on iOS. This shows the p95 of cold starts:

And Android remained stable:

Overall these metrics were promising and gave us a high degree of confidence that our changes were at worst invisible to users (if not better!).

Not only did the performance improve, but our app started behaving more reliably, with the user-perceived freezes trending down:

Step 6: Migrating plugins afterwards

Now that we had a stable Capacitor foundation out, we were able to migrate the plugins that weren’t mission critical over to Capacitor in smaller releases. This helped us manage risk and iterate on smaller pieces at a time. 

Working With Native SDKs

One of the best parts of this migration was that we could finally work with native Android and iOS code directly. We used this ability to embed more functionality in this layer such as custom instrumentation, error tracking and monitoring. This lets us continue to provide an even better app to our customers! 

In Cordova, these tasks were often brittle and frustrating. With Capacitor, we just follow normal native development practices.

What Stayed the Same

Our frontend deployment model hasn’t changed. We’re also still running a hybrid architecture, which continues to be the right tradeoff for our use case and team composition.

What We’d Do Differently

Given the constraints at hand, this was a massive success. The only thing different we would do in a future world is ship the app with the ability to remotely toggle whether we actually display the Cordova or Capacitor app to users. This would allow us to revert back to the old behavior without having to issue a new app release. 

Final Thoughts

This wasn’t a flashy project, but it made a big difference. Capacitor gave us a modern, maintainable native shell for our hybrid app, and removed a lot of the friction we’d grown used to with Cordova. 

We still rely on web tech for our UI, but now we have a native foundation that feels like it belongs in 2025, not 2012. We feel confident that we have a solid foundation to keep shipping products our customers love heading into the future!

If you're maintaining a hybrid app and the native side is starting to feel like a liability, it's worth taking a look at Capacitor. You don't need to rebuild your app from scratch to get a better foundation.




App icon cactus
All-in-one banking
for the self-employed

Found is a financial technology company, not a bank. Banking services are provided by Piermont Bank, Member FDIC.