Skip to content

Commit bfc03a6

Browse files
Merge branch 'main' into main
2 parents 5162143 + 11e29ff commit bfc03a6

28 files changed

Lines changed: 986 additions & 120 deletions

.github/workflows/codeql-analysis.yml

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ jobs:
2424
analyze:
2525
name: Analyze
2626
runs-on: ubuntu-latest
27+
permissions:
28+
actions: read
29+
contents: read
30+
security-events: write
2731

2832
strategy:
2933
fail-fast: false
@@ -35,11 +39,13 @@ jobs:
3539

3640
steps:
3741
- name: Checkout repository
38-
uses: actions/checkout@v2
42+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
43+
with:
44+
persist-credentials: false
3945

4046
# Initializes the CodeQL tools for scanning.
4147
- name: Initialize CodeQL
42-
uses: github/codeql-action/init@v2
48+
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
4349
with:
4450
languages: ${{ matrix.language }}
4551
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +56,7 @@ jobs:
5056
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
5157
# If this step fails, then you should remove it and run the build manually (see below)
5258
- name: Autobuild
53-
uses: github/codeql-action/autobuild@v2
59+
uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
5460

5561
# ℹ️ Command-line programs to run using the OS shell.
5662
# 📚 https://git.io/JvXDl
@@ -64,4 +70,4 @@ jobs:
6470
# make release
6571

6672
- name: Perform CodeQL Analysis
67-
uses: github/codeql-action/analyze@v2
73+
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3

.github/workflows/test.yml

Lines changed: 27 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -15,39 +15,29 @@ on:
1515
schedule:
1616
- cron: 0 0 * * 1-5
1717

