Angular 20 to 21 upgrade, revised

Move TopFans from Angular 20.3 to 21.2 before the v20 LTS window closes on 2026-11-28. Replace @angular/fire with the Firebase SDK, bump the libraries whose installed release rejects Angular 21, and leave zone.js, NgModules, Karma, and quill alone. Done means the scripts CI actually runs are green, the SSR log is clean, and push notifications work for both a first-time and a returning user.

Branch feature/release-0.0.23/16-17-18-19-20-21, cut from 64a1a3d259. This revision folds in two independent audits of the first draft. Items marked changed are new or corrected. Sources: 21.0.0 release notes, update guide, GHSA-x288-3778-4hhx.

What changed from the first draft

Where the repo is today

ItemCurrentNotes
Angular core / CLI20.3.31 / 20.3.37Application builder, SSR via CommonEngine, prerender from prerender.routes.txt.
TypeScript5.8.321 needs >=5.9 <6.1. ng update moves it to 5.9.3.
Node / npm, dev machine24.5.0 / 12.0.1npm 12 warns it wants Node 24.15. Local annoyance only; see the CI row.
Node / npm, CI changednodejs: 20 / npm@10.8.1All four buildspec-*.yml files pin these and run npm install --legacy-peer-deps. Angular 21 needs Node ^20.19.0. Whether CodeBuild's "20" is 20.19 or later depends on the image. Unverified.
Install policy changed.npmrc: legacy-peer-deps=trueCommitted. npm ls exits 0 today because of it. CI passes the same flag explicitly.
BootstrapNgModule, zone.js 0.15.1platformBrowserDynamic().bootstrapModule(AppModule); AppServerModule imports AppModule.
TestsKarma configured, zero .spec.tsEvery TestBed item in the official checklist is a no-op.
tsconfigbundler, strict: false, strictTemplates: falsenoUncheckedIndexedAccess and noUnusedLocals are on.
SSR hostsallowedHosts set at server.ts:191Already satisfies the @angular/ssr 21.1.5 fix for GHSA-x288-3778-4hhx.
RouterCustom TrailingSlashUrlSerializerProvided as UrlSerializer at app.module.ts:98. Overrides parse, delegates serialize.
Working tree8 modified, 12 untrackedMarketing route data, environment.ts, and HTML plans. All of it blocks ng update.

Decisions

Replace @angular/fire with the firebase SDK. The only Angular 21 build is 21.0.0-rc.0, it depends on firebase ^12.4.0, and nothing newer has shipped. The repo uses four SDK functions and three providers. Calling firebase/messaging and firebase/remote-config directly removes the dependency and keeps Firebase at 11.10.0, the version installed today.

Do the Firebase swap first, on Angular 20. If push tokens break after the framework bump, the framework did it.

Use --force on both ng update commands and say why. changed ng update reads installed peerDependencies and refuses when any package rejects the target. @cotufa82/ng-ivs declares Angular 12, @khajegan/ngx-audio-player declares Angular 17 and Material 17, ngx-stripe declares Angular 18. Neither .npmrc nor overrides nor npm 12's packageExtensions affects that check. --force skips the check, not the migrations. It is the same situation the 20 upgrade was in.

Keep zone.js. 46 files touch NgZone. The 21 migration adds provideZoneChangeDetection() to AppModule on its own; that is the only change.

Keep Karma, skip Vitest. No spec files, nothing to run.

Leave ngx-quill 16.2.1 and quill 1.3.7. Peer is @angular/core >=13. ngx-quill 30 needs quill 2, which is its own project.

Upgrade rule. changed Bump a library when its installed release rejects Angular 21 or when a same-major patch exists for 21. Leave libraries with open peer ranges even if a newer major exists. That keeps ngx-scrollbar 9 (17 files use it; 19.1.5 exists with API changes), ngx-image-cropper 6, and ngx-sharebuttons 8 where they are.

Defer ng-ivs, ngx-audio-player, and ngx-stripe. All three already sit outside their declared range and run through the partial-compilation linker. ngx-stripe 21.9.0 would force @stripe/stripe-js to 9.x; the repo is on 3.5.0 and already outside ngx-stripe 18's own >=4 <5 range.

Stay on NgModules. No standalone, ngClass, or ngStyle migrations here.

Official 21 checklist, mapped to this repo

Every breaking change from the 21.0.0 release notes plus the CLI-level items. Each row says whether it applies and what to do.

