Skip to content

Commit deb882f

Browse files
Merge branch 'main' into a20-dependency-declarations
2 parents 9e21c25 + 5a6dc5e commit deb882f

28 files changed

Lines changed: 12678 additions & 10314 deletions

README.md

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,33 @@ by conforming to Angular conventions.
1212
- **Observable based** - Utilize RxJS rather than callbacks for real-time streams.
1313
- **NgRx friendly API** - Integrate with NgRx using AngularFire's action based APIs.
1414
- **Lazy-loading** - AngularFire dynamically imports much of Firebase, reducing the time to load your app.
15-
- **Deploy schematics** - Get your Angular application deployed on Firebase Hosting with a single command.
1615
- **Google Analytics** - Zero-effort Angular Router awareness in Google Analytics.
1716
- **Router Guards** - Guard your Angular routes with built-in Firebase Authentication checks.
1817

1918
## Example use
2019

2120
```ts
21+
import { ApplicationConfig } from '@angular/core';
2222
import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
2323
import { getFirestore, provideFirestore } from '@angular/fire/firestore';
2424

2525
export const appConfig: ApplicationConfig = {
2626
providers: [
27-
provideFirebaseApp(() => initializeApp({ ... })),
27+
provideFirebaseApp(() => initializeApp({ /* ...your Firebase config... */ })),
2828
provideFirestore(() => getFirestore()),
29-
...
29+
// ...
3030
],
31-
...
32-
})
31+
// ...
32+
}
3333
```
3434

