Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions skills/firebase-ssr/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: firebase-ssr
description: "How to use Firebase in Server-Side Rendering (SSR) environments. Make sure to use this skill whenever the user mentions Next.js, Nuxt, SvelteKit, Angular SSR, Remix, or any other server-side framework, or asks about initializeServerApp, session cookies, fetching data on the server, or serializing Firebase data between server and client."
---

# Firebase in SSR Environments

When building universal/SSR applications correctly, you must isolate Firebase apps to prevent cross-request state pollution and securely pass Firebase-specific data structures back to the client.

## Framework Selection Workflow

The core concepts of Firebase SSR (request isolation and serialization mappings) apply to all major backend JS frameworks, but the execution syntax drastically changes.

**Step 1:** Identify the SSR framework the user is building with.

**Step 2:** Read the appropriate framework-specific reference guide before attempting to implement Firebase integration.
- `[references/nextjs.md](file:///Users/mtewani/source/agent-skills/skills/firebase-ssr/references/nextjs.md)` - For Next.js App Router (RSCs, Route Handlers).
- `[references/remix.md](file:///Users/mtewani/source/agent-skills/skills/firebase-ssr/references/remix.md)` - For Remix (`loader` / `action` functions).
- `[references/angular-ssr.md](file:///Users/mtewani/source/agent-skills/skills/firebase-ssr/references/angular-ssr.md)` - For Angular Universal/SSR (`REQUEST` token and `TransferState`).
Comment thread
maneesht marked this conversation as resolved.
Outdated

*Note: If the user's framework is not explicitly listed (e.g., SvelteKit, Nuxt), read `nextjs.md` mentally translating Next-specific concepts (like `headers()`) to the equivalent request handling method corresponding to their actual framework.*

---

## Core Principles

Regardless of the framework selected in Step 2, you must aggressively enforce the following core principles.

### 1. Initializing Firebase: Avoid the Singleton

In a Node.js SSR context, utilizing the single `initializeApp` singleton is extremely dangerous because the server instance is shared across all incoming requests globally.

> [!WARNING]
> DO NOT use the standard `initializeApp()` inside server-side code that responds to HTTP endpoints or renders pages. It will cause severe data and authentication token leakage between different users.

Instead, use `initializeServerApp` to create a lightweight, request-scoped Firebase app instance.

```typescript
import { initializeServerApp } from "firebase/app";

// Must be called for every incoming request handling routine
const app = initializeServerApp(firebaseConfig, {
authIdToken: extractedToken // Provided by framework-specific headers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

initializeServerApp also takes an appCheckToken, does that need to be mentioned here, and how to use it? I guess it's in this official documentation page, do you expect the agent to already have found and read this page? https://firebase.google.com/docs/web/ssr-apps#:~:text=Use%20App%20Check%20in%20SSR%20environments,-App%20Check%20enforcement&text=The%20resulting%20token%20is%20then,initialization%20of%20the%20FirebaseServerApp%20instance.

});
```

### 2. Firestore Serialization Requirements

Data from `getFirestore` contains complex prototype objects (like `Timestamp`, `DocumentReference`, and `GeoPoint`) which cannot be natively serialized into JSON strings across network boundaries.

Always map over fetched Firestore documents to extract and convert these specific types to their serializable equivalents (such as `.toDate().toISOString()`) *before* returning them from the server component/loader.

### 3. Data Connect Serialization Differences

Unlike Firestore, Firebase Data Connect utilizes standard GraphQL over its protocol. Responses to generated query functions are immediately returned as perfectly serializable JSON primitives.

Data fetched via Data Connect Server SDKs (`executeGraphql` or generated SDKs like `@firebasegen/default-connector`) does not require manual conversion of structures before being passed as page props or signals.

### 4. Other Firebase Products (RTDB, Storage, Functions)

The `initializeServerApp` pattern is not limited to Firestore; you can safely initialize the client SDKs for Realtime Database, Cloud Storage, and Cloud Functions on the server. Because the app instance is authenticated via `authIdToken`, these calls will securely interact with Firebase infrastructure using the requesting user's identity.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to say "auth"? I didn't see any description of how to use initializeServerApp with Firestore above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't think I follow. Do you mean that it should say:

