From 7c641a6d184625c2929b8376ef1a4b5b948f758b Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Sat, 31 Oct 2026 03:07:27 -0700 Subject: [PATCH 01/11] docs: say which wrapped calls do not hold the app unstable docs/zones.md says callback APIs hold the app unstable until an initial value, so a server render waits for data. #3827 makes a set of calls exceptions because their first callback may never come. Waiting on a callback that never comes would keep the app from ever becoming stable. List those calls in docs/zones.md, and point at onValue, object, list, listVal and fromRef with the value event as the calls that still make a server render wait. In docs/database.md, say the same under stateChanges, auditTrail and fromRef. Refs #3826 --- docs/database.md | 6 ++++++ docs/zones.md | 2 ++ 2 files changed, 8 insertions(+) diff --git a/docs/database.md b/docs/database.md index 1eac2244b..dcc1f39ea 100644 --- a/docs/database.md +++ b/docs/database.md @@ -100,6 +100,8 @@ The `listVal()` function creates an observable that emits an object mapped to it The `stateChanges()` function creates an observable that emits each time a change occurs at the reference or query passed. This is useful for tracking the changes in your list. The optional `events` parameter will filter which child events populate the array. +It emits only on child events, which an empty list never produces, so it does not hold the application unstable and a server render does not wait for it. Use `list()` when the render should wait for a list's initial contents. + | | | |-----------------|------------------------------------------------------| | **function** | `stateChanges(ref, options?)` | @@ -110,6 +112,8 @@ The `stateChanges()` function creates an observable that emits each time a chang The `auditTrail()` function creates an observable that emits the entire state trail. This is useful for debugging or replaying the state of a list in your app. The optional `events` parameter will filter which child events populate the array. +Like `stateChanges()`, it waits for child events, so it does not hold the application unstable and a server render does not wait for it. + | | | |-----------------|------------------------------------------------------| | **function** | `auditTrail(ref, options?)` | @@ -120,6 +124,8 @@ The `auditTrail()` function creates an observable that emits the entire state tr The `fromRef()` function creates an observable that emits reference changes. +With `ListenEvent.value` it holds the application unstable until the first value is emitted, so a server render waits for the data. With a child event it does not, because it may never emit. + | | | |-----------------|------------------------------------------| | **function** | `fromRef(ref, event)` | diff --git a/docs/zones.md b/docs/zones.md index 99aa0de58..ca1d8df4f 100644 --- a/docs/zones.md +++ b/docs/zones.md @@ -10,6 +10,8 @@ These wrappers ensure Firebase APIs are called outside of the Angular zone. This Observables, Promise-based APIs, and those with callbacks will purposely destabilize your application until a initial value is returned, this ensures that server-side rendering (SSR) and static site generation (SSG/pre-rendering) wait for data before rendering the page's HTML. +Some calls are exceptions. They are still zone wrapped, but they do not hold the application unstable, because their first callback may never come or a render has no reason to wait for them: `onLog`, `onTokenChanged`, `beforeAuthStateChanged`, `onMessage`, `onRegistered`, `onUnregistered`, `logEvent`, `isSupported`, and in the Realtime Database `onChildAdded`, `onChildChanged`, `onChildMoved`, `onChildRemoved`, `off`, `enableLogging`, `stateChanges`, `auditTrail`, and `fromRef` with a child event. To make a server render wait for Realtime Database data, use `onValue`, `object`, `list`, `listVal`, or `fromRef` with `ListenEvent.value`. + ## Consequences of not Zone wrapping When using a Firebase or RxFire API without importing from AngularFire or if AngularFire APIs are used outside of an injection context you _may_ experience instability. From d7cbf2c3457f9c44e2104f09b4ac715d40207278 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 4 Nov 2026 09:17:18 -0800 Subject: [PATCH 02/11] docs: describe firebase 13 support in the v22 upgrade guide #3831 lets AngularFire 22 accept firebase 12 or 13, and the ng update migration now writes the range of the firebase major already installed. The upgrade guide still said the migration writes ^12.18.0 and quoted peer warnings that name only that range. State the supported range and what the migration writes, update the Yarn 1 and pnpm 9/10 warnings to their current wording, and add a section on moving to firebase 13: VertexAIBackend is removed and its packages declare Node 24.12 or later. Refs #3830 --- docs/version-22-upgrade.md | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/docs/version-22-upgrade.md b/docs/version-22-upgrade.md index 693a4a7bc..8a9b6a512 100644 --- a/docs/version-22-upgrade.md +++ b/docs/version-22-upgrade.md @@ -1,6 +1,6 @@ # Upgrading to the AngularFire 22 release candidate -AngularFire 22 targets **Angular 22** and the **Firebase JS SDK v12**. Most of the upgrade is handled for you by `ng update`. +AngularFire 22 targets **Angular 22** and the **Firebase JS SDK v12 or v13**. Most of the upgrade is handled for you by `ng update`. ## Run the update @@ -14,8 +14,10 @@ Include `@angular/fire@next` in the same command as Angular. On its own, `ng upd The AngularFire update runs a migration that: -- **Adds** `firebase` **to your** `package.json` **if it is missing**, as `"firebase": "^12.18.0"`, and reinstalls. See the next section for why. -- **Aligns an existing** `firebase` **entry to** `^12.18.0` when it requests a version outside that range. +- **Adds** `firebase` **to your** `package.json` **if it is missing**, and reinstalls. See the next section for why. +- **Aligns an existing** `firebase` **entry** when it requests a version outside `^12.18.0 || ^13.0.0`, the range AngularFire 22 supports. + +Either way, the migration writes `^13.0.0` when firebase 13 is already installed and `^12.18.0` otherwise, so an app on firebase 12 stays on 12 and an app on firebase 13 stays on 13. ## Your app now owns the Firebase SDK @@ -23,7 +25,7 @@ Through AngularFire 21, `@angular/fire` listed `firebase` under `dependencies`, AngularFire 22 lists `firebase` under `peerDependencies` instead. So your app should declare `firebase` in its `package.json` (on Yarn it must, since Yarn installs no peer dependencies). This gives you exactly one copy of `firebase`, and an incompatible version is refused at install time instead of failing at runtime. -`ng update @angular/fire@next` updates an older `firebase` entry in your `package.json` to `^12.18.0` when it requests a version outside that range. If your app doesn't list `firebase` in `package.json` the migration adds `"firebase": "^12.18.0"` and reinstalls. Verify with `npm ls firebase`, which should show exactly one copy. +`ng update @angular/fire@next` updates an older `firebase` entry in your `package.json` when it requests a version outside `^12.18.0 || ^13.0.0`. If your app doesn't list `firebase` in `package.json`, the migration adds it and reinstalls. Verify with `npm ls firebase`, which should show exactly one copy. ### If you install with `npm install` instead of `ng update` @@ -49,14 +51,31 @@ Do not use `--force` or `--legacy-peer-deps` to get past this message. Both inst Yarn doesn't install peer dependencies. Add `firebase` to your own `package.json` as shown above, or run `ng update @angular/fire@next`, which adds it. -An older version installs anyway with a warning (`has incorrect peer dependency "firebase@^12.18.0"` on Yarn 1, `YN0060: INCOMPATIBLE_PEER_DEPENDENCY` on Yarn 2+), so treat that warning as an upgrade step you still have to do. +An older version installs anyway with a warning (`has incorrect peer dependency "firebase@^12.18.0 || ^13.0.0"` on Yarn 1, `YN0060: INCOMPATIBLE_PEER_DEPENDENCY` on Yarn 2+), so treat that warning as an upgrade step you still have to do. ### pnpm -pnpm 8 and later install a missing `firebase` automatically. An older version installs anyway with a warning (`unmet peer firebase@^12.18.0: found 11.x` on pnpm 9 and 10, `Issues with peer dependencies found. Run "pnpm peers check" to list them.` on pnpm 11 and 12), and pnpm prints that warning only once, when it first resolves the tree, never on a later install from the committed lockfile. +pnpm 8 and later install a missing `firebase` automatically. An older version installs anyway with a warning (`unmet peer firebase@"^12.18.0 || ^13.0.0": found 11.x` on pnpm 9 and 10, `Issues with peer dependencies found. Run "pnpm peers check" to list them.` on pnpm 11 and 12), and pnpm prints that warning only once, when it first resolves the tree, never on a later install from the committed lockfile. Set `strict-peer-dependencies=true` in `.npmrc` to make pnpm refuse the mismatch outright. +## Firebase 13 is optional + +AngularFire 22 works with firebase 12 (`^12.18.0`) and firebase 13 (`^13.0.0`). `ng update` keeps the firebase major your app already has, so moving to firebase 13 is a separate step you can take when you are ready: + +```bash +npm install firebase@^13.0.0 +``` + +Before you update, check these firebase 13 changes: + +- **`VertexAIBackend` is removed** from `firebase/ai`, and so from `@angular/fire/ai`. Use `AgentPlatformBackend`, which firebase 12.18 also exports, so you can switch before you upgrade. + - `VertexAIBackend` defaults to the `us-central1` location and `AgentPlatformBackend` to `global`, so replace `new VertexAIBackend()` with `new AgentPlatformBackend('us-central1')` to keep the same location. + - A call that already passes a location keeps it as is. +- **Node.js 24.12 or later.** firebase 13's packages declare `"node": ">=24.12.0"`, so on Node.js 22 npm prints `EBADENGINE` warnings when it installs them. + +For everything else that changed, see the [Firebase JavaScript SDK release notes](https://firebase.google.com/support/release-notes/js). + ## Fewer build-time packages `@angular/fire` no longer installs `@angular-devkit/schematics`, `@angular-devkit/core` or `@schematics/angular`. The Angular CLI supplies them when it runs `ng add` and `ng update`, so nothing changes for an Angular CLI or Nx workspace. From 40b07fa20a535aea88d6ebf5f7f1189ed2331732 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 4 Nov 2026 09:39:23 -0800 Subject: [PATCH 03/11] docs: describe microtask delivery in the v22 upgrade guide #3833 moves every Observable read from a setInterval timer to the next microtask checkpoint. As a breaking change it needs a section in the upgrade guide: which tests now see a value they used to wait for, the setTimeout ordering flip, and the zoneless server render case for hand-written TransferState entries. Refs #3832 --- docs/version-22-upgrade.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/version-22-upgrade.md b/docs/version-22-upgrade.md index 8a9b6a512..e93200fee 100644 --- a/docs/version-22-upgrade.md +++ b/docs/version-22-upgrade.md @@ -88,6 +88,19 @@ Deploying server-side rendering to Cloud Functions with `ng deploy` (a deploy ta Hosting-only deploys (`ssr` absent or `false`) and Cloud Run deploys (`ssr: 'cloud-run'`) are not deprecated. They no longer print the old "Legacy ng-deploy Firebase is deprecated" message. +## Observable reads deliver as microtasks + +Every Observable read (`collectionData`, `docData`, `authState`, `user`, `objectVal`, `listVal` and the rest) used to hand each value to your subscriber on a timer (`setInterval` with no delay), one event-loop turn later. It now does so at the next microtask checkpoint, which is sooner. The compat `AngularFireAuth` streams change the same way. What arrives, in which order, and when change detection runs do not change. + +What an upgrading app may notice: + +- A test that waits only for microtasks (a few awaited promises, vitest fake timers without advancing, `fakeAsync` with `flushMicrotasks()` alone) now sees a value whose source emits in a microtask, as Auth and a promise-based mock do. + - In a test that fakes timers, a Firestore value still arrives only after the clock is advanced with `tick()` or `vi.advanceTimersByTime()`, because the Firestore SDK hands each snapshot over inside a `setTimeout`. + - A test that already advances the clock, or waits for a real timer, sees the value exactly as before. +- Code that registers its own `setTimeout` when a read emits sees that timer run after the value arrives, not before. +- In a zoneless server render, a value arriving between the app reporting stable and serialization now reaches a `TransferState` entry you write by hand but not the HTML, because the zoneless change-detection pass is itself a later timer. + - Before this change such a value reached neither. AngularFire itself writes nothing into `TransferState`. + ## Other notes - **Node.js 20 is not supported.** Angular 22 requires Node.js `^22.22.3 || ^24.15.0 || >=26.0.0`. Node.js 20 reached end of life on 2026-04-30. From 196d4db920a5168ba1c6a9ed85d919852a4aa13c Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Wed, 4 Nov 2026 18:54:25 -0800 Subject: [PATCH 04/11] docs: describe the callback pending-task change in the v22 upgrade guide #3837 removes a callback read's pending task right after the callback instead of one setTimeout(fn, 0) delay later. As a breaking change it needs a section in the upgrade guide: what moves sooner, and which tests notice. Refs #3836 --- docs/version-22-upgrade.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/version-22-upgrade.md b/docs/version-22-upgrade.md index e93200fee..467c62d2d 100644 --- a/docs/version-22-upgrade.md +++ b/docs/version-22-upgrade.md @@ -101,6 +101,15 @@ What an upgrading app may notice: - In a zoneless server render, a value arriving between the app reporting stable and serialization now reaches a `TransferState` entry you write by hand but not the HTML, because the zoneless change-detection pass is itself a later timer. - Before this change such a value reached neither. AngularFire itself writes nothing into `TransferState`. +## Callback reads give up their pending task right after the callback + +Every callback read (`onSnapshot`, `onAuthStateChanged`, `onValue` and the other callback-taking functions wrapped with a pending task) used to keep Angular's pending task for one more `setTimeout(fn, 0)` delay after its first callback ran, and again after unsubscribe. It now removes the task right after the callback body returns, and right after the unsubscribe call. What the callback receives, and when change detection runs, do not change. + +### What an upgrading app may notice: + +- `ApplicationRef.whenStable()` and a server render follow a callback read's first value one `setTimeout(fn, 0)` delay sooner. +- A test that inspects Angular's pending tasks sees the task gone as soon as the callback has run, with no timer to advance. A test that awaits `whenStable()` under fake timers still needs the clock advanced, because Angular's own change-detection tick runs on a timer. + ## Other notes - **Node.js 20 is not supported.** Angular 22 requires Node.js `^22.22.3 || ^24.15.0 || >=26.0.0`. Node.js 20 reached end of life on 2026-04-30. From 1c85fbe1d0e0e77618b98cbbaf8478b71953f7c3 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Thu, 5 Nov 2026 14:23:22 -0800 Subject: [PATCH 05/11] docs: say a handler passed as an observer object is wrapped too #3829 wraps a handler passed as an observer object, such as { next, error }, the way it wraps a function, so the injection context reaches both forms. Refs #3828 --- docs/zones.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/zones.md b/docs/zones.md index ca1d8df4f..322b34041 100644 --- a/docs/zones.md +++ b/docs/zones.md @@ -60,7 +60,7 @@ When you call a wrapped API, the first thing AngularFire does is ask Angular for That division is worth knowing, because it explains what you are and are not responsible for: * **You** supply the context at the call site, from a field initializer, a constructor, or an explicit `runInInjectionContext`. -* **AngularFire** re-enters the environment injector inside a callback you hand to the call itself, which is why you never wrap an `onSnapshot` handler yourself. That reaches anything provided at the application level, but not providers declared on a component, and it does not extend to subscribers of a returned Observable, which is what the `switchMap` and `runInInjectionContext` example above is for. +* **AngularFire** re-enters the environment injector inside a callback you hand to the call itself, whether you pass it as a function or as an observer object such as `{ next, error }`, which is why you never wrap an `onSnapshot` handler yourself. That reaches anything provided at the application level, but not providers declared on a component, and it does not extend to subscribers of a returned Observable, which is what the `switchMap` and `runInInjectionContext` example above is for. What you lose depends on the call. A call that does asynchronous work, such as `onSnapshot`, `getDoc` or `collectionData`, is normally added to Angular's `PendingTasks` register, which is what server-side rendering waits on before it serializes the page. Called outside a context, it never is. A call that returns immediately, such as `getFirestore`, was never registered anyway, so it loses only the zone handling. From 53ffc4f77045afc1bb1a48c0e800298674b90ddb Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Thu, 5 Nov 2026 16:47:25 -0800 Subject: [PATCH 06/11] docs(compat): add a setup section in NgModule and standalone form The compat API's setup steps left docs/install-and-setup.md when #3236 rewrote it for the modular API, and nothing replaced them. docs/compat.md had no setup at all, the Realtime Database pages pointed at an "Installation guide" that no longer covered compat, and no page showed the standalone form. A standalone app with only the modular providers then fails its first compat injection with NG0201 for angularfire2.app.options (#3683). The new section on docs/compat.md gives both forms, says when the modular providers may stay, states the error the missing module produces, and lists the compat module of each product. The twelve compat pages that had an NgModule-only snippet or no setup at all now point at it. Fixes #3683 --- docs/compat.md | 69 ++++++++++++++++++++ docs/compat/analytics/getting-started.md | 2 + docs/compat/auth/getting-started.md | 2 + docs/compat/firestore/collections.md | 2 + docs/compat/firestore/documents.md | 2 + docs/compat/firestore/offline-data.md | 2 + docs/compat/functions/functions.md | 2 + docs/compat/messaging/messaging.md | 2 + docs/compat/performance/getting-started.md | 2 + docs/compat/remote-config/getting-started.md | 2 + docs/compat/rtdb/lists.md | 2 +- docs/compat/rtdb/objects.md | 4 +- docs/compat/storage/storage.md | 2 + 13 files changed, 92 insertions(+), 3 deletions(-) diff --git a/docs/compat.md b/docs/compat.md index 11bba4496..3364538b0 100644 --- a/docs/compat.md +++ b/docs/compat.md @@ -15,6 +15,75 @@ The compatibility API (`@angular/fire/compat/*`) is still supported. It gets bug Some of its tests are turned off right now: the ones for `AngularFirestoreCollection`, `AngularFirestoreCollectionGroup` and `AngularFirestoreDocument`, and one Cloud Storage upload test. Until [#3822](https://github.com/angular/angularfire/issues/3822) turns them back on, a bug in those APIs may not be caught before a release, so please report any you find. +### Setup + +Install AngularFire and Firebase with `ng add @angular/fire`, as in [the quickstart](install-and-setup.md#2-install-angularfire-and-firebase) (its "Before you begin" notes apply here too), or with `npm install @angular/fire firebase`. + +At the `ng add` feature prompt, select the products you use: `ng add` then writes the modular providers (`provideFirebaseApp(...)` and one per selected feature) into your app configuration with your Firebase configuration inlined, and with nothing selected it installs the packages and sets up nothing. + +The compat API does not use the modular providers. It needs `AngularFireModule.initializeApp(...)` with your Firebase configuration, plus the compat module of each Firebase product you use. + +The modular providers can stay if `AngularFireModule.initializeApp(...)` gets the same configuration object, both APIs then share one Firebase app. With two different configurations, whichever API starts second fails with `app/duplicate-app`. + +In a standalone application, add them to `app.config.ts`: + +```ts +import { ApplicationConfig, importProvidersFrom } from '@angular/core'; +import { AngularFireModule } from '@angular/fire/compat'; +import { AngularFireDatabaseModule } from '@angular/fire/compat/database'; + +export const appConfig: ApplicationConfig = { + providers: [ + importProvidersFrom( + AngularFireModule.initializeApp({ /* your Firebase configuration */ }), + AngularFireDatabaseModule, + ), + ], +}; +``` + +In an NgModule application, import them in `app-module.ts`: + +```ts +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { AngularFireModule } from '@angular/fire/compat'; +import { AngularFireDatabaseModule } from '@angular/fire/compat/database'; +import { App } from './app'; + +@NgModule({ + declarations: [App], + imports: [ + BrowserModule, + AngularFireModule.initializeApp({ /* your Firebase configuration */ }), + AngularFireDatabaseModule, + ], + bootstrap: [App], +}) +export class AppModule {} +``` + +Without `AngularFireModule.initializeApp(...)`, injecting a compat service fails with `NG0201`, which a development build reports as: + +``` +NG0201: No provider found for `InjectionToken angularfire2.app.options`. +``` + +The compat module for each product: + +| Product | Module | Import from | +|---|---|---| +| Analytics | `AngularFireAnalyticsModule` | `@angular/fire/compat/analytics` | +| Authentication | `AngularFireAuthModule` | `@angular/fire/compat/auth` | +| Authentication route guards | `AngularFireAuthGuardModule` | `@angular/fire/compat/auth-guard` | +| Cloud Firestore | `AngularFirestoreModule` | `@angular/fire/compat/firestore` | +| Cloud Functions | `AngularFireFunctionsModule` | `@angular/fire/compat/functions` | +| Cloud Messaging | `AngularFireMessagingModule` | `@angular/fire/compat/messaging` | +| Cloud Storage | `AngularFireStorageModule` | `@angular/fire/compat/storage` | +| Performance Monitoring | `AngularFirePerformanceModule` | `@angular/fire/compat/performance` | +| Realtime Database | `AngularFireDatabaseModule` | `@angular/fire/compat/database` | +| Remote Config | `AngularFireRemoteConfigModule` | `@angular/fire/compat/remote-config` | + ### Monitor usage of your application in production > `AngularFireAnalytics` provides a convenient method of interacting with Google Analytics in your Angular application. The provided `ScreenTrackingService` and `UserTrackingService` automatically log events when you're using the Angular Router or Firebase Authentication respectively. [Learn more about Google Analytics](https://firebase.google.com/docs/analytics). diff --git a/docs/compat/analytics/getting-started.md b/docs/compat/analytics/getting-started.md index 3c5af8aaf..d4d8acbf2 100644 --- a/docs/compat/analytics/getting-started.md +++ b/docs/compat/analytics/getting-started.md @@ -40,6 +40,8 @@ import { AngularFireAnalyticsModule } from '@angular/fire/compat/analytics'; export class AppModule { } ``` +For a standalone application, and the compat module of every product, see the [compat guide's Setup section](../../compat.md#setup). + `AngularFireAnalyticsModule` will dynamically import and configure `firebase/analytics`. A `page_view` event will automatically be logged (see `CONFIG` below if you wish to disable this behavior.) In your component you can then dependency inject `AngularFireAnalytics` and make calls against the SDK: diff --git a/docs/compat/auth/getting-started.md b/docs/compat/auth/getting-started.md index 48aed34b6..368306658 100644 --- a/docs/compat/auth/getting-started.md +++ b/docs/compat/auth/getting-started.md @@ -8,6 +8,8 @@ the Firebase docs for more information on what methods are available.](https://f > **NOTE**: [AngularFire has a new tree-shakable API](../../../README.md#developer-guide), you're looking at the documentation for the compatability version of the library. [See the v7 upgrade guide for more information on this change.](../../version-7-upgrade.md). +Setup for the compat API, `AngularFireModule.initializeApp(...)` plus `AngularFireAuthModule`, is in the [compat guide's Setup section](../../compat.md#setup). + **Example app:** ```ts diff --git a/docs/compat/firestore/collections.md b/docs/compat/firestore/collections.md index 709a969c5..95f50e95e 100644 --- a/docs/compat/firestore/collections.md +++ b/docs/compat/firestore/collections.md @@ -5,6 +5,8 @@ Each *document* contains a set of key-value pairs. Cloud Firestore is optimized > **NOTE**: [AngularFire has a new tree-shakable API](../../../README.md#developer-guide), you're looking at the documentation for the compatability version of the library. [See the v7 upgrade guide for more information on this change.](../../version-7-upgrade.md). +Setup for the compat API, `AngularFireModule.initializeApp(...)` plus `AngularFirestoreModule`, is in the [compat guide's Setup section](../../compat.md#setup). + ## Using `AngularFirestoreCollection` The `AngularFirestoreCollection` service is a wrapper around the native Firestore SDK's [`CollectionReference`](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.CollectionReference) and [`Query`](https://firebase.google.com/docs/reference/js/v8/firebase.firestore.Query) types. It is a generic service that provides you with a strongly typed set of methods for manipulating and streaming data. This service is designed for use as an `@Injectable()`. diff --git a/docs/compat/firestore/documents.md b/docs/compat/firestore/documents.md index c59624685..5bab5ba8c 100644 --- a/docs/compat/firestore/documents.md +++ b/docs/compat/firestore/documents.md @@ -5,6 +5,8 @@ Each *document* contains a set of key-value pairs. Cloud Firestore is optimized > **NOTE**: [AngularFire has a new tree-shakable API](../../../README.md#developer-guide), you're looking at the documentation for the compatability version of the library. [See the v7 upgrade guide for more information on this change.](../../version-7-upgrade.md). +Setup for the compat API, `AngularFireModule.initializeApp(...)` plus `AngularFirestoreModule`, is in the [compat guide's Setup section](../../compat.md#setup). + ## Using `AngularFirestoreDocument` The `AngularFirestoreDocument` service is a wrapper around the native Firestore SDK's [`DocumentReference` type](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference). It is a generic service that provides you with a strongly typed set of methods for manipulating and streaming data. This service is designed for use as an `@Injectable()`. diff --git a/docs/compat/firestore/offline-data.md b/docs/compat/firestore/offline-data.md index 3e16b845c..5698d584c 100644 --- a/docs/compat/firestore/offline-data.md +++ b/docs/compat/firestore/offline-data.md @@ -32,6 +32,8 @@ export class AppModule {} ``` +For a standalone application, and the compat module of every product, see the [compat guide's Setup section](../../compat.md#setup). + While offline your listeners will receive listen events when the locally cached data changes. You can use to Documents and Collections normally. To check whether you're receiving data from the server or the cache, use the `fromCache` property on the `SnapshotMetadata` in your snapshot event. If `fromCache` is true, the data came from the cache and might be stale or incomplete. If `fromCache` is false, the data is complete and current with the latest updates on the server. diff --git a/docs/compat/functions/functions.md b/docs/compat/functions/functions.md index 708788ca7..b43c9f70d 100644 --- a/docs/compat/functions/functions.md +++ b/docs/compat/functions/functions.md @@ -28,6 +28,8 @@ import { environment } from '../environments/environment'; export class AppModule {} ``` +For a standalone application, and the compat module of every product, see the [compat guide's Setup section](../../compat.md#setup). + ### Injecting the AngularFireFunctions service Once the `AngularFireFunctionsModule` is registered you can inject the `AngularFireFunctions` service. diff --git a/docs/compat/messaging/messaging.md b/docs/compat/messaging/messaging.md index 593e59003..ec8e88377 100644 --- a/docs/compat/messaging/messaging.md +++ b/docs/compat/messaging/messaging.md @@ -38,6 +38,8 @@ import { environment } from '../environments/environment'; export class AppModule {} ``` +For a standalone application, and the compat module of every product, see the [compat guide's Setup section](../../compat.md#setup). + ### Setting up the Firebase Messaging Service Worker There are two parts to Firebase Messaging, a Service Worker and the DOM API. AngularFireMessaging allows you to request permission, get tokens, delete tokens, and subscribe to messages on the DOM side. To register to receive notifications you need to set up the Service Worker. [The official Firebase documentation for setting up the details exactly how to do that](https://firebase.google.com/docs/cloud-messaging/js/client). diff --git a/docs/compat/performance/getting-started.md b/docs/compat/performance/getting-started.md index c435693bf..0d6bc422d 100644 --- a/docs/compat/performance/getting-started.md +++ b/docs/compat/performance/getting-started.md @@ -27,6 +27,8 @@ import { environment } from '../environments/environment'; export class AppModule {} ``` +For a standalone application, and the compat module of every product, see the [compat guide's Setup section](../../compat.md#setup). + The page load trace breaks down into the following default metrics: * [first paint traces](https://firebase.google.com/docs/perf-mon/automatic-web#first-paint) — measure the time between when the user navigates to a page and when any visual change happens diff --git a/docs/compat/remote-config/getting-started.md b/docs/compat/remote-config/getting-started.md index ab4bfa049..eff629b2c 100644 --- a/docs/compat/remote-config/getting-started.md +++ b/docs/compat/remote-config/getting-started.md @@ -132,3 +132,5 @@ constructor(remoteConfig: AngularFireRemoteConfig) { remoteConfig.lastFetchStatus.then(status => ...); } ``` + +For a standalone application, and the compat module of every product, see the [compat guide's Setup section](../../compat.md#setup). diff --git a/docs/compat/rtdb/lists.md b/docs/compat/rtdb/lists.md index 084c54eeb..fcc0297cb 100644 --- a/docs/compat/rtdb/lists.md +++ b/docs/compat/rtdb/lists.md @@ -10,7 +10,7 @@ The guide below demonstrates how to retrieve, save, and remove data as lists. ## Injecting the `AngularFireDatabase` service -**Make sure you have bootstrapped your application for AngularFire. See the Installation guide for bootstrap setup.** +**Make sure you have set up AngularFire for the compat API, see [Setup](../../compat.md#setup).** AngularFireDatabase is a service which can be injected through the constructor of your Angular component or `@Injectable()` service. In the previous step, we modified the `/src/app/app.component.ts` to retrieve data as an object. In this step, let's start with a clean slate. diff --git a/docs/compat/rtdb/objects.md b/docs/compat/rtdb/objects.md index 76c9725ee..b68ca21cb 100644 --- a/docs/compat/rtdb/objects.md +++ b/docs/compat/rtdb/objects.md @@ -10,11 +10,11 @@ The guide below demonstrates how to retrieve, save, and remove data as objects. ## Injecting the `AngularFireDatabase` service -**Make sure you have bootstrapped your application for AngularFire. See the Installation guide for bootstrap setup.** +**Make sure you have set up AngularFire for the compat API, see [Setup](../../compat.md#setup).** `AngularFireDatabase` is a service which can be injected through the constructor of your Angular component or `@Injectable()` service. -If you've followed the earlier step "Installation and Setup" your `/src/app/app.component.ts` should look like below. +With the [Setup](../../compat.md#setup) done, inject `AngularFireDatabase` into a component, for example: ```ts import { Component } from '@angular/core'; diff --git a/docs/compat/storage/storage.md b/docs/compat/storage/storage.md index 867805041..22b0e8ba4 100644 --- a/docs/compat/storage/storage.md +++ b/docs/compat/storage/storage.md @@ -28,6 +28,8 @@ import { environment } from '../environments/environment'; export class AppModule {} ``` +For a standalone application, and the compat module of every product, see the [compat guide's Setup section](../../compat.md#setup). + The `BUCKET` injection token can be used to customise the storage bucket. ```ts From 02e529c56a9b4393d1551250df9bde49a693d5a7 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Fri, 6 Nov 2026 14:49:03 -0800 Subject: [PATCH 07/11] docs: say which package managers refuse an incompatible firebase The v22 upgrade guide said an incompatible firebase is refused at install time. Only npm refuses it, and only when package.json requests the incompatible version. Yarn and pnpm install it with a warning. --- docs/version-22-upgrade.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/version-22-upgrade.md b/docs/version-22-upgrade.md index 467c62d2d..ea9e19e8a 100644 --- a/docs/version-22-upgrade.md +++ b/docs/version-22-upgrade.md @@ -23,7 +23,7 @@ Either way, the migration writes `^13.0.0` when firebase 13 is already installed Through AngularFire 21, `@angular/fire` listed `firebase` under `dependencies`, so npm could install a second copy of the SDK under `node_modules/` whenever your app asked for a different version. The two copies reject each other's objects at runtime, with errors such as `Expected type '_Query', but it was: a custom _CollectionReference object` or `toDate is not a function`. -AngularFire 22 lists `firebase` under `peerDependencies` instead. So your app should declare `firebase` in its `package.json` (on Yarn it must, since Yarn installs no peer dependencies). This gives you exactly one copy of `firebase`, and an incompatible version is refused at install time instead of failing at runtime. +AngularFire 22 lists `firebase` under `peerDependencies` instead. So your app should declare `firebase` in its `package.json` (on Yarn it must, since Yarn installs no peer dependencies). This gives you exactly one copy of `firebase`. When your `package.json` requests an incompatible version, npm refuses to install it and Yarn and pnpm warn (see below), instead of the app failing at runtime. `ng update @angular/fire@next` updates an older `firebase` entry in your `package.json` when it requests a version outside `^12.18.0 || ^13.0.0`. If your app doesn't list `firebase` in `package.json`, the migration adds it and reinstalls. Verify with `npm ls firebase`, which should show exactly one copy. From 7f6566951cd9d4935d199c6bbc0e072d0c9b559d Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Fri, 6 Nov 2026 17:02:37 -0800 Subject: [PATCH 08/11] docs: say which Firestore functions are not zone wrapped #3842 re-exports Firestore's query and reference builders, field values, comparators and setLogLevel without the zone wrapper. In docs/zones.md, add a paragraph listing them, change the switchMap example to a read, since building the query no longer warns, and say the Verbose log level covers only wrapped calls. Refs #3842 --- docs/zones.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/zones.md b/docs/zones.md index 322b34041..7bbe8a6cb 100644 --- a/docs/zones.md +++ b/docs/zones.md @@ -12,6 +12,8 @@ Observables, Promise-based APIs, and those with callbacks will purposely destabi Some calls are exceptions. They are still zone wrapped, but they do not hold the application unstable, because their first callback may never come or a render has no reason to wait for them: `onLog`, `onTokenChanged`, `beforeAuthStateChanged`, `onMessage`, `onRegistered`, `onUnregistered`, `logEvent`, `isSupported`, and in the Realtime Database `onChildAdded`, `onChildChanged`, `onChildMoved`, `onChildRemoved`, `off`, `enableLogging`, `stateChanges`, `auditTrail`, and `fromRef` with a child event. To make a server render wait for Realtime Database data, use `onValue`, `object`, `list`, `listVal`, or `fromRef` with `ListenEvent.value`. +Some functions are not wrapped at all, because they only build or compare values and schedule no work. In `@angular/fire/firestore` and `@angular/fire/firestore/lite` these are the query and reference builders such as `where`, `orderBy`, `query`, `collection` and `doc`, field values such as `increment` and `serverTimestamp`, the `*Equal` comparators, and Firestore's own `setLogLevel`. They are the Firebase SDK's own functions, so they never destabilize your application and never print the warnings described under Logging, wherever you call them. + ## Consequences of not Zone wrapping When using a Firebase or RxFire API without importing from AngularFire or if AngularFire APIs are used outside of an injection context you _may_ experience instability. @@ -22,7 +24,7 @@ There are a number of situations where AngularFire's Zone wrapping is inconseque ## Keeping calls inside an injection context -A common way to trip the warning is calling an AngularFire API from inside an asynchronous callback, for example building a Firestore query inside a `switchMap` on the signed-in user. The surrounding service is created inside an injection context, but the callback runs *later*, after that context is gone, so AngularFire can no longer wrap the call. +A common way to trip the warning is calling an AngularFire API from inside an asynchronous callback, for example reading a Firestore query with `collectionData` inside a `switchMap` on the signed-in user. The surrounding service is created inside an injection context, but the callback runs *later*, after that context is gone, so AngularFire can no longer wrap the call. Capture an `EnvironmentInjector` while the context is still active (any field initializer or constructor), then re-establish it inside the callback with [`runInInjectionContext`](https://angular.dev/api/core/runInInjectionContext): @@ -74,7 +76,7 @@ There are three logging levels in AngularFire: * **Silent**: when the logging level is set to silent only the above banner is displayed when AngularFire APIs are called outside of an injection context, this is the default when using Zoneless change-detection. * **Warn**: when the logging level is set to warn, only blocking reads, long-lived tasks, and APIs with high risk of destabilizing your application are logged, this is the default when using ZoneJS. -* **Verbose**: when the logging level is set to verbose, all AngularFire APIs called outside of an injection context are logged—helping you track down APIs that may be destabilizing your application +* **Verbose**: when the logging level is set to verbose, every wrapped AngularFire API called outside of an injection context is logged, helping you track down APIs that may be destabilizing your application. The unwrapped functions described above never log. You can change the log-level like so: From 7d4a339f954e8b85a2d2c8867d9d4a030154faf0 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Tue, 10 Nov 2026 14:19:54 -0800 Subject: [PATCH 09/11] docs(auth): build the server app with initializeBrowserOrServerApp The server-rendering guide's step 4 becomes the one call, the five details become three, the App Check token gets a relay section, the response gets a Cache-Control header, and the manual callback stays in a "Building the app yourself" section that step 4 points at, for apps that prefer it or whose AngularFire version has no such function. The release modes say what a throwing ngOnDestroy does to a timed release. --- docs/app-check.md | 4 +- docs/auth.md | 203 ++++++++++++++++++++++++++++--------- docs/version-22-upgrade.md | 1 + 3 files changed, 157 insertions(+), 51 deletions(-) diff --git a/docs/app-check.md b/docs/app-check.md index 3b8bb7b9d..d49ea3d60 100644 --- a/docs/app-check.md +++ b/docs/app-check.md @@ -52,7 +52,9 @@ App Check's reCAPTCHA providers need a browser, so AngularFire skips App Check d ### Firebase requests made during server rendering -With no App Check instance on the server, requests to Firebase made while the server renders a page carry no App Check token. If you [enforce App Check](https://firebase.google.com/docs/app-check/enable-enforcement) for a Firebase product, those requests are rejected. Firebase's recommended fix is to hand a token from the browser to the server and pass it to `initializeServerApp` as `appCheckToken`, described in [Use App Check in SSR environments](https://firebase.google.com/docs/web/ssr-apps#use-app-check). +With no App Check instance on the server, requests to Firebase made while the server renders a page carry no App Check token unless the browser relays one. If you [enforce App Check](https://firebase.google.com/docs/app-check/enable-enforcement) for a Firebase product, requests without a token are rejected. The fix is to hand the browser's token to the server and build the server app with `initializeBrowserOrServerApp`, which passes it on as `appCheckToken`. [Relaying the App Check token](auth.md#relaying-the-app-check-token) in the Authentication guide walks through it. A relayed token wins over `APP_CHECK_ON_SERVER` for Firestore, which sends the server app's token whatever App Check instance the server holds. + +In development mode the server console says so once per process: "AngularFire skips App Check during server rendering, so server renders without a relayed App Check token send none", followed by the two ways to change that. ### Running App Check during server rendering diff --git a/docs/auth.md b/docs/auth.md index ca0123790..dc21c6474 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -57,7 +57,7 @@ Update the imports from `import { ... } from 'firebase/auth'` to `import { ... } ## Server-side Rendering -When Angular renders your app on the server, the server does not know which user is visiting. To render the page as that user, pass their Auth ID token to `initializeServerApp`, which gives you a Firebase app that is already signed in as them. +When Angular renders your app on the server, the server does not know which user is visiting. To render the page as that user, hand their Auth ID token to `initializeBrowserOrServerApp`, which gives you a Firebase app that is already signed in as them on the server, and the ordinary Firebase app in the browser. Getting the token to the server is your app's job. This guide keeps it in a cookie, because the browser attaches cookies to every request on its own. @@ -78,6 +78,12 @@ export const serverRoutes: ServerRoute[] = [ The rest of this guide has no effect on routes rendered any other way. +The request context that step 3 fills and step 4 reads is provided by `AngularNodeAppEngine` and `AngularAppEngine`, the engines `ng new --ssr` sets up. An app still rendering through the older `CommonEngine` has to provide it itself, on every render: + +```ts +engine.render({ ...renderOptions, providers: [{ provide: REQUEST_CONTEXT, useValue: { authIdToken } }] }) +``` + ### 2. Keep the ID token in a cookie Install [js-cookie](https://github.com/js-cookie/js-cookie): @@ -173,6 +179,8 @@ import cookieParser from 'cookie-parser'; app.use(cookieParser()); app.use((req, res, next) => { + // A page rendered for one visitor must never be served to another from a cache. + res.setHeader('Cache-Control', 'private, no-store'); angularApp .handle(req, { authIdToken: req.cookies?.__session }) .then((response) => @@ -184,26 +192,17 @@ app.use((req, res, next) => { Keep it where the generated block already was, below the block that serves static files, so real files are still served before Angular tries to render them. The rest of the file, including the part that starts the server, stays as it is. -The second argument to `handle` is what the render reads back as `REQUEST_CONTEXT`. +The second argument to `handle` is what the render reads back as `REQUEST_CONTEXT`, and `initializeBrowserOrServerApp` reads the `authIdToken` key from it. If you forget the second argument, AngularFire says so once in the server console in development mode. + +The `Cache-Control` header is not optional. A signed-in render puts that visitor's data into the HTML, so a CDN or proxy that cached the response would hand it to the next visitor. -### 4. Build the server app from the token +### 4. Build the app with `initializeBrowserOrServerApp` -In `app.config.ts`, choose the Firebase app based on where the code is running, and pass that app to every Firebase provider: +In `app.config.ts`, build the Firebase app with `initializeBrowserOrServerApp` and pass that app to every Firebase provider: ```ts -import { - ApplicationConfig, - PLATFORM_ID, - REQUEST_CONTEXT, - inject, -} from '@angular/core'; -import { isPlatformBrowser } from '@angular/common'; -import { - FirebaseApp, - initializeApp, - initializeServerApp, - provideFirebaseApp, -} from '@angular/fire/app'; +import { ApplicationConfig, inject } from '@angular/core'; +import { FirebaseApp, initializeBrowserOrServerApp, provideFirebaseApp } from '@angular/fire/app'; import { getAuth, provideAuth } from '@angular/fire/auth'; import { getFirestore, provideFirestore } from '@angular/fire/firestore'; @@ -211,21 +210,7 @@ const firebaseConfig = { /* ...your Firebase configuration... */ }; export const appConfig: ApplicationConfig = { providers: [ - provideFirebaseApp(() => { - if (isPlatformBrowser(inject(PLATFORM_ID))) { - return initializeApp(firebaseConfig); - } - const requestContext = inject(REQUEST_CONTEXT, { optional: true }) as - | { authIdToken?: string } - | null; - if (!requestContext?.authIdToken) { - return initializeApp(firebaseConfig); - } - return initializeServerApp(firebaseConfig, { - authIdToken: requestContext.authIdToken, - releaseOnDeref: requestContext, - }); - }), + provideFirebaseApp(() => initializeBrowserOrServerApp(firebaseConfig)), provideAuth(() => getAuth(inject(FirebaseApp))), provideFirestore(() => getFirestore(inject(FirebaseApp))), // ... @@ -233,42 +218,160 @@ export const appConfig: ApplicationConfig = { }; ``` -#### Five details make this work +`initializeBrowserOrServerApp` returns the right kind of app for where the code is running: + +- In the browser, the ordinary Firebase app, as `initializeApp(firebaseConfig)` would. +- On the server, when the request context carries an `authIdToken`, a [`FirebaseServerApp`](https://firebase.google.com/docs/web/ssr-apps) signed in as that visitor. The same goes for an `appCheckToken`, see [Relaying the App Check token](#relaying-the-app-check-token). +- On the server with neither token, which is what prerendering and a signed-out visitor look like, the ordinary Firebase app, so the page renders signed out. +- On the server with a token Firebase cannot parse, the ordinary Firebase app after a warning in the server console, so the page renders signed out instead of failing the request. + +#### Three details make this work + +- **Keep exactly one `provideFirebaseApp`.** AngularFire hands you the app you provided only when a single one is registered, and falls back to the default app otherwise. With a second registration anywhere in your configuration, a signed-in render asks for a default app that does not exist on a freshly started server and fails. +- **Pass `inject(FirebaseApp)` to every provider, not just `provideAuth`.** `ng add @angular/fire` writes them without an argument, which resolves the default app. On a signed-in request the app is a server app instead, so a provider that asks for the default app fails outright on a freshly started server. +- **Never call `deleteApp` on the injected app.** `initializeBrowserOrServerApp` releases the server app itself. A second release from your code can delete an app another render is still using. + +#### When the server app is released + +By default the server app is released once the render has been garbage collected, which is also what the manual setup below does. Until then, a repeat request from the same visitor, such as a reload or a link opened in a new tab, reuses the app, and that reuse is what keeps such a request as fast as a signed-out one. A fresh server app costs a new Firestore connection and an account lookup, about 300 to 400 ms before the HTML is sent. + +To release at a fixed time instead, pass `releaseAfterRender`: `0` releases the app when the render ends, which bounds memory to the renders in flight and pays that cost on every repeat request, and a positive number keeps the app for that many milliseconds after the render ends. + +```ts +provideFirebaseApp(() => initializeBrowserOrServerApp(firebaseConfig, { releaseAfterRender: 60_000 })), +``` -- **Keep exactly one `provideFirebaseApp`.** AngularFire hands you the app you provided only when a single one is registered, and falls back to the default app otherwise. A second registration anywhere in your configuration would make the server app be silently ignored. -- **Pass `inject(FirebaseApp)` to every provider, not just `provideAuth`.** `ng add @angular/fire` writes them without an argument, which resolves the default app. On a signed-in request the factory above builds a server app instead, so a provider that asks for the default app fails outright on a freshly started server. -- **Keep the signed-out fallback.** There is no request context when Angular prerenders a page, and no token when the visitor is signed out, so the fallback builds an ordinary Firebase app and the page renders signed out. -- **Pass `releaseOnDeref`.** It tells the SDK when it may release the server app. The SDK watches the object you give it and releases once that object is garbage collected, so pass one that lives exactly as long as the render, such as the request context itself. Leave it out and the SDK requires you to call `deleteApp` yourself for each server app you create. -- **The cast is needed** because Angular types `REQUEST_CONTEXT` as `unknown`. +A runtime without `FinalizationRegistry` cannot release at garbage collection, so the app is released when the render ends there whatever the setting. -AngularFire's [sample app](https://github.com/angular/angularfire/tree/main/sample) does this differently, giving the browser and the server their own `app.config.client.ts` and `app.config.server.ts` instead of deciding at runtime, inside a single `provideFirebaseApp` factory, which of `initializeApp` and `initializeServerApp` to call. That is also fine, and it keeps the server-only code out of the browser bundle, at the cost of an extra file to wire up. +With `releaseAfterRender` set, the release runs when Angular destroys the render, after your services' `ngOnDestroy` hooks. A service whose `ngOnDestroy` throws stops that teardown, so the server app is never released and one stays behind for every signed-in render. The default release does not depend on teardown and is not affected. -ID tokens are short-lived, and a returning visitor's browser can send one that expired while the tab was closed. The server cannot refresh it, because a user restored from an ID token has no refresh token, so Firebase logs an error and the page renders signed out. The browser then refreshes the token and the page updates. +AngularFire's [sample app](https://github.com/angular/angularfire/tree/main/sample) gives the browser and the server their own `app.config.client.ts` and `app.config.server.ts`. `initializeBrowserOrServerApp` works there too, in the server file, with the client file keeping `initializeApp`. + +ID tokens are short-lived, and a returning visitor's browser can send one that expired while the tab was closed. The server cannot refresh it, because a user restored from an ID token has no refresh token, so Firebase logs an error and the page renders signed out. The browser then refreshes the token and the page updates. A token that cannot be parsed at all, such as a cookie another site set under the same name, also renders signed out, with one warning in the server console. + +If you would rather build the app yourself, or your AngularFire version has no `initializeBrowserOrServerApp`, see [Building the app yourself](#building-the-app-yourself) below. ### Using `REQUEST` instead of a cookie -Angular also exposes the request itself through the `REQUEST` token, so you can read the ID token from an `Authorization` header rather than a cookie. Firebase's [session management with service workers](https://firebase.google.com/docs/auth/web/service-worker-sessions) guide covers attaching that header. Steps 1, 3 and 4 stay the same apart from the server half of the factory, which becomes: +Angular also exposes the request itself through the `REQUEST` token, so you can read the ID token from an `Authorization` header rather than a cookie. Firebase's [session management with service workers](https://firebase.google.com/docs/auth/web/service-worker-sessions) guide covers attaching that header. Steps 1, 3 and 4 stay the same apart from the call in step 4, which passes the token explicitly: + +```ts +import { REQUEST, inject } from '@angular/core'; + +provideFirebaseApp(() => initializeBrowserOrServerApp(firebaseConfig, { + authIdToken: inject(REQUEST, { optional: true })?.headers.get('authorization')?.split('Bearer ')[1], +})), +``` + +`REQUEST` is a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), so headers are read with `headers.get(...)`. Angular sets it to `null` during builds, during static site generation, and during route extraction in development, and it is only supplied at all on `RenderMode.Server` routes. A missing header gives `undefined`, and `initializeBrowserOrServerApp` then looks in the request context and falls back to the ordinary app. + +### Relaying the App Check token + +If you [enforce App Check](https://firebase.google.com/docs/app-check/enable-enforcement) for a Firebase product, server renders need an App Check token too, or their requests are rejected. The server cannot mint one, so the browser relays its own, and `initializeBrowserOrServerApp` passes it to the server app as `appCheckToken`. + +Relay the ordinary token from `getToken`, not a limited-use one. Replay protection only applies where a backend consumes the token, such as a callable Cloud Function with `consumeAppCheckToken`, and product enforcement for Firestore, Realtime Database and Storage does not. + +Keep both tokens in the one `__session` cookie, since behind Firebase Hosting no other cookie reaches your server. In the cookie sync from step 2, write the cookie as JSON and rewrite it whenever either token changes: ```ts -import { PLATFORM_ID, REQUEST, inject } from '@angular/core'; +import { AppCheck } from '@angular/fire/app-check'; +// imported from firebase/app-check so a token that never arrives does not hold the app busy +import { getToken, onTokenChanged } from 'firebase/app-check'; + +const appCheck = inject(AppCheck, { optional: true }); +let authIdToken: string | undefined; +let appCheckToken: string | undefined; + +const writeSessionCookie = () => { + if (!authIdToken && !appCheckToken) { + cookies.remove('__session'); + return; + } + cookies.set('__session', JSON.stringify({ authIdToken, appCheckToken }), { secure: true, sameSite: 'lax' }); +}; + +idToken(auth) + .pipe(takeUntilDestroyed(destroyRef)) + .subscribe((token) => { + authIdToken = token ?? undefined; + writeSessionCookie(); + }); + +if (appCheck) { + destroyRef.onDestroy(onTokenChanged(appCheck, (result) => { + appCheckToken = result.token; + writeSessionCookie(); + })); + getToken(appCheck).then((result) => { + appCheckToken = result.token; + writeSessionCookie(); + }); +} +``` + +The `beforeAuthStateChanged` part of step 2 stays as it is, writing `authIdToken` and calling `writeSessionCookie()` instead of passing the token in. In `server.ts`, unpack the cookie and pass both keys: + +```ts +const readSessionCookie = (value: unknown): { authIdToken?: string; appCheckToken?: string } => { + if (typeof value !== 'string') { + return {}; + } + try { + return JSON.parse(value); + } catch { + return {}; + } +}; + +app.use((req, res, next) => { + res.setHeader('Cache-Control', 'private, no-store'); + angularApp + .handle(req, readSessionCookie(req.cookies?.__session)) + .then((response) => + response ? writeResponseToNodeResponse(response, res) : next(), + ) + .catch(next); +}); +``` + +Three things to know before relaying it: + +- **Every visitor's first server render builds a server app.** An App Check token is per device, so even signed-out visitors get a server app carrying it, and that first render pays the 300 to 400 ms described above. Relay the token only if you enforce App Check. +- **A stale cookie fails the first request after the token's lifetime.** The browser rewrites the cookie only after the page's JavaScript has run and Firebase has refreshed the token. A visitor who returns after about an hour sends the old one, so that first server render's Firebase requests are rejected under enforcement, and the page shows whatever your app shows for a failed read until it hydrates and reads again. For the ID token the same stale first request only means a signed-out render. If that first render must succeed, Firebase's [service-worker relay](https://firebase.google.com/docs/web/ssr-apps#use-app-check) refreshes the token before the request leaves the browser, at the cost of a service worker your app registers itself. +- **A relayed token wins over `APP_CHECK_ON_SERVER` for Firestore.** When the server app carries an `appCheckToken`, Firestore sends it whatever App Check instance the server holds. + +### Building the app yourself + +If you would rather build the app yourself, or your AngularFire version does not export `initializeBrowserOrServerApp`, choose the app in step 4 with your own callback. The `try` around `initializeServerApp` is what keeps a token Firebase cannot parse from failing the request with an HTTP 500, and `releaseOnDeref` is what releases the server app, the way `initializeBrowserOrServerApp` does by default: + +```ts +import { PLATFORM_ID, REQUEST_CONTEXT, inject } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { initializeApp, initializeServerApp } from '@angular/fire/app'; + +const hasAuthIdToken = (value: unknown): value is { authIdToken: string } => + typeof value === 'object' && value !== null && typeof Reflect.get(value, 'authIdToken') === 'string'; provideFirebaseApp(() => { if (isPlatformBrowser(inject(PLATFORM_ID))) { return initializeApp(firebaseConfig); } - const request = inject(REQUEST, { optional: true }); - const authIdToken = request?.headers.get('authorization')?.split('Bearer ')[1]; - if (!authIdToken) { + const requestContext = inject(REQUEST_CONTEXT, { optional: true }); + if (!hasAuthIdToken(requestContext) || !requestContext.authIdToken) { + return initializeApp(firebaseConfig); + } + try { + return initializeServerApp(firebaseConfig, { + authIdToken: requestContext.authIdToken, + releaseOnDeref: requestContext, + }); + } catch { return initializeApp(firebaseConfig); } - return initializeServerApp(firebaseConfig, { - authIdToken, - releaseOnDeref: request, - }); }), ``` -`REQUEST` is a standard [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request), so headers are read with `headers.get(...)`. Angular sets it to `null` during builds, during static site generation, and during route extraction in development, and it is only supplied at all on `RenderMode.Server` routes, so keep the signed-out fallback for those passes. +Keep the signed-out fallback, since there is no request context when Angular prerenders a page and no token when the visitor is signed out, and keep the first two of the three details above. An app that already has this callback can switch to `initializeBrowserOrServerApp` in one deploy. The callback keeps working on later versions too. ## Convenience observables diff --git a/docs/version-22-upgrade.md b/docs/version-22-upgrade.md index ea9e19e8a..f96c049e6 100644 --- a/docs/version-22-upgrade.md +++ b/docs/version-22-upgrade.md @@ -117,3 +117,4 @@ Every callback read (`onSnapshot`, `onAuthStateChanged`, `onValue` and the other - **Coming from a 21 release candidate** (`21.0.0-rc.0`, `rc.1` or `rc.2`), the v21 migration runs once more and reports "No changes made", because `21.0.0` lies between your version and `22.0.0`. That is expected. - **Coming from a 22 canary build.** On `22.0.0-canary.*`, run `ng update @angular/fire@next`. On `22.0.1-canary.*`, run `ng update @angular/fire@canary`. - **Screen tracking** (`ScreenTrackingService`) reads the routed component's selector through Angular's `reflectComponentType`, because `ComponentFactoryResolver` is gone from Angular 22. Nothing changes for your app. +- **Server rendering in one call.** `initializeBrowserOrServerApp` from `@angular/fire/app` replaces the `provideFirebaseApp` callback the Authentication guide used to ask server-rendered apps to write. It builds the ordinary app in the browser and a `FirebaseServerApp` for the visitor's tokens on the server, releases it, and renders signed out instead of failing on a token Firebase cannot parse. See [Server-side Rendering](auth.md#server-side-rendering). Existing callbacks keep working. From bbc2beaca466ccf9c501f7b4a04bb626fcd68d5d Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Tue, 10 Nov 2026 17:13:40 -0800 Subject: [PATCH 10/11] docs(auth): describe the sample as an example of the one-call server app The sample app now has one provideFirebaseApp with initializeBrowserOrServerApp in its shared app.config.ts (#3845), so the sentence about its split client and server files no longer holds. Refs #3843 --- docs/auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/auth.md b/docs/auth.md index dc21c6474..e8dd37374 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -245,7 +245,7 @@ A runtime without `FinalizationRegistry` cannot release at garbage collection, s With `releaseAfterRender` set, the release runs when Angular destroys the render, after your services' `ngOnDestroy` hooks. A service whose `ngOnDestroy` throws stops that teardown, so the server app is never released and one stays behind for every signed-in render. The default release does not depend on teardown and is not affected. -AngularFire's [sample app](https://github.com/angular/angularfire/tree/main/sample) gives the browser and the server their own `app.config.client.ts` and `app.config.server.ts`. `initializeBrowserOrServerApp` works there too, in the server file, with the client file keeping `initializeApp`. +AngularFire's [sample app](https://github.com/angular/angularfire/tree/main/sample) is an example of this step: one `provideFirebaseApp(() => initializeBrowserOrServerApp(...))` in its shared `app.config.ts`, with `app.config.server.ts` adding only `provideServerRendering`. ID tokens are short-lived, and a returning visitor's browser can send one that expired while the tab was closed. The server cannot refresh it, because a user restored from an ID token has no refresh token, so Firebase logs an error and the page renders signed out. The browser then refreshes the token and the page updates. A token that cannot be parsed at all, such as a cookie another site set under the same name, also renders signed out, with one warning in the server console. From fa2f0f2ffdf100fedc7e70c80e91b4f3fdf1c160 Mon Sep 17 00:00:00 2001 From: Armando Navarro Date: Tue, 10 Nov 2026 19:26:23 -0800 Subject: [PATCH 11/11] docs: say how to move from 22.0.0-rc.0 in the v22 upgrade guide The guide covered upgrades from 21, a 21 release candidate and a 22 canary build, but not from the first 22 release candidate. ng add saves a prerelease as an exact version, so an app on 22.0.0-rc.0 stays there until ng update @angular/fire@next moves it. That command also runs migration-v22 again, which reports "No changes made" for a firebase entry already inside the supported range. Refs #3834 --- docs/version-22-upgrade.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/version-22-upgrade.md b/docs/version-22-upgrade.md index f96c049e6..3528d7202 100644 --- a/docs/version-22-upgrade.md +++ b/docs/version-22-upgrade.md @@ -115,6 +115,9 @@ Every callback read (`onSnapshot`, `onAuthStateChanged`, `onValue` and the other - **Node.js 20 is not supported.** Angular 22 requires Node.js `^22.22.3 || ^24.15.0 || >=26.0.0`. Node.js 20 reached end of life on 2026-04-30. - **TypeScript 6.0 is required** (AngularFire 22 peers `typescript >=6.0 <6.1`). TypeScript 6 turns `strict` on by default, drops `downlevelIteration` and deprecates `baseUrl` and `moduleResolution: "node"`, so an app carrying those settings in its `tsconfig.json` may see new compiler errors that Angular's own update does not fix. - **Coming from a 21 release candidate** (`21.0.0-rc.0`, `rc.1` or `rc.2`), the v21 migration runs once more and reports "No changes made", because `21.0.0` lies between your version and `22.0.0`. That is expected. +- **Coming from `22.0.0-rc.0`,** run `ng update @angular/fire@next`. + - `ng add` saved the release candidate as an exact version, so it does not move on its own. + - The v22 migration runs once more and reports "No changes made" when your `firebase` entry is already inside the supported range. - **Coming from a 22 canary build.** On `22.0.0-canary.*`, run `ng update @angular/fire@next`. On `22.0.1-canary.*`, run `ng update @angular/fire@canary`. - **Screen tracking** (`ScreenTrackingService`) reads the routed component's selector through Angular's `reflectComponentType`, because `ComponentFactoryResolver` is gone from Angular 22. Nothing changes for your app. - **Server rendering in one call.** `initializeBrowserOrServerApp` from `@angular/fire/app` replaces the `provideFirebaseApp` callback the Authentication guide used to ask server-rendered apps to write. It builds the ordinary app in the browser and a `FirebaseServerApp` for the visitor's tokens on the server, releases it, and renders signed out instead of failing on a token Firebase cannot parse. See [Server-side Rendering](auth.md#server-side-rendering). Existing callbacks keep working.