Engineering · Legacy modernization

Upgrading a 7-year-old Ionic app in production: Angular 8 → 19, Capacitor 2 → 7

mySAME / myDeutz-Fahr is the consumer app our engineers built and maintain for SDF Group - farmers across 13 European markets use it to register tractors, find dealers, and read live Bluetooth telemetry. It has shipped continuously since 2019, and it could not stop shipping while we dragged its foundations forward by seven years. Here's how we did it, and what actually broke.

Why not rewrite: the honest math

Every modernization conversation starts with someone suggesting a rewrite, and we understand why. An Angular 8 codebase in 2026 feels like a liability - the tooling complains, new hires wince, and every dependency you touch is three majors behind. A greenfield repo feels like relief.

But the math rarely supports it for an app that's actually in production. This app has seven years of accumulated behavior: VIN registration flows that handle the quirks of three tractor brands (Deutz-Fahr, SAME, Lamborghini Trattori), dealer lookup across 13 markets, promotions, recall notices, maintenance schedules, live telemetry over Bluetooth, ten languages of translations. Very little of that is visible in the codebase as "features" - it lives in the small fixes, the edge cases, the odd conditional someone added in 2021 because a specific market needed it. A rewrite means re-earning all of that while shipping nothing new, and the industry's track record on rewriting working software is genuinely bad.

The rewrite is the right call in one situation: when the product is changing fundamentally, not just the framework version. That wasn't the case here. Farmers were happy with what the app did - the problem was purely that its foundations had aged out of the ecosystem. So we upgraded in place. All of it.

The gap you're really crossing

"Upgrade the app" undersells what a 2019 → 2026 jump means in the Ionic/Angular world. This is what the two ends of our migration actually looked like, straight from the two branches' package.json:

Layer2019 stack (master)2026 stack (upgrade branch)
Angular8.2 (pre-Ivy era)19.1 - Ivy, standalone components, new control flow
Ionic57
Capacitor2 - Cordova plugins still everywhere7 - Cordova ecosystem effectively retired
TypeScript3.45.x
RxJS67
LintingTSLint + codelyzer (both dead)ESLint 9 + angular-eslint
Slides / carouselsion-slidesSwiper 11
Native pluginsMix of Cordova + early community plugins, several abandonedOfficial Capacitor 7 plugins + maintained community ones

That's roughly eleven Angular majors, the entire Ivy transition, the death of the Cordova plugin ecosystem, AndroidX, years of new Android permission models, and Apple's privacy-manifest era - all crossed in one project. The framework columns are the easy part to list. The plugin row is where the project is actually won or lost, and we'll get to it.

Strategy: one long-lived branch, app shippable throughout

The constraint that shaped everything: master had to keep shipping. Recalls, promotions, seasonal campaigns - the business doesn't pause because engineering is modernizing. So we ran the upgrade on one long-lived branch alongside a master that stayed on the old stack, and we treated "the branch builds and runs on both platforms" as a standing requirement, not a milestone at the end.

That sounds obvious. It isn't how most modernizations die. The usual failure is a branch that spends four months in a broken state - hundreds of compile errors, "we'll fix the tests at the end" - until nobody can say how far from done it is, a release deadline lands, and the branch is quietly abandoned. Keeping the branch runnable at all times costs more discipline day-to-day and buys you the one thing that matters: at any point, you know exactly what works and what doesn't, and you can demo it to the client on a real phone.

It also changes how you sequence. We didn't organize the work by feature ("migrate the dealer module") but by layer: framework first, then plugins, then the UI-level breakage, testing native flows on both platforms after each layer. Feature changes landing on master got ported across as we went - tedious, but far cheaper than one giant merge at the end.

The plugin graveyard

Here is the part every "Ionic 5 to Ionic 7 migration" tutorial skips: the framework upgrade has documentation, schematics, and a community that crossed it before you. Your plugins have none of that. A Capacitor 2 app from 2019 is standing on Cordova plugins and first-generation community plugins, and a brutal share of those were abandoned when their authors moved on. This is where our estimates were tested, not in Angular.

The clearest example was social login. The 2019 app used three separate plugins - one for Google, one for Facebook, and an Apple sign-in plugin installed straight from a Git URL because it never made it to a registry. By 2026, none of them had a maintained path to Capacitor 7. Rather than hunt for three individual successors, we consolidated all three onto a single maintained plugin, @capgo/capacitor-social-login, which handles Google, Facebook, and Apple behind one API. One dependency to watch instead of three, and the app-side auth code got simpler in the process.

Not every plugin has a successor, though. Our Android push-notification setup depended on a plugin that is no longer maintained but still works - except for one incompatibility with the new toolchain. Forking it means owning a whole plugin forever to fix ten lines. The pragmatic middle ground is a postinstall patch: a small script that runs after every npm install and fixes the plugin in place.

// package.json
"scripts": {
  "postinstall": "node script/android-pn-plugin-patch.js"
}

// the script: read the plugin's Android source out of node_modules,
// replace the handful of lines that break on the modern toolchain,
// write it back. Runs on every install, so every developer
// and every CI build gets the fix automatically.

It's not elegant, and we treat it as a loan rather than a solution - the script is a named, versioned admission that this dependency needs replacing eventually. But it unblocked the migration for a fraction of the cost of a fork, and it's honest in a way that a silently hand-edited node_modules never is.