Suggested change
The `initializeServerApp` pattern is not limited to Firestore; you can safely initialize the client SDKs for Realtime Database, Cloud Storage, and Cloud Functions on the server. Because the app instance is authenticated via `authIdToken`, these calls will securely interact with Firebase infrastructure using the requesting user's identity.
The `initializeServerApp` pattern is not limited to Auth; you can safely initialize the client SDKs for Realtime Database, Cloud Storage, and Cloud Functions on the server. Because the app instance is authenticated via `authIdToken`, these calls will securely interact with Firebase infrastructure using the requesting user's identity.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't sure, that was my best guess at what you meant, since Firestore wasn't mentioned.

- **Realtime Database**: Data returned from `get(ref(db, 'path'))` is already primitively structured (JSON serializable).
- **Cloud Storage**: You can safely fetch download URLs utilizing `getDownloadURL(ref(storage, 'path'))` on the server.
- **Cloud Functions**: You can securely execute callable functions using `httpsCallable(functions, 'name')(data)` on the server on behalf of the user.

### 5. Resuming Server Context in the Client

Once you have initialized the server app and fetched data, it is a best-practice to seamlessly "resume" this state in the CSR (Client-Side Rendering) environment without generating a layout shift or making redundant network requests:

- **Firebase Auth Hydration**: Instead of rendering a blank or unauthenticated state while `onAuthStateChanged` initializes the client Firebase Auth SDK, pass the parsed user data (obtained from the decoded session cookie) as an initial property (e.g. `initialUser` prop in React, or via `TransferState` in Angular) to your client-side Auth Provider.
- **Firestore `onSnapshotResume`**: In Firebase JS v10+, if you initiate a Firestore query on the server (using `getDocs()` or `getDoc()`), you can pass the `.toJSON()` representation of that snapshot to the client. The client can then call `onSnapshotResume(db, serializedSnapshot, ...)` to immediately resume the listener from the server's state, preventing the client from re-downloading the initial snapshot.
- **Data Connect `subscribe`**: When executing generated queries on the server (e.g., `listMovies()`), the returned result object exposes a `.toJSON()` function. By passing this serialized representation to the client, you can hydrate initial UI state and supply it directly into the generated `subscribe(serializedQuery, ...)` function to resume watching for cache updates without re-triggering the initial query.
321 changes: 321 additions & 0 deletions skills/firebase-ssr/references/angular-ssr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
# Angular SSR Firebase Reference

When building with Angular Universal/SSR, properly initializing Firebase per-request prevents cross-contamination.

## 1. Initializing the Server App

Access the underlying Express `Request` object injected via the `REQUEST` token in Angular SSR.

```typescript
import { Injectable, Inject, Optional } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
Comment thread
maneesht marked this conversation as resolved.
Outdated
import { Request } from 'express';
import { initializeServerApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getFirestore } from "firebase/firestore";

@Injectable({ providedIn: 'root' })
export class FirebaseSSRService {
constructor(@Optional() @Inject(REQUEST) private request: Request) {}

public initializeApp() {
const firebaseConfig = {
apiKey: "...",
authDomain: "...",
projectId: "...",
// ...
};

// Extract custom Auth token securely from the injected HTTP request context
const authHeader = this.request?.headers['authorization'];
const authIdToken = authHeader ? authHeader.split('Bearer ')[1] : undefined;

const app = initializeServerApp(firebaseConfig, {
authIdToken: authIdToken,
releaseOnDeref: this.request
});

return {
app,
auth: getAuth(app),
db: getFirestore(app)
};
}
}
```

## 2. Firestore Serialization

When bridging server state over to the client browser via Angular `TransferState` or Signals, complex instances (`Timestamp` and `GeoPoint`) must be mapped to primitive JS types.

```typescript
// auth.service.ts
import { TransferState, makeStateKey } from '@angular/core';

import { User } from 'firebase/auth';
const USER_DATA_KEY = makeStateKey<User | null>('user_data_SSR');
Comment thread
maneesht marked this conversation as resolved.
Outdated

// In your Server Service resolving FireStore data
const docSnap = await getDoc(doc(db, "users", "123"));
const data = docSnap.data();

if (data) {
// Convert Firestore types to serialization-friendly primitives
const serializableData = {
...data,
createdAt: data.createdAt?.toDate().toISOString(),
updatedAt: data.updatedAt?.toDate().toISOString(),
};

// Safe to transfer!
this.transferState.set(USER_DATA_KEY, serializableData);
}
```