3535
```ts
3636
import { AsyncPipe } from '@angular/common';
37-
import { inject } from '@angular/core';
37+
import { Component, inject } from '@angular/core';
3838
import { Firestore, collectionData, collection } from '@angular/fire/firestore';
3939

4040
interface Item {
41-
name: string,
42-
...
41+
name: string;
4342
};
4443

4544
@Component({
@@ -55,7 +54,7 @@ interface Item {
5554
`,
5655
imports: [AsyncPipe]
5756
})
58-
export class AppComponent {
57+
export class App {
5958
firestore = inject(Firestore);
6059
itemCollection = collection(this.firestore, 'items');
6160
items$ = collectionData<Item>(this.itemCollection);
@@ -70,7 +69,7 @@ export class AppComponent {
7069

7170
[Contributing](CONTRIBUTING.md)
7271

73-
[Stackblitz Template](https://stackblitz.com/edit/angular-fire-start) - Remember to set your Firebase configuration in `app/app.module.ts`.
72+
[Stackblitz Template](https://stackblitz.com/edit/angular-fire-start) - A runnable AngularFire example. Add your own Firebase configuration to connect it to your project.
7473

7574
[Upgrading from v6.0? Check out our guide.](docs/version-7-upgrade.md)
7675

@@ -84,7 +83,7 @@ The [`sample`](sample) folder contains a kitchen sink application that demonstra
8483

8584
Get help on our [Q&A board](https://github.com/angular/angularfire/discussions?discussions_q=category%3AQ%26A), the official [Firebase Mailing List](https://groups.google.com/forum/#!forum/firebase-talk), the [Firebase Community Slack](https://firebase.community/) (`#angularfire2`), the [Angular Community Discord](http://discord.gg/angular) (`#firebase`), [Gitter](https://gitter.im/angular/angularfire2), the [Firebase subreddit](https://www.reddit.com/r/firebase), or [Stack Overflow](https://stackoverflow.com/questions/tagged/angularfire2).
8685

87-
> **NOTE:** While relatively stable, AngularFire is a [developer preview](https://angular.io/guide/releases#developer-preview) and is subject to change before general availability. Questions on the mailing list and issues filed here are answered on a <strong>best-effort basis</strong> by maintainers and other community members. If you are able to reproduce a problem with Firebase <em>outside of AngularFire's implementation</em>, please [file an issue on the Firebase JS SDK](https://github.com/firebase/firebase-js-sdk/issues) or reach out to the personalized [Firebase support channel](https://firebase.google.com/support/).
86+
> **NOTE:** Questions on the mailing list and issues filed here are answered on a <strong>best-effort basis</strong> by maintainers and other community members. If you are able to reproduce a problem with Firebase <em>outside of AngularFire's implementation</em>, please [file an issue on the Firebase JS SDK](https://github.com/firebase/firebase-js-sdk/issues) or reach out to the personalized [Firebase support channel](https://firebase.google.com/support/).
8887
8988
## Developer Guide
9089

@@ -98,10 +97,26 @@ This developer guide assumes you're using the new tree-shakable AngularFire API,
9897
<tr>
9998
<td>
10099

100+
#### [AI Logic](docs/ai.md#ai-logic)
101+
```ts
102+
import { } from '@angular/fire/ai';
103+
```
104+
</td>
105+
<td>
106+
101107
#### [Analytics](docs/analytics.md#analytics)
102108
```ts
103109
import { } from '@angular/fire/analytics';
104110
```
111+
</td>
112+
</tr>
113+
<tr>
114+
<td>
115+
116+
#### [App Check](docs/app-check.md#app-check)
117+
```ts
118+
import { } from '@angular/fire/app-check';
119+
```
105120
</td>
106121
<td>
107122

@@ -173,22 +188,6 @@ import { } from '@angular/fire/database';
173188
```ts
174189
import { } from '@angular/fire/remote-config';
175190
```
176-
</td>
177-
</tr>
178-
<tr>
179-
<td>
180-
181-
#### [App Check](docs/app-check.md#app-check)
182-
```ts
183-
import { } from '@angular/fire/app-check';
184-
```
185-
</td>
186-
<td>
187-
188-
#### [AI Logic](docs/ai.md#ai-logic)
189-
```ts
190-
import { } from '@angular/fire/ai';
191-
```
192191
</td>
193192
</tr>
194193
</table>

docs/auth.md

Lines changed: 201 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,36 +60,220 @@ Update the imports from `import { ... } from 'firebase/auth'` to `import { ... }
6060

6161
## Server-side Rendering
6262

63-
To support Auth context in server-side rendering, you can provide `FirebaseServerApp`:
63+
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.
64+
65+
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.
66+
67+
All 4 steps below are required. Miss any one of them and the page still renders, but it renders signed out, with no error to tell you why.
68+
69+
### 1. Serve the route with `RenderMode.Server`
70+
71+
`ng new --ssr` scaffolds `app.routes.server.ts` with every route set to `RenderMode.Prerender`. Prerendering runs at build time, so there is no request and no cookie, and Angular provides neither `REQUEST` nor `REQUEST_CONTEXT`. Any route that must already render as the signed-in user before hydration has to be `RenderMode.Server`. A `RenderMode.Client` route renders in the browser, where the user is already signed in, so it needs none of this.
6472

6573
```ts
66-
import { ApplicationConfig, PLATFORM_ID, inject } from '@angular/core';
74+
import { RenderMode, ServerRoute } from '@angular/ssr';
75+
76+
export const serverRoutes: ServerRoute[] = [
77+
{ path: 'account', renderMode: RenderMode.Server },
78+
{ path: '**', renderMode: RenderMode.Prerender },
79+
];
80+
```
81+
82+
The rest of this guide has no effect on routes rendered any other way.
83+
84+
### 2. Keep the ID token in a cookie
85+
86+
Install [js-cookie](https://github.com/js-cookie/js-cookie):
87+
88+
```bash
89+
npm install js-cookie
90+
npm install --save-dev @types/js-cookie
91+
```
92+
93+
Add the cookie sync to your `app.config.ts`. AngularFire's `idToken` observable emits on sign-in, on sign-out, and whenever the token is refreshed.
94+
95+
```ts
96+
import { DestroyRef, PLATFORM_ID, inject, provideAppInitializer } from '@angular/core';
6797
import { isPlatformBrowser } from '@angular/common';
68-
import { provideFirebaseApp, initializeApp, initializeServeApp, initializeServerApp } from '@angular/fire/app';
69-
import { provideAuth, getAuth } from '@angular/fire/auth';
98+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
99+
import { Auth, idToken } from '@angular/fire/auth';
100+
import { beforeAuthStateChanged } from 'firebase/auth';
101+
import cookies from 'js-cookie';
102+
103+
// add to appConfig.providers
104+
provideAppInitializer(() => {
105+
if (!isPlatformBrowser(inject(PLATFORM_ID))) {
106+
return;
107+
}
108+
const auth = inject(Auth);
109+
const destroyRef = inject(DestroyRef);
110+
111+
const writeSessionCookie = (token: string | undefined) => {
112+
if (token) {
113+
cookies.set('__session', token, { secure: true, sameSite: 'lax' });
114+
} else {
115+
cookies.remove('__session');
116+
}
117+
};
118+
119+
idToken(auth)
120+
.pipe(takeUntilDestroyed(destroyRef))
121+
.subscribe((token) => writeSessionCookie(token ?? undefined));
122+
123+
let priorToken: string | undefined;
124+
const unsubscribe = beforeAuthStateChanged(
125+
auth,
126+
async (user) => {
127+
// Must update the cookie before the sign-out completes, otherwise a page
128+
// load that races it still sends the signed-out user's token.
129+
priorToken = cookies.get('__session');
130+
writeSessionCookie(await user?.getIdToken());
131+
},
132+
() => writeSessionCookie(priorToken)
133+
);
134+
destroyRef.onDestroy(unsubscribe);
135+
}),
136+
```
137+
138+
The 2 hooks cover different moments:
139+
- `idToken` fires after an auth state change has completed, and also when Firebase refreshes the token in the background, which is what keeps the cookie current.
140+
- `beforeAuthStateChanged` fires earlier, while an auth state change is still in progress and before Firebase sets the new user, so a page load that races a sign-out cannot send a token for the user who just left and get their data rendered back. Its third argument puts the cookie back if another blocking callback rejects the auth state change.
141+
142+
Name the cookie `__session`. Behind Firebase Hosting it is the [only cookie forwarded](https://firebase.google.com/docs/hosting/manage-cache#using_cookies) to your server code, and any other name is dropped before your app sees it.
143+
144+
#### Both attributes matter
145+
146+
The cookie sync above sets `{ secure: true, sameSite: 'lax' }`, and neither attribute is optional.
147+
148+
- `secure` keeps the cookie off unencrypted connections. Browsers make an exception for `localhost`, so local development still works.
149+
- `sameSite: 'lax'` keeps the cookie off cross-site requests while still sending it when someone follows a link into your app, which is what lets that first page render signed in. If your app never needs a signed-in first render from an external link, use `'strict'` instead.
150+
151+
#### What this cookie carries
152+
153+
This cookie carries a short-lived ID token that scripts on your page can read. Firebase already keeps the signed-in state in browser storage, so the cookie does not create a new place for a token to be stolen from, but it does travel on every request.
154+
155+
If you need a session the browser cannot read, use Firebase's [session cookies](https://firebase.google.com/docs/auth/admin/manage-cookies) with the Admin SDK instead. Those cannot be handed to `initializeServerApp`, so that approach means verifying the cookie yourself and building your own server-side Auth context.
156+
157+
#### `beforeAuthStateChanged` from `firebase/auth`
158+
159+
One import in the code above is deliberately different from the rest of this guide. `beforeAuthStateChanged` comes from `firebase/auth` rather than `@angular/fire/auth`. AngularFire's version keeps the app marked as busy until its callback first runs, and this callback only runs when someone signs in or out.
160+
161+
Importing it from `@angular/fire/auth` makes `ng build` hang during route extraction and fail with a timeout. That is a bug on our side, tracked in [#3748](https://github.com/angular/angularfire/issues/3748). Once the fix lands, this can be imported from `@angular/fire/auth` like everything else.
162+
163+
### 3. Pass the cookie into the render
164+
165+
Install [cookie-parser](https://github.com/expressjs/cookie-parser):
166+
167+
```bash
168+
npm install cookie-parser
169+
npm install --save-dev @types/cookie-parser
170+
```
171+
172+
The `server.ts` the Angular CLI generated already renders your app for every request that is not a static file. Replace that existing `app.use` block with this one, which reads the cookie and hands the token to the render. Do not add a second block, because the first one to match wins and the token would never arrive:
173+
174+
```ts
175+
// server.ts
176+
import cookieParser from 'cookie-parser';
177+
178+
app.use(cookieParser());
179+
180+
app.use((req, res, next) => {
181+
angularApp
182+
.handle(req, { authIdToken: req.cookies?.__session })
183+
.then((response) =>
184+
response ? writeResponseToNodeResponse(response, res) : next(),
185+
)
186+
.catch(next);
187+
});
188+
```
189+
190+
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.
191+
192+
The second argument to `handle` is what the render reads back as `REQUEST_CONTEXT`.
193+
194+
### 4. Build the server app from the token
195+
196+
In `app.config.ts`, choose the Firebase app based on where the code is running, and pass that app to every Firebase provider:
197+
198+
```ts
199+
import {
200+
ApplicationConfig,
201+
PLATFORM_ID,
202+
REQUEST_CONTEXT,
203+
inject,
204+
} from '@angular/core';
205+
import { isPlatformBrowser } from '@angular/common';
206+
import {
207+
FirebaseApp,
208+
initializeApp,
209+
initializeServerApp,
210+
provideFirebaseApp,
211+
} from '@angular/fire/app';
212+
import { getAuth, provideAuth } from '@angular/fire/auth';
213+
import { getFirestore, provideFirestore } from '@angular/fire/firestore';
214+
215+
const firebaseConfig = { /* ...your Firebase configuration... */ };
70216

71217
export const appConfig: ApplicationConfig = {
72218
providers: [
73219
provideFirebaseApp(() => {
74220
if (isPlatformBrowser(inject(PLATFORM_ID))) {
75221
return initializeApp(firebaseConfig);
76222
}
77-
// Optional, since it's null in dev-mode and SSG
78-
const request = inject(REQUEST, { optional: true });
79-
const authIdToken = request?.headers.authorization?.split("Bearer ")[1];
223+
const requestContext = inject(REQUEST_CONTEXT, { optional: true }) as
224+
| { authIdToken?: string }
225+
| null;
226+
if (!requestContext?.authIdToken) {
227+
return initializeApp(firebaseConfig);
228+
}
80229
return initializeServerApp(firebaseConfig, {
81-
authIdToken,
82-
releaseOnDeref: request || undefined
230+
authIdToken: requestContext.authIdToken,
231+
releaseOnDeref: requestContext,
83232
});
84233
}),
85-
provideAuth(() => getAuth(inject(FirebaseApp)),
86-
...
234+
provideAuth(() => getAuth(inject(FirebaseApp))),
235+
provideFirestore(() => getFirestore(inject(FirebaseApp))),
236+
// ...
87237
],
88-
...
89-
})
238+
};
239+
```
240+
241+
#### Five details make this work
242+
243+
- **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.
244+
- **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.
245+
- **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.
246+
- **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.
247+
- **The cast is needed** because Angular types `REQUEST_CONTEXT` as `unknown`.
248+
249+
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.
250+
251+
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.
252+
253+
### Using `REQUEST` instead of a cookie
254+
255+
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:
256+
257+
```ts
258+
import { PLATFORM_ID, REQUEST, inject } from '@angular/core';
259+
260+
provideFirebaseApp(() => {
261+
if (isPlatformBrowser(inject(PLATFORM_ID))) {
262+
return initializeApp(firebaseConfig);
263+
}
264+
const request = inject(REQUEST, { optional: true });
265+
const authIdToken = request?.headers.get('authorization')?.split('Bearer ')[1];
266+
if (!authIdToken) {
267+
return initializeApp(firebaseConfig);
268+
}
269+
return initializeServerApp(firebaseConfig, {
270+
authIdToken,
271+
releaseOnDeref: request,
272+
});
273+
}),
90274
```
91275

92-
Follow Firebase's [ Session Management with Service Workers documentation](https://firebase.google.com/docs/auth/web/service-worker-sessions) to learn how to pass the `idToken` to the server. __Note: this will not currently work in dev-mode as Angular SSR does not provide a method to get the Request headers.__
276+
`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.
93277

94278
## Convenience observables
95279

@@ -187,15 +371,15 @@ export class UserComponent implements OnDestroy {
187371
## Connecting the emulator suite
188372

189373
```ts
190-
import { ApplicationConfig } from '@angular/core';
191-
import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
374+
import { ApplicationConfig, inject } from '@angular/core';
375+
import { FirebaseApp, provideFirebaseApp, initializeApp } from '@angular/fire/app';
192376
import { connectAuthEmulator, getAuth, provideAuth } from '@angular/fire/auth';
193377

194378
export const appConfig: ApplicationConfig = {
195379
providers: [
196380
provideFirebaseApp(() => initializeApp({ ... })),
197381
provideAuth(() => {
198-
const auth = getAuth();
382+
const auth = getAuth(inject(FirebaseApp));
199383
connectAuthEmulator(auth, 'http://localhost:9099', { disableWarnings: true });
200384
return auth;
201385
}),

docs/deploy/getting-started.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# Deploy your application on Firebase Hosting & Functions
22

3+
> **This page is out of date.** It describes a setup flow that `ng add @angular/fire` no longer performs, and wiring the deploy target by hand from the samples below puts you on a deprecated path that warns on every deploy. For current guidance see [the Quickstart](../install-and-setup.md), step 6, and for server-side rendered apps see the [App Hosting guide](../app-hosting.md).
4+
35
In this guide, we'll look at how to use `@angular/fire` to automatically deploy an Angular application to Firebase hosting or functions by using the Angular CLI.
46

57
`@angular/fire` uses Firebase functions to deploy your Angular universal projects, with server-side rendering enabled.

0 commit comments

Comments
 (0)