The modernization was also the moment to add what the old stack couldn't support well: @capacitor-community/bluetooth-le for the live tractor telemetry, hardware-backed secure storage for tokens, App Tracking Transparency for iOS compliance, Firebase Analytics on the maintained Capacitor plugin, and on-device OCR with tesseract.js so farmers can scan a VIN plate instead of typing seventeen characters in a field.

The hard-won lesson: before you estimate a Capacitor upgrade, audit every native plugin and sort them into four buckets - officially maintained, has a maintained successor, needs a patch, needs replacing outright. The framework migration is the predictable half of the project. We have never seen an estimate die on ng update; we have watched several die on a plugin nobody had looked at since 2020.

Angular 8 → 19: the mechanics

Eleven majors invites a tempting shortcut: skip the ladder, create a fresh Angular 19 workspace, and copy the source across. We've tried that on other projects. It works for small apps and fails for real ones, because you lose the migration schematics - every Angular major ships automated code migrations that rewrite your source for the new APIs, and they only run when you step through the versions. For an app with this many pages and services, those schematics did a large share of the mechanical work.

So we went major-by-major with ng update, in batches, keeping the app compiling at each step:

ng update @angular/core@9 @angular/cli@9   # the Ivy crossing - the hard one
ng update @angular/core@10 @angular/cli@10
# ... one major at a time through 19,
# building and committing after each step

What actually broke, in rough order of pain:

  • The dead-tooling tax. TSLint and codelyzer don't exist on the modern stack; the lint setup was rebuilt on ESLint 9 with angular-eslint. Protractor is gone the same way. This is throwaway-and-rebuild work, not migration work.
  • RxJS 6 → 7. Mostly mechanical - renamed operators, stricter types - but it touches nearly every service, so it's wide rather than deep. Strict TypeScript 5 surfaced years of quietly wrong types along the way.
  • ion-slides → Swiper. Ionic retired its slides component; every carousel and onboarding flow moved to Swiper 11 with different markup and a different API. Pure grind.
  • Standalone components. Angular's ecosystem is standalone-first now, so we adopted the pattern as we touched components - flagging them standalone with explicit imports - rather than converting the whole NgModule tree in one theatrical sweep. New code is standalone by default; old modules retire as they're touched.
  • Third-party Angular libraries. Every UI library pinned to an old Angular needed its own upgrade or replacement - translation loaders, QR code components, selectable dropdowns. Small individually, numerous collectively.

The honest summary: none of this is intellectually hard, and almost all of it is volume. The skill isn't in any single fix - it's in sequencing hundreds of them so the app never stops being a working app.

What we'd tell you before you start

  1. Audit plugins before you estimate anything. The four-bucket exercise from the callout above, done in the first week, is the difference between an estimate and a guess.
  2. Keep the branch shippable, always. A migration branch that doesn't build is a migration you can't measure and can't defend when priorities get questioned.
  3. Step through Angular majors; don't leap. The schematics are most of the value of the official upgrade path. Skipping them to "save time" costs more than it saves on any app of size.
  4. Consolidate dependencies while you're in there. Three social-login plugins became one. Every dependency you remove is one that can't abandon you in 2030.
  5. Patch unmaintained plugins deliberately. A versioned postinstall script beats a fork you'll own forever, and beats hand-edited node_modules by an even wider margin.
  6. Regression-test native flows on hardware, per platform, per layer. Push, Bluetooth, camera, social login - simulators lie about exactly the things a Capacitor upgrade changes.
  7. Keep porting master's changes as you go. One giant merge at month four is how long-lived branches die.

Frequently asked questions

Can you upgrade Ionic 5 straight to Ionic 7?

The Ionic package itself, mostly yes - the component APIs are surprisingly stable. But Ionic 7 requires a modern Angular, so in practice the question is really about the Angular migration underneath. We took Angular major-by-major with ng update, and moved Ionic and Capacitor once the framework was current. Trying to move everything in one leap is how these projects stall.

What breaks when upgrading Capacitor 2 to Capacitor 7?

Mostly the plugin ecosystem, not Capacitor itself. Capacitor 2-era apps lean on Cordova plugins and early community plugins, and many of those were abandoned years ago. Expect to replace social login, storage, analytics, and push plugins; expect AndroidX and Gradle changes, new runtime permission flows, and iOS privacy manifest requirements. Budget more time for plugins than for the framework.

Should I rewrite my legacy Ionic app instead of upgrading?

Usually not, if the app is in production and users depend on it. A rewrite means re-earning years of accumulated fixes and edge cases while shipping nothing, and most rewrites of working apps overrun badly. Upgrade in place on a long-lived branch, keeping the app shippable throughout. Rewrite only if the product itself is changing fundamentally - not just the framework version.

How long does an Ionic/Angular modernization take?

For an app of real size - dozens of pages, native plugins, multiple languages - plan for roughly three to five months of focused engineering, longer if the same team is also shipping features. The framework upgrade itself is the predictable half; auditing and replacing abandoned plugins, then regression-testing every native flow on both platforms, is where the time actually goes.

Work with Lumetha

Sitting on a legacy Ionic or Angular app? We've crossed this gap.

Modernization is exactly what our dedicated pods are built for: long-running, detail-heavy work that has to happen without stopping your releases. Start with a two-week paid pilot - we'll audit your plugin graveyard in week one and you'll know precisely what you're facing.

Book a 30-minute call →

Related: the full mySAME / myDeutz-Fahr case study · hire a dedicated development team · staff augmentation vs. dedicated team - an honest guide · the architecture of a cloud-to-printer IoT platform we engineered