Migrating from Cordova to Capacitor

It would be awesome to see someone doing a demo of this at the impact conference this year

3 Likes

Sounds super exciting. Do you think capacity-community/electron would work seamlessly?

I ported cordova-plugin-meteor-webapp to Capacitor!

It’s iOS-only at this point (only because I haven’t tackled Android yet and have less experience with that platform) and not thoroughly tested or reviewed (i.e. not calling it production-ready yet), but it WORKS!

There are a full set of instructions (different from those in my first post) for how to get Capacitor running with Meteor in the README. The plugin changes the way files are loaded to match how the Cordova plugin works, so there’s no longer a need to massage the output in the built server’s web.cordova directory.

We also need to shim window.WebAppLocalServer and proxy that API to CapacitorMeteorWebApp, so that the Meteor runtime is able to interact with window.WebAppLocalServer as usual and remain blissfully ignorant that we’re not in Cordova anymore. The banjerluke:capacitor-meteor-webapp-shim Meteor package will do this for you. As of v0.1.0 the Capacitor plugin adds this directly to the index.html file before serving it, so it’s all taken care of.

The build process still feels a little “hacky”, but it’s a good DX now (one build script). I am curious to see what Meteor maintainers might think of adapting the core to make this script less hacky, perhaps outputting a static index.html and program.json with a version field so that there was no need to boot up the server.

I will be out of town during the conference but I encourage anyone to take this as a starting point and run with it!

Haven’t tried, and probably won’t (ToDesktop has been working flawlessly for me) but I don’t see why it wouldn’t work! When all is said and done, you’re left with a pretty standard Capacitor setup, so any Capacitor plugin should work…

6 Likes

Question: what’s the real difference between capacitor and cordova? In the end they are both a webview running your app, no? What’s the benefit of migrating to capacitor?

1 Like

Hey @banjerluke! Great to see your work on this approach. Even if it feels a bit hacky, having it running is a big relief each time, I know that.

I have a few comments:

Dev mode: What worked for me was relying on Capacitor’s built-in live reload. If you update the www folder (in your case capacitor/www-dist), changes reload automatically. For development mode only, I use this in my Capacitor config:

"server": {
	"url": "http://{{LOCAL_IP}}:{{PORT}}",
	"cleartext": true
},

The tricky part is enabling a watcher that runs your build script whenever changes happen in Meteor’s Cordova context (.meteor/local/build/programs/web.cordova). In my setup, dev mode runs Meteor with the Cordova arch enabled, plus a background watcher on that folder. Any effective change from Meteor triggers my process to rebuild www-dist, which then refreshes automatically in the emulator.

HCP: Making cordova-plugin-meteor-webapp work with Capacitor is nice, but it might conflict with Capacitor’s own local webview/server. I didn’t go too deep into it, since the Capgo approach felt smoother and easier to reason about. Meteor HCP does a lot for what is essentially just replacing the native bundle.

Capgo can be used without their paid service. I used its API (CapacitorUpdater.download / CapacitorUpdater.set, and having much more utilities) to build a simple mechanism that prompts an update if a new bundle is available in a AWS S3 bucket and the user is on an older native build. That bucket just holds the zipped, versioned www-dist. Capgo fetches and replaces it cleanly. I could have used Meteor’s own express app to serve the bundle, but chose S3 to offload it. Still, I may experiment with a more integrated express-based approach when I go deeper into Meteor–CapacitorJS integration. But having cordova-plugin-meteor-webapp compatible with your experiments would also be great!

1 Like

Hey @matheusccastro, glad to see you here :blush:

Capacitor and Cordova both follow the webview approach, allowing the same web code to run on native platforms with little effort.

The main reasons to migrate are features and maintenance. Cordova itself still gets updates, but the ecosystem depends heavily on plugins, and many of them are abandoned, deprecated, or not updated with the latest native APIs. A clear example is RevenueCat’s Cordova plugin for cross-platform payments, which is being deprecated in favor of its CapacitorJS version. This will happen more often, and Capacitor ensures plugins evolve and integrate better with the latest native features.

Some key differences:

  • Native projects: Cordova generates them as build artifacts (changes can be overwritten). Capacitor treats them as independent projects that you keep and commit.
  • Plugins: Cordova relies on an old plugin ecosystem, while Capacitor plugins are modern, Promise-based, easier to write, and actively maintained.
  • Web support: Capacitor APIs often work in the browser too (PWA support). For example, camera or file management plugins run both natively and on the web, with dedicated implementations for each in a single module.
  • Developer experience: Capacitor encourages direct use of Xcode/Android Studio, integrates more smoothly with modern SDKs, and provides typed, well-documented APIs.
  • Features & Maintenance: Both Capacitor core and its plugin ecosystem are actively updated with new features and support for the latest native APIs. This matters because it lets you rely more on the open source community instead of maintaining your own core and plugin forks and have experts on each of them.