ItemAppliesAction
ng update @angular/core@21 @angular/cli@21yesPhase 2, with --force. Lands core 21.2.23, CLI 21.2.24.
ng update @angular/material@21yesPhase 2, with --force because ngx-audio-player peers Material 17. Lands 21.2.14.
TypeScript 5.9 requiredyesng update bumps to 5.9.3. Watch noUncheckedIndexedAccess and noUnusedLocals.
Zone apps must add provideZoneChangeDetection()yesMandatory migration, runs without a prompt. Confirm it appears in AppModule.providers. Without it the app renders once and never updates.
Host binding type checking on by defaultverifyEight @HostBinding uses in five files: header.component.ts, header-user-menu.component.ts, header-layout.component.ts, profile-skeleton.component.ts, signup-by-refer.component.ts. Plus video-player-v2's five host class bindings. Fix types first; typeCheckHostBindings: false only if a fix changes behaviour.
Custom UrlSerializer query-param handling (#64449) changedverifyThe router now defers to the custom serializer for query params. TrailingSlashUrlSerializer only overrides parse, so this is probably fine, but test /username/?key=x, /creators?key=x, and a hash URL in Phase 6.
Router navigations take extra microtasksverify10 files subscribe to router.events. Smoke-test navigation, back/forward, lazy loads.
@angular/ssr 21.1.5 allowedHostsdonegetSsrAllowedHosts() in server.ts already covers it.
BootstrapContext for server bootstrapn/aOnly affects bootstrapApplication. CommonEngine still takes an NgModule type.
moduleId, interpolation, ngComponentOutletContent, ApplicationConfig import, ignoreChangesOutsideZone, lastSuccessfulNavigation, NgModuleFactory, UpgradeAdapter, emitDeclarationOnlyn/aZero occurrences of each.
New formArray directiven/aOnly formArrayName is used (35 templates).
Signal inputs on custom elementsn/a@angular/elements is in package.json but never imported. No signal inputs anywhere.
TestBed changesn/aNo spec files.

Dependency matrix

Angular

PackageNowTargetHow
@angular/* framework packages20.3.3121.2.23ng update
@angular/cli, @angular/ssr, @angular/pwa, @angular-devkit/build-angular20.3.3721.2.24ng update. karma and server builders still ship in 21.2.24.
@angular-devkit/architect0.2003.370.2102.24ng update. Own version scheme.
@angular/material, @angular/cdk20.2.1421.2.14ng update @angular/material@21 --force
typescript5.8.35.9.3ng update
zone.js0.15.1whatever ng update picksPeer is ~0.15 || ~0.16. Do not bump by hand.
@angular-eslint/*19.8.121.4.0Manual. Peer allows eslint ^8.57, so .eslintrc.json stays.

Firebase

PackageNowTargetHow
@angular/fire20.0.1removedPhase 1.
firebase11.10.0, transitive11.10.0, directExact version, not a range. Firebase 12 is a separate task.

Libraries that reject Angular 21 today

PackageNowTargetNote
primeng20.4.021.1.10Peers @angular/core ^21.0.7.
ngx-bootstrap20.0.221.2.2Peers ^21.2.0. Modal and datepicker are app-wide.
@ng-select/ng-select20.4.021.8.2Theme CSS path unchanged.
@ngrx/store20.1.021.1.1ng update @ngrx/store@21 has a schematic.
@ng-bootstrap/ng-bootstrap19.0.120.0.0Only NgbActiveModal is used. Also peers @angular/localize ^21, which ng update handles.
ngx-toastr19.1.020.0.5lazy-toastr bundle path unchanged.
ngx-mask20.0.321.1.0
ngx-owl-carousel-o20.1.121.0.3Carousel theme regressions were fixed in 35f314c117; re-check those views.
ngx-infinite-scroll20.0.021.0.0
@zxing/ngx-scanner20.0.021.0.0Peers TypeScript ^5.9; fine after Phase 2.
@fortawesome/angular-fontawesome changed2.0.14.0.0Depends on fontawesome-svg-core ^7.1.0. Bump fontawesome-svg-core, free-brands-svg-icons, free-solid-svg-icons to ^7.3.1 together or the tree carries two core majors. Only two icons are registered: faXTwitter, faCommentSms.
ngx-device-detector10.1.011.0.0

Left alone

Open peer range. Install cleanly on 21.

  • ngx-quill 16.2.1, quill 1.3.7, quill-emoji
  • @ctrl/ngx-emoji-mart 9.3.0
  • angular-mentions, ng-circle-progress, ng-otp-input
  • ngx-clipboard, ngx-color-picker, ngx-editor 13, ngx-image-compress
  • ngx-scrollbar 9 (19.1.5 exists, API changes, 17 files), ngx-image-cropper 6 (9.1.7 exists), ngx-sharebuttons 8 (17.0.0 exists). Newer majors are a separate task.
  • ngx-material-timepicker, ngx-intl-tel-input 15

Out of range, deferred, need --force.

  • @cotufa82/ng-ivs 0.1.0 declares Angular 12. No newer release.
  • @khajegan/ngx-audio-player 17.1.0 declares Angular 17 and Material 17. No newer release.
  • ngx-stripe 18.1.0 declares Angular 18 and @stripe/stripe-js >=4 <5. 21.9.0 needs stripe-js 9.x.

npm ls does not complain about these because .npmrc turns peer checking off. ng update checks anyway.

Firebase: the SDK without AngularFire

Everything Firebase-related is in two files plus the service worker. The public shape of FirebaseService does not change, so its 14 consumers are untouched.

FileChange
package.jsonRemove @angular/fire. Add "firebase": "11.10.0" to dependencies.
src/app/app.module.tsDelete provideFirebaseApp, provideMessaging, provideRemoteConfig and their imports. Initialization moves into the service.
src/app/modules/shared/services/firebase.service.tsReplace inject(Messaging) and the RemoteConfig constructor param with lazy, browser-only getters. Import initializeApp, getApps from firebase/app; getMessaging, getToken, onMessage, isSupported from firebase/messaging; getRemoteConfig, fetchAndActivate, getValue, isSupported from firebase/remote-config. Alias the two isSupported imports.
src/firebase-messaging-sw.jsNo change. Compat 9.6.1 from gstatic, never depended on AngularFire.
environment*.tsNo change.

Contract the rewritten service must keep

Sequence

Phase 0. Baseline and toolchain

Checkpoint: green build on Angular 20 with numbers written down and a known CI Node version.

Phase 1. Firebase SDK swap, still on Angular 20

Checkpoint: commit replace @angular/fire with firebase sdk. The framework has not moved, so any breakage here is ours.

Phase 2. Framework

ng update @angular/core@21 @angular/cli@21 --force
ng update @angular/material@21 --force

Checkpoint: commit angular 21 core, cli, material. The build may not be green yet.

Phase 3. Libraries

ng update @ngrx/store@21 --force
npm i primeng@21 ngx-bootstrap@21 @ng-select/ng-select@21 @ng-bootstrap/ng-bootstrap@20 \
  ngx-toastr@20 ngx-mask@21 ngx-owl-carousel-o@21 ngx-infinite-scroll@21 \
  @zxing/ngx-scanner@21 ngx-device-detector@11 \
  @fortawesome/angular-fontawesome@4 @fortawesome/fontawesome-svg-core@7 \
  @fortawesome/free-brands-svg-icons@7 @fortawesome/free-solid-svg-icons@7
npm i -D @angular-eslint/builder@21 @angular-eslint/eslint-plugin@21 \
  @angular-eslint/eslint-plugin-template@21 @angular-eslint/schematics@21 \
  @angular-eslint/template-parser@21

Checkpoint: commit angular 21 third-party libraries.

Phase 4. Make it compile

Checkpoint: commit fix compile and lint for angular 21. Dev build green.

Phase 5. The builds CI runs changed

The buildspecs run npm run $BUILD_ENV. Run the scripts that variable actually takes, not shorter stand-ins.

Checkpoint: every listed script green, sizes within budget, SSR log clean. Push the branch and let CodeBuild run it on Node 20 / npm 10.8.1.

Phase 6. Hands-on smoke test

Checkpoint: open the PR against feature/release-0.0.23/16-17-18-19-20 or the release branch the team prefers.

Risks

RiskWhy it matters hereMitigation
CI Node version changedLocal is Node 24.5, CI is Node 20. If the image's 20 is older than 20.19, the branch passes locally and fails in the pipeline.Phase 0 check. Bump buildspecs to 22 if needed.
SSR regressionsCustom server.ts with domino, a hand-rolled URL shim, installServerDom, prerender-first routing. Framework majors tend to bite this layer.Phase 0 baseline, Phase 5 route list, read the SSR log.
Host binding type errorsNew default in 21. strictTemplates is off, so these bindings have never been type-checked.Fix types; documented fallback flag.
Firebase behaviour driftAngularFire zone-wrapped calls, returned null on the server, and swallowed unsupported-browser errors. The raw SDK does none of that.Browser guards on both entry points, per-product isSupported(), NgZone.run(), two-state push test, Phase 1 before the framework moves.
Three libraries compiled against Angular 12, 17, 18They run through the partial-compilation linker. One more major is usually fine, not guaranteed.Smoke-test audio and IVS in Phase 6. If one breaks, pull the deferred task forward.
Router changesMicrotask timing, plus the custom-serializer fix in #64449.Phase 6 navigation and query-param cases.
White-label generatorGenerates angular.json projects from the API. If the template hardcodes 20-shaped options, every white-label build breaks at once.Generate and build one in Phase 5.

Open questions and assumptions

Out of scope