18+
permissions:
19+
contents: read
20+
1821
jobs:
1922
build:
2023
runs-on: ubuntu-latest
2124
name: Build
2225
steps:
2326
- name: Checkout
24-
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
27+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
28+
with:
29+
persist-credentials: false
2530
- name: Setup node
26-
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903
31+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
2732
with:
2833
node-version: '20'
2934
check-latest: false
30-
- name: angular build cache
31-
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
32-
with:
33-
path: ./.angular
34-
key: angular-cache
35-
- name: node_modules cache
36-
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
37-
id: node_modules_cache
38-
with:
39-
path: ./node_modules
40-
key: ${{ runner.os }}-20-${{ hashFiles('package-lock.json') }}
41-
restore-keys: |
42-
${{ runner.os }}-20-
4335
- name: Install deps
44-
if: steps.node_modules_cache.outputs.cache-hit != 'true'
45-
run: |
46-
npm ci
36+
run: npm ci
4737
- name: Build
4838
run: ./tools/build.sh
4939
- name: 'Upload Artifact'
50-
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4
40+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
5141
with:
5242
name: angularfire-${{ github.run_id }}
5343
path: dist
@@ -64,25 +54,18 @@ jobs:
6454
name: Test Node ${{ matrix.node }} (${{ matrix.os }})
6555
steps:
6656
- name: Checkout
67-
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
57+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
58+
with:
59+
persist-credentials: false
6860
- name: Setup node
69-
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903
61+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
7062
with:
7163
node-version: ${{ matrix.node }}
7264
check-latest: true
73-
- name: node_modules cache
74-
id: node_modules_cache
75-
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
76-
with:
77-
path: ./node_modules
78-
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('package-lock.json') }}
79-
restore-keys: |
80-
${{ runner.os }}-${{ matrix.node }}-
8165
- name: Install deps
82-
if: steps.node_modules_cache.outputs.cache-hit != 'true'
8366
run: npm ci
8467
- name: Download Artifacts
85-
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
68+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
8669
with:
8770
name: angularfire-${{ github.run_id }}
8871
path: dist
@@ -106,35 +89,23 @@ jobs:
10689
fail-fast: false
10790
steps:
10891
- name: Checkout
109-
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
92+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
93+
with:
94+
persist-credentials: false
11095
- name: Setup node
111-
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903
96+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
11297
with:
11398
node-version: 20
11499
check-latest: false
115100
- name: Setup java
116-
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165
101+
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
117102
with:
118103
distribution: 'temurin'
119104
java-version: '21'
120-
- name: node_modules cache
121-
id: node_modules_cache
122-
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
123-
with:
124-
path: ./node_modules
125-
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('package-lock.json') }}
126-
restore-keys: |
127-
${{ runner.os }}-${{ matrix.node }}-
128105
- name: Install deps
129-
if: steps.node_modules_cache.outputs.cache-hit != 'true'
130106
run: npm ci
131-
- name: Firebase emulator cache
132-
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
133-
with:
134-
path: ~/.cache/firebase/emulators
135-
key: firebase_emulators
136107
- name: Download Artifacts
137-
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
108+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
138109
with:
139110
name: angularfire-${{ github.run_id }}
140111
path: dist
@@ -151,22 +122,15 @@ jobs:
151122
fail-fast: false
152123
steps:
153124
- name: Checkout
154-
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
125+
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
126+
with:
127+
persist-credentials: false
155128
- name: Setup node
156-
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903
129+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
157130
with:
158131
node-version: ${{ matrix.node }}
159132
check-latest: true
160-
- name: node_modules cache
161-
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
162-
id: node_modules_cache
163-
with:
164-
path: ./node_modules
165-
key: ${{ runner.os }}-${{ matrix.node }}-${{ hashFiles('package-lock.json') }}
166-
restore-keys: |
167-
${{ runner.os }}-${{ matrix.node }}-
168133
- name: Install deps
169-
if: steps.node_modules_cache.outputs.cache-hit != 'true'
170134
run: npm ci
171135
#- name: Lint
172136
# run: npm run lint
@@ -176,13 +140,8 @@ jobs:
176140
run: |
177141
npm run build:jasmine
178142
npm run test:node
179-
- name: Firebase emulator cache
180-
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
181-
with:
182-
path: ~/.cache/firebase/emulators
183-
key: firebase_emulators
184143
- name: Setup java
185-
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165
144+
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
186145
with:
187146
distribution: 'temurin'
188147
java-version: '21'
@@ -206,13 +165,13 @@ jobs:
206165
if: ${{ github.ref == 'refs/heads/main' || github.event_name == 'release' }}
207166
steps:
208167
- name: Setup node
209-
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903
168+
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
210169
with:
211170
node-version: '20'
212171
registry-url: 'https://registry.npmjs.org'
213172
check-latest: false
214173
- name: 'Download Artifacts'
215-
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53
174+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
216175
with:
217176
name: angularfire-${{ github.run_id }}
218177
path: dist

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ interface Item {
4646
selector: 'app-root',
4747
template: `
4848
<ul>
49-
@for (item of (item$ | async); track item) {
49+
@for (item of (items$ | async); track item) {
5050
<li>
5151
{{ item.name }}
5252
</li>
@@ -58,14 +58,16 @@ interface Item {
5858
export class AppComponent {
5959
firestore = inject(Firestore);
6060
itemCollection = collection(this.firestore, 'items');
61-
item$ = collectionData<Item>(itemCollection);
61+
items$ = collectionData<Item>(this.itemCollection);
6262
}
6363
```
6464

6565
## Resources
6666

6767
[Quickstart](docs/install-and-setup.md) - Get your first application up and running by following our quickstart guide.
6868

69+
[Deploying SSR to App Hosting](docs/app-hosting.md) - Deploy a server-rendered app to Firebase App Hosting, and avoid the silent SSR-to-CSR fallback.
70+
6971
[Contributing](CONTRIBUTING.md)
7072

7173
[Stackblitz Template](https://stackblitz.com/edit/angular-fire-start) - Remember to set your Firebase configuration in `app/app.module.ts`.

docs/app-hosting.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<small>
2+
<a href="https://github.com/angular/angularfire">AngularFire</a> &#10097; <a href="../README.md#developer-guide">Developer Guide</a> &#10097; Deploying SSR to Firebase App Hosting
3+
</small>
4+
5+
# Deploying a server-rendered app to Firebase App Hosting
6+
7+
[Firebase App Hosting](https://firebase.google.com/docs/app-hosting) is Firebase's recommended way to deploy a server-side-rendered (SSR) Angular application. It builds your app in Google Cloud Build, runs the Node server, and serves it behind Google's CDN and proxy. This applies to either way of deploying to App Hosting, a connected GitHub repository or `firebase deploy` from your own machine; both build the same way and are affected the same way.
8+
9+
This guide covers one thing that trips up almost every new SSR deployment: **the server can silently stop server-rendering and fall back to client-side rendering (CSR), with no error anywhere obvious.** It explains how to detect that in 30 seconds and how to fix it.
10+
11+
## How to deploy
12+
13+
If you have not deployed yet, follow Firebase's own guides: [Get started with App Hosting](https://firebase.google.com/docs/app-hosting/get-started) to connect a GitHub repository (App Hosting builds and deploys on every push), or [Alternative ways to deploy](https://firebase.google.com/docs/app-hosting/alt-deploy) to deploy with `firebase deploy` from your own machine. Both build your app the same way in Google Cloud Build. The rest of this guide covers an Angular-specific issue you can hit once your app is deployed either way.
14+
15+
## The symptom: SSR silently downgrades to CSR
16+
17+
A freshly deployed Angular SSR app usually *looks* fine in a browser, the page renders and works. But the server may be sending an almost-empty HTML shell and letting the browser do all the rendering. When that happens you lose the whole point of SSR: crawlers and link previews see no content, and first paint on slow devices is worse.
18+
19+
There is no error in the deploy output and no error in the browser. The only trace is in your backend's server logs, which nothing prompts you to check.
20+
21+
App Hosting runs on Cloud Run, not Cloud Functions, so its logs live in Cloud Logging:
22+
23+
- In the Firebase console, open your backend and go to its **Logs** tab, then look at **Runtime logs**.
24+
- From a terminal, use `gcloud`:
25+
26+
```bash
27+
gcloud logging read 'resource.type=cloud_run_revision AND resource.labels.service_name=YOUR_BACKEND_ID' --project YOUR_PROJECT_ID --limit 10
28+
```
29+
30+
See Firebase's [View logs and metrics](https://firebase.google.com/docs/app-hosting/logging) guide for more.
31+
32+
## The 30-second check to run after every deploy
33+
34+
Pick an SSR route (not an SSG/prerendered one) and fetch it:
35+
36+
```bash
37+
curl -s https://YOUR-SITE/ | grep ng-server-context
38+
```
39+
40+
Angular's server renderer stamps a `ng-server-context` attribute on the app's root element:
41+
42+
- `ng-server-context="ssr"` - the route was server-rendered (SSR). This is what you want.
43+
- `ng-server-context="ssg"` - the route was prerendered (SSG). Fine in itself, but not the SSR path this guide is about.
44+
- **No `ng-server-context` at all** - the server returned a client-only shell and the browser is doing all the rendering. This is the silent CSR fallback described above, regardless of how the page looks in a browser.
45+
46+
## Why it happens
47+
48+
Angular's server engine only trusts the `X-Forwarded-*` headers a proxy attaches to a request when it is told to. App Hosting's proxy adds several of these headers (for example `x-forwarded-for` and `x-forwarded-proto`). If Angular does not trust the full set the platform sends, it treats the request as untrusted and de-optimizes to CSR on every request.
49+
50+
<!--
51+
Maintainer note (2026-07-19): the two-step fix below is a workaround for a lag in
52+
Firebase App Hosting's build image. The underlying fix (injecting the full
53+
X-Forwarded-* set into NG_TRUST_PROXY_HEADERS) is already merged upstream in
54+
GoogleCloudPlatform/buildpacks (commit 9346e60b, 2026-06-17) but had not reached
55+
production builders as of 2026-07-19. Once it ships, `npm update` alone suffices
56+
and the trustProxyHeaders step becomes optional; revisit and simplify this guide.
57+
-->
58+
59+
## The Fix
60+
61+
Two steps are needed together.
62+
63+
**1. Update Angular to the latest patch:**
64+
65+
```bash
66+
npm update @angular/core @angular/ssr
67+
```
68+
69+
**2. Turn on proxy-header trust when you create the server engine.** In `src/server.ts`, pass `trustProxyHeaders: true` to `AngularNodeAppEngine`:
70+
71+
```ts
72+
import { AngularNodeAppEngine } from '@angular/ssr/node';
73+
74+
const angularApp = new AngularNodeAppEngine({
75+
trustProxyHeaders: true,
76+
});
77+
```
78+
79+
Redeploy, then run the 30-second check above. You should now see `ng-server-context="ssr"`.
80+
81+
### Why both steps
82+
83+
The two steps address different halves of the same handshake, and neither alone is enough:
84+
85+
- Recent Angular patches let the `trustProxyHeaders` engine option take effect on App Hosting; on older patches a platform environment variable wins instead, so the code option is ignored. The `npm update` gets you onto a patch where the option is honored.
86+
- Even on the latest patch, you still have to *set* the option, so App Hosting's proxy headers are trusted.
87+
88+
This matches Firebase's own guidance. For the current, authoritative version of this fix (including any App Hosting or Angular version notes), see Firebase's [App Hosting troubleshooting guide](https://firebase.google.com/docs/app-hosting/troubleshooting#angular-proxy-trust).
89+
90+
> Note: App Hosting also has an experimental, opt-in local-build deploy option (you build on your own machine, then `firebase deploy`) in which the Firebase CLI applies this proxy-header configuration for you. It is off by default and not a documented, supported workflow, so this guide targets the standard source deploy; apply the fix above.
91+
92+
## Related Angular documentation
93+
94+
- [Angular SSR guide](https://angular.dev/guide/ssr)
95+
- [Configuring trusted proxy headers](https://angular.dev/best-practices/security#configuring-trusted-proxy-headers)

docs/auth.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ import { Auth, User, user } from '@angular/fire/auth';
106106

107107
export class UserComponent implements OnDestroy {
108108
private auth: Auth = inject(Auth);
109-
user$ = user(auth);
109+
user$ = user(this.auth);
110110
userSubscription: Subscription;
111111
...
112112

@@ -136,7 +136,7 @@ import { Auth, authState } from '@angular/fire/auth';
136136

137137
export class UserComponent implements OnDestroy {
138138
private auth: Auth = inject(Auth);
139-
authState$ = authState(auth);
139+
authState$ = authState(this.auth);
140140
authStateSubscription: Subscription;
141141
...
142142

@@ -165,7 +165,7 @@ import { Auth, idToken } from '@angular/fire/auth';
165165

166166
export class UserComponent implements OnDestroy {
167167
private auth: Auth = inject(Auth);
168-
idToken$ = idToken(auth);
168+
idToken$ = idToken(this.auth);
169169
idTokenSubscription: Subscription;
170170
...
171171

0 commit comments

Comments
 (0)