Meteor moving to CapacitorJS is one of the smoothest integrations possible. CapacitorJS feels like the natural successor to CordovaJS, with built-in compatibility for Cordova plugins. This makes the transition simple for existing native Meteor projects, though it’s recommended to migrate to modern Capacitor alternatives over time.

3 Likes

I don’t think this is true in reality – at least I have not had any issues with it in my local testing (still working on porting tests from the old plugin). And that’s because I am using the same approach that Capgo is using. The Capacitor bridge object exposes setServerBasePath, which lets you change where files are served from. So instead of serving directly from the bundled public folder, it serves from a different folder.

At first, the plugin “organizes” the public folder (which is taken straight from web.cordova build output, with an app directory and __cordova paths in index.html and everything) into an “initial bundle” that is then served by Capacitor using setServerBasePath. When a new bundle is registered by Meteor, it is fetched and “organized” into a directory (based on it’s program.json manifest) and then once WebAppLocalServer.switchToPendingVersion() is called (which Meteor does automatically for Cordova), Capacitor is told to serve files from that folder instead. It’s a much more “native” approach than what was being done before in Cordova, since we don’t have to bundle our own local web server. (WebAppLocalServer is kind of a misnomer now, but it’s kept for backwards compatibility.)

Capgo seems nice, but for an existing Meteor application – and for anything that might be considered as an “official” Meteor-Capacitor integration someday – I think keeping backwards compatibility with Meteor Cordova HCP is definitely the way to go. Why add a new dependency and complicate the deployment process? It also shifts the burden of adapting a web.cordova Meteor bundle from a build script (s/__cordova//g and so forth) to the Capacitor plugin.

For a new app, Capgo might be favorable for certain reasons. But for an existing Meteor app migrating from Cordova to Capacitor, maintaining HCP would be a smoother transition. It’s definitely what I’m going to do, in any case.

I think maintaining backward compatibility with Meteor HCP would also solve dev mode, without the need for a custom watch/sync script to finagle web.cordova output. I haven’t gotten into that yet though.

My plugin is still “unstable” in that I haven’t finished porting over the tests from cordova-plugin-meteor-webapp… not to mention I haven’t touched Android, though that should be a straightforward port. But it is working well in testing on my devices. I did start working on tests in the swift-testing branch, and I think the approach is solid (testing with swift test outside of an actual Capacitor environment by mocking the Capacitor bridge and external APIs) but I need to focus on some more pressing issues with my app right now.

1 Like

Yes, your reasoning makes sense. Backwards compatibility is the best path. For my app it has been more than enough, and removing much of Meteor’s overconfiguration was a big relief. It’s also good to see that we can keep some of the way the Meteor plugin handled files and live updates. Otherwise, we can rely on what Capacitor already offers with live reloads for dev mode, plus a plugin for HCP. Either way, Capacitor will be integrated with Meteor.

Why add a new dependency and complicate the deployment process?

Depending on the dependency, cordova-plugin-meteor-webapp has been hard to maintain and to publish new versions without risking breakage. I prefer to rely on a more popular, delegated dependency with many eyes reviewing it in the ecosystem of the tool we’re adopting, like Capacitor. We can keep backward compatibility by using that external plugin to hook into Meteor so end users don’t notice.

Same reasoning applies to the latest Rspack integration. Who could have imagined a more native approach to Meteor, with tree shaking, ESM support, and the other features a modern bundler provides today? There have been attempts before, but none were completed and the motivation faded. Yes, we are adding a new dependency, but I believe it’s for the best.

I’m also open to experimenting to revive cordova-plugin-meteor-webapp, understand this piece better, ensure proper and accurate testing, and ship future updates.


Let’s continue experimenting. After the modern bundler integration, I’ll have more time to join in and help explore the options for keeping native HCP while still using Capacitor’s modern features without regressions. I will keep an eye on your work here.

I wasn’t very clear about my hesitation around a “new dependency”, so just to clarify: I’m reluctant to introduce dependencies to new services (Capgo paid service, S3 bucket, etc.) more than depending on external libraries per se. Not because of cost (Capgo pricing is very reasonable and S3 is dirt cheap) but overhead and complexity and wanting to keep things simple.

In general, I think having dependencies on well-established libraries in the JS ecosystem is smart. I’m very excited about the Rspack integration, for example.

Besides, I’m fully aware that a Capacitor port of cordova-plugin-meteor-webapp would also be a “new dependency”, and one with less eyes on it than a library like Capgo, so it’s certainly no panacea.

Looking at the Capgo API a bit closer, it seems like I could potentially have the Meteor server itself replace the hosted Capgo service by responding to API POST calls on a self-hosted endpoint. I guess this is what you’re doing. I am intrigued and may look into this further. But I would feel bad making any sort of public or official integration like this, because it would circumvent Martin’s hosted service and he should be supported for his work.

(Not to mention that I could never get the Capgo update process to behave as smoothly and with as much control over timing as with the Meteor approach, but that could be some combination of user error and having a large bundle size due to tens of megabytes of audio assets in the bundle.)

Thanks for all your thoughts @nachocodoner. Exciting stuff.

1 Like

I’m not in the know enough here to maybe understand the why.

But it’s seems rendering an iOS WebView natively or the Meteor React Native support package would be preferable now compared to when Meteor originally was trying to do Cordova support?

Capacitor is rendering an iOS WebView natively/directly. I think it’s about as efficient and performant as you can get while still getting to render HTML.

React Native is great but it requires you to rewrite your entire view layer in React Native components, as it doesn’t render DOM HTML but native components instead, so it’s not a replacement (nor an option for this one-man development team that has to maintain a web version of the app as well).

3 Likes

Amazing initiative @banjerluke
Please, take a look in our paid articles program, this journey gives a good content :wink:

Hello everyone,

Thanks a lot @banjerluke for your post — it really helped me get back into trying to publish a Meteor app to the mobile stores! Also, big thanks to @nachocodoner for your reply to my previous post on this topic.

I’m almost there. I’ve managed to set up a relatively comfortable dev environment with this Makefile, which automatically synchronizes the Meteor Cordova build with the Capacitor mobile build:

build-sync:
	rm -rf capacitor/www-dist/* || exit 1; \
	cp .meteor/local/build/programs/web.cordova/*.{css,html,json} capacitor/www-dist || exit 1; \
	cp -r .meteor/local/build/programs/web.cordova/packages capacitor/www-dist/ || exit 1; \
	mkdir capacitor/www-dist/app || exit 1; \
	cp -r .meteor/local/build/programs/web.cordova/app/*.js capacitor/www-dist/app || exit 1; \
	rsync -r --exclude='*.js' .meteor/local/build/programs/web.cordova/app/ capacitor/www-dist/ || exit 1; \
	rm -rf capacitor/www-dist/**/*.map || exit 1; \
	sed -i '' "s|WebAppLocalServer\.onError(function(e){console\.error(e)})||g" capacitor/www-dist/**/*.js || exit 1; \
	curl -f --connect-timeout 20 'http://localhost:3000/__cordova/index.html' | \
		sed -e 's|/__cordova/client/|/client/|g' -e 's|/__cordova/|/|g' \
		> capacitor/www-dist/index.html; \
	sed -i '' "3r capacitor/WALS-shim.html" capacitor/www-dist/index.html; \
	npx cap sync;

enable-watch:
	watchman-make -p '.meteor/local/build/programs/web.browser/**/*' -t build-sync

Combined with @nachocodoner’s suggestion in the Capacitor config:

"server": {
  "url": "http://{{LOCAL_IP}}:{{PORT}}",
  "cleartext": true
}

Now, I’ve hit what might be the final hurdle.
I’m working with Meteor 3.3.2, and I’m facing the same issue I ran into in previous experiments.

Our project uses the npm dependency @tanstack/react-query@5.36.0. Everything works perfectly on a classic web session - or using the development capacitor config since it directly loads the url within the WebView, but something breaks in the Cordova build:

getQueryDefaults(queryKey) {
  const defaults = [...__privateGet(this, _queryDefaults).values()];
  // TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function
  const result = {};
  defaults.forEach((queryDefault) => {
    if ((0, import_utils.partialMatchKey)(queryKey, queryDefault.queryKey)) {
      Object.assign(result, queryDefault.defaultOptions);
    }
  });
  return result;
}

I believe that same error was raised when using the web.browser.legacy build.

After digging into the generated builds, I noticed that the packages/modules output differs between builds.
That’s confusing, since according to the Meteor documentation, Cordova bundles should now be modern.

Has anyone else encountered this, or figured out how to make sure the Cordova build uses the same modern compilation as web.browser?

Any pointers appreciated!

Huh. I had this in my meteor-settings.json:

  "packages": {
    "modern-browsers": {
      "unknownBrowsersAssumedModern": true
    }
  },

The link in your post suggests this is no longer needed with Meteor 3.3.2+, but could be worth a try.

You could also try putting this in your package.json:

  "meteor":
    "nodeModules": {
      "recompile": {
        "@tanstack/react-query": "legacy",
      }
    }
  },