## 3. Data Connect Serialization

Firebase Data Connect returns standard JSON responses via its GraphQL endpoints. You do not need to do any serialization mapping before pushing the responses into a Signal or transferring them via Angular `TransferState`.

```typescript
import { listAllMenuItems } from '@firebasegen/default-connector';
import { TransferState, makeStateKey, Injectable } from '@angular/core';

interface MenuItem { id: string; title: string; url: string; }
const MENU_KEY = makeStateKey<MenuItem[]>('menu_ssr_state');

@Injectable({ providedIn: 'root' })
export class MenuService {
constructor(private transferState: TransferState) {}

async fetchMenuFromDataConnect() {
const response = await listAllMenuItems();

// 'response.data.menuItems' is already primitive JSON!
this.transferState.set(MENU_KEY, response.data.menuItems);
}
}
```

## 4. Realtime Database (RTDB)

Realtime Database snapshots are natively JSON-serializable, allowing them to be pushed directly into standard Angular mechanisms like `TransferState` without type massaging.

```typescript
import { getDatabase, ref, get } from "firebase/database";
import { TransferState, makeStateKey, Injectable } from '@angular/core';

interface ConfigState { featureFlag: boolean; version: string; }
const RTDB_KEY = makeStateKey<ConfigState | null>('rtdb_state');

@Injectable({ providedIn: 'root' })
export class RealtimeService {
constructor(
private transferState: TransferState,
private ssrContext: FirebaseSSRService // From Step 1
) {}

async fetchLeaderboard() {
const { app } = this.ssrContext.initializeApp();
const db = getDatabase(app);

const snapshot = await get(ref(db, "leaderboard"));
const data = snapshot.val(); // Fully serializable JSON

this.transferState.set(RTDB_KEY, data);
}
}
```

## 5. Cloud Storage for Firebase

You can securely retrieve download URLs for Storage objects by passing the context-aware app to `getStorage`.

```typescript
import { getStorage, ref, getDownloadURL } from "firebase/storage";
import { TransferState, makeStateKey, Injectable } from '@angular/core';

const AVATAR_KEY = makeStateKey<string>('avatar_url_state');

@Injectable({ providedIn: 'root' })
export class StorageService {
constructor(
private transferState: TransferState,
private ssrContext: FirebaseSSRService
) {}

async fetchUserAvatar() {
const { app } = this.ssrContext.initializeApp();
const storage = getStorage(app);

const fileRef = ref(storage, "users/me/avatar.png");
const url = await getDownloadURL(fileRef);

this.transferState.set(AVATAR_KEY, url);
}
}
```

## 6. Cloud Functions

You can securely invoke Firebase HTTP Callable functions natively on the server on behalf of the requesting user.

```typescript
import { getFunctions, httpsCallable } from "firebase/functions";
import { TransferState, makeStateKey, Injectable } from '@angular/core';

interface SubscriptionResult { id: string; status: string; }
const FUNC_RES_KEY = makeStateKey<SubscriptionResult | null>('func_res_state');

@Injectable({ providedIn: 'root' })
export class SubscriptionService {
constructor(
private transferState: TransferState,
private ssrContext: FirebaseSSRService
) {}

async checkoutTier(plan: string) {
const { app } = this.ssrContext.initializeApp();
const functions = getFunctions(app);

const createSubscription = httpsCallable(functions, 'createSubscription');
const result = await createSubscription({ plan });

this.transferState.set(FUNC_RES_KEY, result.data);
}
}
```

## 7. Resuming Server Context in the Client

To avoid client-side layout shifts and redundant network requests, resume the server-initialized Firebase state within your Angular components using `TransferState`.

### Firebase Auth Hydration
Instead of rendering a blank loading screen while `onAuthStateChanged` resolves, evaluate the user from cookies on the server, store it in `TransferState`, and use it as the initial signal state on the client.

