Our main mobile app at Tawk.to had been running on Cordova since before I joined the team. It worked, but every time we needed to update a native plugin or deal with a platform-specific quirk, it felt like we were fighting the tooling instead of building features. After evaluating Capacitor for a few weeks, we decided to make the switch. Six months later, I can say it was one of the best infrastructure decisions we have made. Here is how we did it and what we learned along the way.

Why Cordova Was Holding Us Back

Cordova served us well for years, but several pain points had been accumulating. Plugin management was fragile. Updating one plugin would often break another because of conflicting native dependencies. The build process felt opaque; when something went wrong in the native layer, debugging meant digging through generated code that was not meant to be edited. And the community had clearly shifted its energy toward Capacitor, which meant fewer updates and slower bug fixes for Cordova plugins.

The breaking point came when we needed to support iOS 17's new privacy manifests. The Cordova ecosystem was slow to adapt, and we found ourselves manually patching plugins to add the required declarations. That was when we committed to migrating.

The Migration Process

The Ionic team has a migration guide, but real-world migrations always have more nuance. Here are the actual steps we followed:

# Step 1: Install Capacitor in the existing project
npm install @capacitor/core @capacitor/cli

# Step 2: Initialize Capacitor
npx cap init "Tawk.to" "com.tawkto.app" --web-dir www

# Step 3: Add platforms
npx cap add ios
npx cap add android

# Step 4: Build your web app and sync
ionic build --prod
npx cap sync

The key difference from Cordova is that Capacitor checks the native project files (the ios and android directories) into version control. This felt wrong at first, coming from Cordova where the platform directories were generated and gitignored. But it turned out to be a huge advantage. You can open the native project in Xcode or Android Studio and make changes directly, without worrying about them being overwritten by a build step.

Plugin Migration: The Biggest Effort

This was the most time-consuming part of the migration. We had 14 Cordova plugins, and each one needed to be replaced with a Capacitor equivalent. Some had direct replacements:

// Before (Cordova)
declare var cordova: any;
cordova.plugins.notification.local.schedule({
  title: 'New message',
  text: 'You have a new chat message',
  foreground: true
});

// After (Capacitor)
import { LocalNotifications } from '@capacitor/local-notifications';

await LocalNotifications.schedule({
  notifications: [{
    title: 'New message',
    body: 'You have a new chat message',
    id: Date.now(),
    schedule: { at: new Date() }
  }]
});

The Capacitor API is TypeScript-first, which means you get autocomplete and type checking. No more guessing plugin APIs or consulting outdated documentation. For three of our plugins, there was no direct Capacitor equivalent, and we had to write custom Capacitor plugins. This sounds intimidating, but the plugin development experience is significantly better than Cordova. You write native Swift or Kotlin code with clear bridge methods, and the tooling generates the TypeScript interface for you.

Native Bridge Improvements

One of the things that impressed me most about Capacitor is how it handles the native bridge. In Cordova, communication between the web layer and native code goes through a string-based message queue that can become a bottleneck during rapid calls. Capacitor uses a more direct bridge that serializes data as JSON and passes it through a faster channel.

In our chat app, where we make frequent calls to the native layer for things like push notifications and local storage, we measured a roughly 30 percent reduction in bridge call latency after the migration. This might not matter for apps that rarely call native APIs, but for our use case it was noticeable.

Live Reload and Developer Experience

Capacitor's live reload is substantially better than Cordova's. With Cordova, we had a custom script that watched for file changes and triggered a rebuild. With Capacitor, it is built in:

# Run with live reload on a connected device
ionic cap run ios --livereload --external

# Or for Android
ionic cap run android --livereload --external

Changes to your web code are reflected on the device in about one to two seconds. This alone has improved our development velocity. No more waiting for full rebuilds to test a CSS change.

Gotchas and Lessons Learned

The migration was not entirely smooth. A few things caught us off guard. First, Capacitor serves your app from an HTTPS origin by default, while Cordova uses a custom scheme. This broke some of our API calls that had hardcoded HTTP origins. Second, file paths work differently between the two. Capacitor uses a different scheme for accessing local files, so any code that constructed file URIs needed updating. Third, some Cordova plugins store data in locations that Capacitor plugins do not automatically read from, so we had to write a one-time migration script to move user preferences from the old storage location to the new one.

Despite these bumps, the migration took about three weeks for two developers, and the result has been a more maintainable, more performant, and more enjoyable codebase. If your team is still on Cordova, I would strongly recommend setting aside the time to migrate. The Capacitor ecosystem is where the community and the tooling are heading, and the developer experience improvement alone justifies the effort.