Skip to content
All field notes

3 min read

The Upgrade Week

Taking a shipping hybrid mobile app through Angular 20 and Capacitor 8 in one sprint — with the template compiler as the only safety net

  • Angular
  • Capacitor
  • Ionic
  • Mobile
  • Upgrades

In December we took the CattleUSA mobile app — shipping on the App Store, Google Play, and the web — through a stacked upgrade in one two-day blitz: Angular 19 to 20, NgRx 20, Material and CDK 20, Capacitor 7 to 8, new Sentry SDKs, a newer Android SDK with release minification finally turned on, and strictTemplates flipped from off to on. This post is about how we sequenced it, and about doing it with an honest handicap: at the time, the app had no meaningful test suite.

Why all at once

Conventional wisdom says upgrade one dependency at a time. For a hybrid app, the dependencies don't respect that: Capacitor major versions pin native SDK expectations, Angular majors pin TypeScript ranges, NgRx and Material track Angular in lockstep, and Sentry's SDKs care about all of the above. Spreading the upgrades over weeks means living in the incompatible middle for weeks. We chose the opposite: clear the calendar, land the whole coherent set, and bisect within the sprint if something breaks.

The sequencing that worked: framework first (Angular, NgRx, Material as one movement), then the native layer (Capacitor 7→8, regenerating both native projects and re-checking every plugin), then tooling (Sentry, Android build stack), then — deliberately last — strictTemplates.

The compiler was the test suite

Turning on strictTemplates after the version bumps was the most valuable decision of the sprint. With no unit tests to catch regressions, the template type-checker became the regression net: it walks every binding in every template and refuses to compile the ones whose types stopped lining up. The follow-up commit — titled, honestly, "resolve issues with stricttemplates" — was a tour of every place the app had been quietly passing the wrong shape into a component input or narrowing a nullable without checking it.

Strictness is a ratchet that substitutes for tests you don't have yet. It's not a replacement — the app got a real test suite (Vitest, CI gates, coverage) a few months later, and that's a story of its own — but if you're staring at a legacy app with zero tests and a big upgrade ahead, flipping on every compiler strictness flag you can afford is the highest-leverage hour you'll spend.

The parts nobody blogs about

  • Release minification, years late. The Android build had been shipping unminified because minification "broke something" once, long ago. Turning it on as part of the same sprint — while everything else was already under scrutiny — was the right time. It held.
  • Route names as an upgrade casualty. Tab routes were renamed to semantic paths in the same window, because touching every route file for the upgrade made the rename nearly free. Batching mechanical churn with upgrade churn keeps the noisy diffs in one place.
  • Device passes over simulator passes. Every upgrade claim got verified on physical hardware — the WebView, the plugins, and the keyboard behave differently on a real phone, and hybrid-app regressions live disproportionately in that gap.
  • One indulgence. The same week, a 5,000-line Phaser mini-game about a flying cow landed in the app. Morale is a dependency too.

The takeaway

The upgrade fortnight worked because we treated it as a project, not a chore: a fixed window, a deliberate order, compiler strictness as the safety net, and hardware verification at every step. Version-lockstep ecosystems punish the slow path more than the risky-looking fast one — the incompatible middle is where the real bugs live.