```typescript
// auth.service.ts
import { Injectable, TransferState, makeStateKey, PLATFORM_ID, Inject, signal } from '@angular/core';
import { isServer } from '@angular/common';
import { getAuth, onAuthStateChanged, User } from 'firebase/auth';
import { app } from './firebase.client'; // Standard initializeApp

const USER_STATE_KEY = makeStateKey<User | null>('auth_user_state');

@Injectable({ providedIn: 'root' })
export class AuthService {
// Initialize signal synchronously with the server-transferred state (if available)
readonly currentUser = signal<User | null>(
this.transferState.get(USER_STATE_KEY, null)
);

constructor(
private transferState: TransferState,
@Inject(PLATFORM_ID) private platformId: Object
) {
if (isServer(this.platformId)) {
// Logic running on server: Decode cookie/token and set TransferState
// (This usually happens in an APP_INITIALIZER or server resolver)
// this.transferState.set(USER_STATE_KEY, decodedUser);
} else {
// Logic running on client: Listen to actual SDK state changes
const auth = getAuth(app);
onAuthStateChanged(auth, (user) => {
this.currentUser.set(user);
});
}
}
}
```

### Firestore `onSnapshotResume`
To instantly render streaming Firestore data without a second network trip, use the Firebase JS SDK v10+ `onSnapshotResume` API. Extract the `.toJSON()` snapshot from the server, transfer it, and resume it.

```typescript
// posts.service.ts
import { Injectable, TransferState, makeStateKey, PLATFORM_ID, Inject, signal } from '@angular/core';
import { isServer } from '@angular/common';
import { getFirestore, onSnapshotResume, collection, query, getDocs } from 'firebase/firestore';
import { app } from './firebase.client';
import { FirebaseSSRService } from './firebase-ssr.service';

interface Post { id: string; title: string; content: string }
const SNAPSHOT_KEY = makeStateKey<Record<string, unknown>>('firestore_snapshot');

@Injectable({ providedIn: 'root' })
export class PostService {
readonly posts = signal<Post[]>([]);

constructor(
private transferState: TransferState,
@Inject(PLATFORM_ID) private platformId: Object,
private ssrContext: FirebaseSSRService
) {}

async fetchAndListenPosts() {
if (isServer(this.platformId)) {
// Server: Fetch snapshot and serialize via .toJSON()
const { app } = this.ssrContext.initializeApp();
const db = getFirestore(app);
const q = query(collection(db, "posts"));
const snapshot = await getDocs(q);

this.transferState.set(SNAPSHOT_KEY, snapshot.toJSON());
} else {
// Client: Resume snapshot from TransferState
const serializedSnapshot = this.transferState.get(SNAPSHOT_KEY, null);
if (serializedSnapshot) {
const db = getFirestore(app);
onSnapshotResume(db, serializedSnapshot, (snapshot) => {
this.posts.set(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() })));
});
}
}
}
}
```

### Data Connect `subscribe` Resumption
Firebase Data Connect's JS SDK uses a similar architecture to `onSnapshotResume` to hydrate client-side watches from server results. You can serialize the `.toJSON()` representation of the query result into `TransferState`, and pass it directly into the generated `subscribe` function.

```typescript
// movies.service.ts
import { Injectable, TransferState, makeStateKey, PLATFORM_ID, Inject, signal } from '@angular/core';
import { isServer } from '@angular/common';
import { listMovies, ListMoviesData, ListMoviesVariables } from '@movie-app/dataconnect';
import { subscribe, SerializedRef } from 'firebase/data-connect';
import { FirebaseSSRService } from './firebase-ssr.service';

const QUERY_KEY = makeStateKey<SerializedRef<ListMoviesData, ListMoviesVariables>>('dataconnect_query_snapshot');

@Injectable({ providedIn: 'root' })
export class MoviesService {
// Use the extracted raw data as your initial state for 0 layout shift
readonly movies = signal<ListMoviesData['movies']>(
this.transferState.get(QUERY_KEY, undefined)?.data?.movies ?? []
);

constructor(
private transferState: TransferState,
@Inject(PLATFORM_ID) private platformId: Object,
private ssrContext: FirebaseSSRService
) {}

async fetchAndListenMovies() {
if (isServer(this.platformId)) {
// Server: Fetch QueryResult and serialize via .toJSON()
this.ssrContext.initializeApp();
const result = await listMovies();

this.transferState.set(QUERY_KEY, result.toJSON());
} else {
// Client: Resume subscription from TransferState SerializedRef
const serializedQuery = this.transferState.get(QUERY_KEY, null);
if (serializedQuery) {
subscribe(serializedQuery, {
onNext: (res) => this.movies.set(res.data.movies)
});
}
}
}
}
```
Loading
Loading