2 Likes

Thanks @banjerluke for your reply. I already tried that suggestion, but it didn’t fix the issue.

After inspecting Meteor’s core more deeply, I’m convinced the root of this problem is that web.cordova is still being compiled under “legacy” assumptions, which is incompatible with many modern JS packages. To solve that properly, we need to treat the Cordova (and Capacitor) build paths as modern by default – just as already suggested in documentation.

I’ll open a draft PR to propose that change. Here is the draft PR Consider `web.cordova` as a modern architecture by tmeyer24 · Pull Request #13983 · meteor/meteor · GitHub

@nachocodoner, I believe this will be essential for proper Capacitor integration as well. It doesn’t make sense that @tanstack/react-query is the only package failing under legacy build—more likely there are multiple packages that assume modern JS semantics. On mobile, just as on the web, usage of modern JavaScript is now the norm. Libraries’ expectations have shifted, and we need the build system to keep pace.

2 Likes

Does this support social sign-ins?

I also know there could be some challenges with getting the sessions signed into the browser to load up as part of the authentication.

Does this support social sign-ins?

I don’t see why it wouldn’t. I’m guessing you’d have to use @capacitor/browser or another in-app browser plugin to show the OAuth screens, but that’s probably the same for Cordova right? It’s possible you’d also need to configure deep links for the app to capture OAuth callbacks. Just guessing here; I don’t use social sign-ins in my app, although I do use an in-app browser for other things and I have deep links set up.

Yeah I think there can be an issue where it loads up a clean browser window with no session information, meaning user has to sign into Google/Facebook/etc even if their mobile browser is already signed in. I believe the Meteor-Cordova integration magically handles that, would be great to see that here too if you have plans to make it a Meteor package or merge into Meteor Core.

As far as I’m aware, the only runtime “special sauce” that Meteor adds to the Cordova app is cordova-plugin-meteor-webapp. I intend to replicate all relevant functionality from that plugin, except for the bundled web server (since Capacitor handles that now) and local filesystem access (since it’s no longer trivial given that we’re using Capacitor’s web server, and I think we’re better off leaving that to other plugins). I’ve completed the reimplementation for iOS (pending further testing) and plan to tackle Android soon.

As far as I can tell, cordova-plugin-meteor-webapp doesn’t add any “magic” in terms of sharing session with the system browser, however. I don’t think that’s even possible unless you actually open the system browser, i.e. the OS switches over to Safari or Chrome (or whatever the default browser is) and away from your app. Besides, as far as I know you can’t even open in-app browser windows for the auth in Cordova unless you have one of the plugins; it’s not part of cordova-plugin-meteor-webapp’s API:

declare global {
  interface Window {
    WebAppLocalServer: {
      startupDidComplete(callback?: () => void): void;
      checkForUpdates(callback?: () => void): void;
      onNewVersionReady(callback: (version: string) => void): void;
      switchToPendingVersion(callback?: () => void, errorCallback?: (error: Error) => void): void;
      onError(callback: (error: Error) => void): void;
      localFileSystemUrl(fileUrl: string): never;
    };
  }
}
1 Like

I pushed a bunch of updates to my cordova-plugin-meteor-webapp replacement plugin, capacitor-meteor-webapp.

It’s got Android support now, along with a whole suite of tests for both platforms (and bug fixes to go along with them). I’m feeling a lot more confident in its production-readiness and am going to update my public beta apps (used by a couple hundred folks) to Capacitor very soon.

To be clear: AI agents (Opus 4.6 & Codex 5.3) did all of the coding, though I was heavily involved in the (multiple) planning and review stages. I think the result is better than if I’d done it myself (given that this is out of my wheelhouse as a primarily-web programmer) but I want to be sure to disclose that. I did everything I could to make a robust plugin because the last thing I want is to have thousands of broken app installs on my production app – that would be catastrophic. (Writing that makes me a bit nervous… but that’s why I have a beta version!)

Now to finally integrate RevenueCat! :smiley_cat:

3 Likes