From a604ae8a42ae273dd962112b7bd6dc2357fd6cf7 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Fri, 27 Mar 2026 10:12:53 -0700 Subject: [PATCH 1/8] Added ssr skills --- skills/firebase-ssr/SKILL.md | 57 ++++++++++ skills/firebase-ssr/references/angular-ssr.md | 94 ++++++++++++++++ skills/firebase-ssr/references/nextjs.md | 86 +++++++++++++++ skills/firebase-ssr/references/remix.md | 101 ++++++++++++++++++ 4 files changed, 338 insertions(+) create mode 100644 skills/firebase-ssr/SKILL.md create mode 100644 skills/firebase-ssr/references/angular-ssr.md create mode 100644 skills/firebase-ssr/references/nextjs.md create mode 100644 skills/firebase-ssr/references/remix.md diff --git a/skills/firebase-ssr/SKILL.md b/skills/firebase-ssr/SKILL.md new file mode 100644 index 00000000..5313a4f9 --- /dev/null +++ b/skills/firebase-ssr/SKILL.md @@ -0,0 +1,57 @@ +--- +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`). + +*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 +}); +``` + +### 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. diff --git a/skills/firebase-ssr/references/angular-ssr.md b/skills/firebase-ssr/references/angular-ssr.md new file mode 100644 index 00000000..d1625a20 --- /dev/null +++ b/skills/firebase-ssr/references/angular-ssr.md @@ -0,0 +1,94 @@ +# 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'; +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 + }); + + 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'; + +const USER_DATA_KEY = makeStateKey('user_data_SSR'); + +// 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'; + +const MENU_KEY = makeStateKey('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); + } +} +``` diff --git a/skills/firebase-ssr/references/nextjs.md b/skills/firebase-ssr/references/nextjs.md new file mode 100644 index 00000000..66cf7d41 --- /dev/null +++ b/skills/firebase-ssr/references/nextjs.md @@ -0,0 +1,86 @@ +# Next.js (App Router) Firebase SSR Reference + +When building with Next.js App Router, utilize React Server Components (RSCs) alongside `initializeServerApp` to securely extract and pass data. + +## 1. Initializing the Server App + +Use the standard Next.js `headers()` or `cookies()` APIs to retrieve tokens for Firebase Auth without accessing the Express request object. + +```tsx +import { initializeServerApp } from "firebase/app"; +import { getAuth } from "firebase/auth"; +import { getFirestore } from "firebase/firestore"; +import { headers } from "next/headers"; + +export async function setupFirebaseSSR() { + const firebaseConfig = { + apiKey: "...", + authDomain: "...", + projectId: "...", + // ... + }; + + // Extract custom Auth token using modern Next.js headers() + const reqHeaders = await headers(); + const authIdToken = reqHeaders.get('authorization')?.split('Bearer ')[1]; + + const app = initializeServerApp(firebaseConfig, { + authIdToken: authIdToken + }); + + return { + app, + auth: getAuth(app), + db: getFirestore(app) + }; +} +``` + +## 2. Firestore Serialization + +When calling `getFirestore` from the Server and passing data into Client Components (`"use client"`), props must be serializable JSON. `Timestamp` or `GeoPoint` objects will throw an error during the Next.js build or SSR phase if they are not explicitly serialized. + +```tsx +// app/page.tsx (Server Component) +import { doc, getDoc } from "firebase/firestore"; +import ClientComponent from './ClientComponent'; + +export default async function Page() { + const { db } = await setupFirebaseSSR(); + const docSnap = await getDoc(doc(db, "users", "123")); + const data = docSnap.data(); + + if (!data) return null; + + // Convert Firestore types to standard JS primitives + const serializableData = { + ...data, + createdAt: data.createdAt?.toDate().toISOString(), + updatedAt: data.updatedAt?.toDate().toISOString(), + }; + + return ; +} +``` + +## 3. Data Connect Serialization + +Firebase Data Connect returns standard JSON responses via its GraphQL endpoints. You do not need to do any mapping or serialization before passing responses as props in Server Components. + +```tsx +// app/menu/page.tsx (Server Component) +import { listAllMenuItems } from '@firebasegen/default-connector'; +import { MenuList } from './client-components/MenuList'; + +export default async function MenuPage() { + const response = await listAllMenuItems(); + + // 'response.data.menuItems' is already primitive JSON! + return ( +
+

Menu

+ +
+ ); +} +``` diff --git a/skills/firebase-ssr/references/remix.md b/skills/firebase-ssr/references/remix.md new file mode 100644 index 00000000..4af7d266 --- /dev/null +++ b/skills/firebase-ssr/references/remix.md @@ -0,0 +1,101 @@ +# Remix Firebase SSR Reference + +When building with Remix, leverage `loader` and `action` functions to securely initialize Firebase contextually on the server. + +## 1. Initializing the Server App + +Extract custom tokens or session IDs using the `request` argument injected automatically by Remix. + +```tsx +import { initializeServerApp } from "firebase/app"; +import { getAuth } from "firebase/auth"; +import { getFirestore } from "firebase/firestore"; + +export async function setupFirebaseSSR(request: Request) { + const firebaseConfig = { + apiKey: "...", + authDomain: "...", + projectId: "...", + // ... + }; + + // Extract custom Auth token using standard Fetch API Headers + const authHeader = request.headers.get('Authorization'); + const authIdToken = authHeader?.split('Bearer ')[1]; + + const app = initializeServerApp(firebaseConfig, { + authIdToken: authIdToken + }); + + return { + app, + auth: getAuth(app), + db: getFirestore(app) + }; +} +``` + +## 2. Firestore Serialization + +When using standard `json()` loader responses to export data to your Remix Route Components, remember that `Timestamp` is not JSON-serializable. + +```tsx +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { doc, getDoc } from "firebase/firestore"; +import { useLoaderData } from "@remix-run/react"; + +export async function loader({ request }: LoaderFunctionArgs) { + const { db } = await setupFirebaseSSR(request); + const docSnap = await getDoc(doc(db, "posts", "some_id")); + const data = docSnap.data(); + + if (!data) throw new Response("Not Found", { status: 404 }); + + // Map out non-serializable fields + const post = { + ...data, + createdAt: data.createdAt?.toDate().toISOString(), + updatedAt: data.updatedAt?.toDate().toISOString(), + }; + + return json({ post }); +} + +export default function PostRoute() { + const { post } = useLoaderData(); + + return ( +
+

{post.title}

+

Created on: {new Date(post.createdAt).toLocaleDateString()}

+
+ ); +} +``` + +## 3. Data Connect Serialization + +Firebase Data Connect returns standard JSON responses via its GraphQL endpoints. You do not need to serialize `timestamps` when calling Data Connect generated functions. + +```tsx +import { json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { listAllMenuItems } from '@firebasegen/default-connector'; + +export async function loader() { + const response = await listAllMenuItems(); + + // Data connect responses are inherently serializable! + return json({ menuItems: response.data.menuItems }); +} + +export default function MenuRoute() { + const { menuItems } = useLoaderData(); + + return ( +
    + {menuItems.map(item =>
  • {item.name}
  • )} +
+ ); +} +``` From 8abdc85dc8f59a2f48e2e3566e4f7767c33fa46d Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Mon, 30 Mar 2026 15:56:35 -0700 Subject: [PATCH 2/8] Created agent skills --- skills/firebase-ssr/SKILL.md | 15 ++ skills/firebase-ssr/references/angular-ssr.md | 230 +++++++++++++++++- skills/firebase-ssr/references/nextjs.md | 172 +++++++++++++ skills/firebase-ssr/references/remix.md | 179 ++++++++++++++ 4 files changed, 594 insertions(+), 2 deletions(-) diff --git a/skills/firebase-ssr/SKILL.md b/skills/firebase-ssr/SKILL.md index 5313a4f9..babc60b6 100644 --- a/skills/firebase-ssr/SKILL.md +++ b/skills/firebase-ssr/SKILL.md @@ -55,3 +55,18 @@ Always map over fetched Firestore documents to extract and convert these specifi 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. +- **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. diff --git a/skills/firebase-ssr/references/angular-ssr.md b/skills/firebase-ssr/references/angular-ssr.md index d1625a20..5f7667b3 100644 --- a/skills/firebase-ssr/references/angular-ssr.md +++ b/skills/firebase-ssr/references/angular-ssr.md @@ -51,7 +51,8 @@ When bridging server state over to the client browser via Angular `TransferState // auth.service.ts import { TransferState, makeStateKey } from '@angular/core'; -const USER_DATA_KEY = makeStateKey('user_data_SSR'); +import { User } from 'firebase/auth'; +const USER_DATA_KEY = makeStateKey('user_data_SSR'); // In your Server Service resolving FireStore data const docSnap = await getDoc(doc(db, "users", "123")); @@ -78,7 +79,8 @@ Firebase Data Connect returns standard JSON responses via its GraphQL endpoints. import { listAllMenuItems } from '@firebasegen/default-connector'; import { TransferState, makeStateKey, Injectable } from '@angular/core'; -const MENU_KEY = makeStateKey('menu_ssr_state'); +interface MenuItem { id: string; title: string; url: string; } +const MENU_KEY = makeStateKey('menu_ssr_state'); @Injectable({ providedIn: 'root' }) export class MenuService { @@ -92,3 +94,227 @@ export class MenuService { } } ``` + +## 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('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('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('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('auth_user_state'); + +@Injectable({ providedIn: 'root' }) +export class AuthService { + // Initialize signal synchronously with the server-transferred state (if available) + readonly currentUser = signal( + 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>('firestore_snapshot'); + +@Injectable({ providedIn: 'root' }) +export class PostService { + readonly posts = signal([]); + + 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>('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( + 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) + }); + } + } + } +} +``` diff --git a/skills/firebase-ssr/references/nextjs.md b/skills/firebase-ssr/references/nextjs.md index 66cf7d41..5db70247 100644 --- a/skills/firebase-ssr/references/nextjs.md +++ b/skills/firebase-ssr/references/nextjs.md @@ -84,3 +84,175 @@ export default async function MenuPage() { ); } ``` + +## 4. Realtime Database (RTDB) + +Realtime Database snapshots are natively JSON-serializable, making them straightforward to pass to Client Components without complex mappings. + +```tsx +// app/rtdb/page.tsx (Server Component) +import { getDatabase, ref, get } from "firebase/database"; +import ClientComponent from './ClientComponent'; + +export default async function RealtimeDataPage() { + const { app } = await setupFirebaseSSR(); + const db = getDatabase(app); + + const snapshot = await get(ref(db, "leaderboard")); + const data = snapshot.val(); // Fully serializable JSON + + return ; +} +``` + +## 5. Cloud Storage for Firebase + +You can securely retrieve download URLs for Storage objects on the server leveraging the user's isolated context. + +```tsx +// app/storage/page.tsx (Server Component) +import { getStorage, ref, getDownloadURL } from "firebase/storage"; +import Image from "next/image"; + +export default async function StorageImagePage() { + const { app } = await setupFirebaseSSR(); + const storage = getStorage(app); + + const fileRef = ref(storage, "users/me/avatar.png"); + const url = await getDownloadURL(fileRef); + + return User Avatar; +} +``` + +## 6. Cloud Functions + +You can securely invoke Firebase HTTP Callable functions natively on the server before rendering the UI. + +```tsx +// app/functions/page.tsx (Server Component) +import { getFunctions, httpsCallable } from "firebase/functions"; +import ClientComponent from './ClientComponent'; + +export default async function FunctionsPage() { + const { app } = await setupFirebaseSSR(); + const functions = getFunctions(app); + + const createSubscription = httpsCallable(functions, 'createSubscription'); + const result = await createSubscription({ plan: "premium" }); + + return ; +} +``` + +## 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 Next.js Client Components. + +### Firebase Auth Hydration +Instead of rendering a blank loading screen while `onAuthStateChanged` resolves, pass the decoded user state (from your session cookie or `headers()`) down as a prop to your React context. + +```tsx +// app/ClientAuthProvider.tsx (Client Component) +"use client"; +import { useEffect, useState } from "react"; +import { getAuth, onAuthStateChanged, User } from "firebase/auth"; +import { app } from "./firebaseClient"; // standard initializeApp setup + +export function ClientAuthProvider({ initialUser, children }: { initialUser: User | null, children: React.ReactNode }) { + const [user, setUser] = useState(initialUser); + + useEffect(() => { + const auth = getAuth(app); + return onAuthStateChanged(auth, setUser); + }, []); + + return {children}; +} +``` + +### Firestore `onSnapshotResume` +To instantly render streaming Firestore data without a second network trip, use the Firebase JS SDK v10+ `onSnapshotResume` API. + +```tsx +// app/page.tsx (Server Component) +import { getFirestore, collection, getDocs, query } from "firebase/firestore"; + +export default async function Page() { + const { app } = await setupFirebaseSSR(); + const db = getFirestore(app); + + const q = query(collection(db, "posts")); + const snapshot = await getDocs(q); + + // Pass the internal .toJSON() representation of the snapshot to the client + return ; +} +``` + +```tsx +// app/PostList.tsx (Client Component) +"use client"; +import { useEffect, useState } from "react"; +import { getFirestore, onSnapshotResume } from "firebase/firestore"; +import { app } from "./firebaseClient"; + +export default function PostList({ serializedSnapshot }: { serializedSnapshot: object }) { + const [docs, setDocs] = useState([]); + + useEffect(() => { + const db = getFirestore(app); + + // Resume the listener from the server's snapshot state seamlessly + const unsubscribe = onSnapshotResume(db, serializedSnapshot, (snapshot) => { + setDocs(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))); + }); + + return unsubscribe; + }, [serializedSnapshot]); + + return
{docs.map(d =>
{d.title}
)}
; +} +``` + +### 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 pass the serialized representation of the query result directly into the generated `subscribe` function. + +```tsx +// app/movies/page.tsx (Server Component) +import { listMovies } from '@movie-app/dataconnect'; + +export default async function MoviesPage() { + await setupFirebaseSSR(); // Request-isolated app injection + + const result = await listMovies(); // Internal QueryResult object + + // Pass the internal .toJSON() SerializedRef representation to the client + return ; +} +``` + +```tsx +// app/movies/MoviesList.tsx (Client Component) +"use client"; +import { useEffect, useState } from "react"; +import { subscribe, SerializedRef } from 'firebase/data-connect'; +import { ListMoviesData, ListMoviesVariables } from '@movie-app/dataconnect'; + +export default function MoviesList({ serializedQuery }: { serializedQuery: SerializedRef }) { + // Use the extracted raw data as your initial state for 0 layout shift + const [movies, setMovies] = useState(serializedQuery.data.movies); + + useEffect(() => { + // Resume the subscription utilizing the serialized reference state + // Without recreating the query context from scratch + const unsubscribe = subscribe(serializedQuery, { + onNext: (res) => setMovies(res.data.movies) + }); + + return unsubscribe; + }, [serializedQuery]); + + return
{movies.map(m =>
{m.title}
)}
; +} +``` diff --git a/skills/firebase-ssr/references/remix.md b/skills/firebase-ssr/references/remix.md index 4af7d266..59febf8a 100644 --- a/skills/firebase-ssr/references/remix.md +++ b/skills/firebase-ssr/references/remix.md @@ -99,3 +99,182 @@ export default function MenuRoute() { ); } ``` + +## 4. Realtime Database (RTDB) + +Realtime Database snapshots are natively JSON-serializable, allowing them to be returned directly inside Remix `json()` loaders. + +```tsx +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { getDatabase, ref, get } from "firebase/database"; +import { useLoaderData } from "@remix-run/react"; + +export async function loader({ request }: LoaderFunctionArgs) { + const { app } = await setupFirebaseSSR(request); + const db = getDatabase(app); + + const snapshot = await get(ref(db, "leaderboard")); + const data = snapshot.val(); // Fully serializable JSON + + return json({ leaderboardData: data }); +} + +export default function RealtimeDataRoute() { + const { leaderboardData } = useLoaderData(); + + return
{JSON.stringify(leaderboardData, null, 2)}
; +} +``` + +## 5. Cloud Storage for Firebase + +You can securely retrieve download URLs for Storage objects within a loader before the page renders to the user. + +```tsx +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { getStorage, ref, getDownloadURL } from "firebase/storage"; +import { useLoaderData } from "@remix-run/react"; + +export async function loader({ request }: LoaderFunctionArgs) { + const { app } = await setupFirebaseSSR(request); + const storage = getStorage(app); + + const fileRef = ref(storage, "users/me/avatar.png"); + const url = await getDownloadURL(fileRef); + + return json({ avatarUrl: url }); +} + +export default function StorageRoute() { + const { avatarUrl } = useLoaderData(); + + return User Avatar; +} +``` + +## 6. Cloud Functions + +You can securely invoke Firebase HTTP Callable functions natively on the server during an `action` or `loader`. + +```tsx +import { json, type ActionFunctionArgs } from "@remix-run/node"; +import { getFunctions, httpsCallable } from "firebase/functions"; + +export async function action({ request }: ActionFunctionArgs) { + const { app } = await setupFirebaseSSR(request); + const functions = getFunctions(app); + + const formData = await request.formData(); + const plan = formData.get("plan"); + + const createSubscription = httpsCallable(functions, 'createSubscription'); + const result = await createSubscription({ plan }); + + return json({ subscriptionResult: 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 Remix components. + +### Firebase Auth Hydration +Instead of rendering a blank loading screen while `onAuthStateChanged` resolves, pass the decoded user state (from your session cookie inside the `loader`) down via `useLoaderData` to initialize your React context. + +```tsx +import { useEffect, useState } from "react"; +import { useLoaderData } from "@remix-run/react"; +import { getAuth, onAuthStateChanged, User } from "firebase/auth"; +import { app } from "~/firebaseClient"; // standard initializeApp setup + +export default function Root() { + // initialUser is extracted from the session cookie in your root loader + const { initialUser } = useLoaderData(); + const [user, setUser] = useState(initialUser); + + useEffect(() => { + const auth = getAuth(app); + return onAuthStateChanged(auth, setUser); + }, []); + + return ; +} +``` + +### Firestore `onSnapshotResume` +To instantly render streaming Firestore data without a second network trip, use the Firebase JS SDK v10+ `onSnapshotResume` API. Return the internal `.toJSON()` representation from your `loader`. + +```tsx +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { getFirestore, collection, getDocs, query, onSnapshotResume } from "firebase/firestore"; +import { useEffect, useState } from "react"; +import { app } from "~/firebaseClient"; + +export async function loader({ request }: LoaderFunctionArgs) { + const { app } = await setupFirebaseSSR(request); + const db = getFirestore(app); + + const q = query(collection(db, "posts")); + const snapshot = await getDocs(q); + + // Pass the internal .toJSON() representation of the snapshot to the client + return json({ serializedSnapshot: snapshot.toJSON() }); +} + +export default function PostRoute() { + const { serializedSnapshot } = useLoaderData(); + const [docs, setDocs] = useState([]); + + useEffect(() => { + const db = getFirestore(app); + + // Resume the listener from the server's snapshot state seamlessly + const unsubscribe = onSnapshotResume(db, serializedSnapshot, (snapshot) => { + setDocs(snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))); + }); + + return unsubscribe; + }, [serializedSnapshot]); + + return
{docs.map(d =>
{d.title}
)}
; +} +``` + +### 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 return the `.toJSON()` serialized representation of the query result from your loader, and pass it directly into the generated `subscribe` function. + +```tsx +import { json, type LoaderFunctionArgs } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; +import { useEffect, useState } from "react"; +import { subscribe, SerializedRef } from 'firebase/data-connect'; +import { listMovies, ListMoviesData, ListMoviesVariables } from '@movie-app/dataconnect'; + +export async function loader({ request }: LoaderFunctionArgs) { + await setupFirebaseSSR(request); + + const result = await listMovies(); + + // Pass the internal .toJSON() SerializedRef representation to the client + return json({ serializedQuery: result.toJSON() }); +} + +export default function MoviesRoute() { + const { serializedQuery } = useLoaderData() as { serializedQuery: SerializedRef }; + + // Use the extracted raw data as your initial state for 0 layout shift + const [movies, setMovies] = useState(serializedQuery.data.movies); + + useEffect(() => { + // Resume the subscription utilizing the serialized reference state + const unsubscribe = subscribe(serializedQuery, { + onNext: (res) => setMovies(res.data.movies) + }); + + return unsubscribe; + }, [serializedQuery]); + + return
{movies.map(m =>
{m.title}
)}
; +} +``` From 0bfd4b79fbbc70c70e0a366cdf4daf25e0505ca1 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Wed, 15 Apr 2026 17:03:05 -0700 Subject: [PATCH 3/8] Updated nextjs advise --- skills/firebase-ssr/references/nextjs.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skills/firebase-ssr/references/nextjs.md b/skills/firebase-ssr/references/nextjs.md index 5db70247..239dfb8f 100644 --- a/skills/firebase-ssr/references/nextjs.md +++ b/skills/firebase-ssr/references/nextjs.md @@ -2,6 +2,9 @@ When building with Next.js App Router, utilize React Server Components (RSCs) alongside `initializeServerApp` to securely extract and pass data. +## Cache Components +Currently, cached components are not supported. If the user is using Next.js v16, ensure that the `cacheComponents` is set to `false` in `next.config.ts`. + ## 1. Initializing the Server App Use the standard Next.js `headers()` or `cookies()` APIs to retrieve tokens for Firebase Auth without accessing the Express request object. From 7588713594ad9fffccae1babf1ffeab467726eac Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Thu, 16 Apr 2026 10:05:03 -0700 Subject: [PATCH 4/8] Added releaseOnDeref --- skills/firebase-ssr/references/angular-ssr.md | 3 ++- skills/firebase-ssr/references/nextjs.md | 3 ++- skills/firebase-ssr/references/remix.md | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/skills/firebase-ssr/references/angular-ssr.md b/skills/firebase-ssr/references/angular-ssr.md index 5f7667b3..b179d5dc 100644 --- a/skills/firebase-ssr/references/angular-ssr.md +++ b/skills/firebase-ssr/references/angular-ssr.md @@ -31,7 +31,8 @@ export class FirebaseSSRService { const authIdToken = authHeader ? authHeader.split('Bearer ')[1] : undefined; const app = initializeServerApp(firebaseConfig, { - authIdToken: authIdToken + authIdToken: authIdToken, + releaseOnDeref: this.request }); return { diff --git a/skills/firebase-ssr/references/nextjs.md b/skills/firebase-ssr/references/nextjs.md index 239dfb8f..0fda3d90 100644 --- a/skills/firebase-ssr/references/nextjs.md +++ b/skills/firebase-ssr/references/nextjs.md @@ -28,7 +28,8 @@ export async function setupFirebaseSSR() { const authIdToken = reqHeaders.get('authorization')?.split('Bearer ')[1]; const app = initializeServerApp(firebaseConfig, { - authIdToken: authIdToken + authIdToken: authIdToken, + releaseOnDeref: reqHeaders }); return { diff --git a/skills/firebase-ssr/references/remix.md b/skills/firebase-ssr/references/remix.md index 59febf8a..9c594b3b 100644 --- a/skills/firebase-ssr/references/remix.md +++ b/skills/firebase-ssr/references/remix.md @@ -24,7 +24,8 @@ export async function setupFirebaseSSR(request: Request) { const authIdToken = authHeader?.split('Bearer ')[1]; const app = initializeServerApp(firebaseConfig, { - authIdToken: authIdToken + authIdToken: authIdToken, + releaseOnDeref: request }); return { From 57a9ff9513f172ebd1026b60336bedb7b1f92738 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Thu, 16 Apr 2026 10:57:17 -0700 Subject: [PATCH 5/8] Update skills/firebase-ssr/SKILL.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- skills/firebase-ssr/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/firebase-ssr/SKILL.md b/skills/firebase-ssr/SKILL.md index babc60b6..3ff5107d 100644 --- a/skills/firebase-ssr/SKILL.md +++ b/skills/firebase-ssr/SKILL.md @@ -14,9 +14,9 @@ The core concepts of Firebase SSR (request isolation and serialization mappings) **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`). +- `[references/nextjs.md](./references/nextjs.md)` - For Next.js App Router (RSCs, Route Handlers). +- `[references/remix.md](./references/remix.md)` - For Remix (`loader` / `action` functions). +- `[references/angular-ssr.md](./references/angular-ssr.md)` - For Angular Universal/SSR (`REQUEST` token and `TransferState`). *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.* From 3501907fc4354dca1adc4d5b0409b2bb08e55b6d Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Thu, 16 Apr 2026 10:58:34 -0700 Subject: [PATCH 6/8] Update skills/firebase-ssr/references/angular-ssr.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- skills/firebase-ssr/references/angular-ssr.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/firebase-ssr/references/angular-ssr.md b/skills/firebase-ssr/references/angular-ssr.md index b179d5dc..0770fb1b 100644 --- a/skills/firebase-ssr/references/angular-ssr.md +++ b/skills/firebase-ssr/references/angular-ssr.md @@ -8,7 +8,7 @@ Access the underlying Express `Request` object injected via the `REQUEST` token ```typescript import { Injectable, Inject, Optional } from '@angular/core'; -import { REQUEST } from '@nguniversal/express-engine/tokens'; +import { REQUEST } from '@angular/ssr'; import { Request } from 'express'; import { initializeServerApp } from "firebase/app"; import { getAuth } from "firebase/auth"; From 8ffb170f7a25a70072001a763f0463d33c0585f8 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Thu, 16 Apr 2026 11:10:43 -0700 Subject: [PATCH 7/8] Updated auth --- skills/firebase-ssr/references/angular-ssr.md | 66 ++++++++++++++----- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/skills/firebase-ssr/references/angular-ssr.md b/skills/firebase-ssr/references/angular-ssr.md index 0770fb1b..82fe7aa2 100644 --- a/skills/firebase-ssr/references/angular-ssr.md +++ b/skills/firebase-ssr/references/angular-ssr.md @@ -49,26 +49,60 @@ export class FirebaseSSRService { 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'; +// user.service.ts +import { Injectable, TransferState, makeStateKey, PLATFORM_ID, Inject, signal } from '@angular/core'; +import { isServer } from '@angular/common'; +import { doc, getDoc, Firestore } from 'firebase/firestore'; -import { User } from 'firebase/auth'; -const USER_DATA_KEY = makeStateKey('user_data_SSR'); +interface UserData { id: string; name: string; createdAt: string; updatedAt: string; } +const USER_DATA_KEY = makeStateKey('user_data_SSR'); -// In your Server Service resolving FireStore data -const docSnap = await getDoc(doc(db, "users", "123")); -const data = docSnap.data(); +@Injectable({ providedIn: 'root' }) +export class UserService { + // Initialize signal synchronously with the server-transferred state (if available) + readonly userData = signal( + this.transferState.get(USER_DATA_KEY, null) + ); -if (data) { - // Convert Firestore types to serialization-friendly primitives - const serializableData = { - ...data, - createdAt: data.createdAt?.toDate().toISOString(), - updatedAt: data.updatedAt?.toDate().toISOString(), - }; + constructor( + private transferState: TransferState, + @Inject(PLATFORM_ID) private platformId: Object, + private db: Firestore // Assume provided or fetched via service + ) {} - // Safe to transfer! - this.transferState.set(USER_DATA_KEY, serializableData); + async fetchUser(userId: string) { + if (isServer(this.platformId)) { + // Server: Fetch data and serialize + const docSnap = await getDoc(doc(this.db, "users", userId)); + 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(), + } as UserData; + + // Safe to transfer! + this.transferState.set(USER_DATA_KEY, serializableData); + this.userData.set(serializableData); + } + } else { + // Client: If not found in TransferState (e.g., direct client navigation), fetch it + if (!this.userData()) { + const docSnap = await getDoc(doc(this.db, "users", userId)); + const data = docSnap.data(); + if (data) { + this.userData.set({ + ...data, + createdAt: data.createdAt?.toDate().toISOString(), + updatedAt: data.updatedAt?.toDate().toISOString(), + } as UserData); + } + } + } + } } ``` From 67536dccb635af48ecaf675571c633079f22d914 Mon Sep 17 00:00:00 2001 From: Maneesh Tewani Date: Thu, 23 Apr 2026 14:49:29 -0700 Subject: [PATCH 8/8] Added references to app check --- skills/firebase-ssr/SKILL.md | 5 +++-- skills/firebase-ssr/references/angular-ssr.md | 4 +++- skills/firebase-ssr/references/nextjs.md | 4 +++- skills/firebase-ssr/references/remix.md | 4 +++- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/skills/firebase-ssr/SKILL.md b/skills/firebase-ssr/SKILL.md index 3ff5107d..d570ef81 100644 --- a/skills/firebase-ssr/SKILL.md +++ b/skills/firebase-ssr/SKILL.md @@ -33,14 +33,15 @@ In a Node.js SSR context, utilizing the single `initializeApp` singleton is extr > [!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. +Instead, use `initializeServerApp` to create a lightweight, request-scoped Firebase app instance. You can pass both the user's Auth ID token and an App Check token (if enabled) to authenticate the server-side requests on behalf of the client. ```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 + authIdToken: extractedToken, // Provided by framework-specific headers + appCheckToken: extractedAppCheckToken // Optional, provided by framework-specific headers if App Check is enabled }); ``` diff --git a/skills/firebase-ssr/references/angular-ssr.md b/skills/firebase-ssr/references/angular-ssr.md index 82fe7aa2..b0907663 100644 --- a/skills/firebase-ssr/references/angular-ssr.md +++ b/skills/firebase-ssr/references/angular-ssr.md @@ -26,12 +26,14 @@ export class FirebaseSSRService { // ... }; - // Extract custom Auth token securely from the injected HTTP request context + // Extract custom Auth token and App Check token securely from the injected HTTP request context const authHeader = this.request?.headers['authorization']; const authIdToken = authHeader ? authHeader.split('Bearer ')[1] : undefined; + const appCheckToken = this.request?.headers['x-firebase-appcheck'] as string | undefined; const app = initializeServerApp(firebaseConfig, { authIdToken: authIdToken, + appCheckToken: appCheckToken, releaseOnDeref: this.request }); diff --git a/skills/firebase-ssr/references/nextjs.md b/skills/firebase-ssr/references/nextjs.md index 0fda3d90..371ac0f9 100644 --- a/skills/firebase-ssr/references/nextjs.md +++ b/skills/firebase-ssr/references/nextjs.md @@ -23,12 +23,14 @@ export async function setupFirebaseSSR() { // ... }; - // Extract custom Auth token using modern Next.js headers() + // Extract custom Auth token and App Check token using modern Next.js headers() const reqHeaders = await headers(); const authIdToken = reqHeaders.get('authorization')?.split('Bearer ')[1]; + const appCheckToken = reqHeaders.get('x-firebase-appcheck'); const app = initializeServerApp(firebaseConfig, { authIdToken: authIdToken, + appCheckToken: appCheckToken, releaseOnDeref: reqHeaders }); diff --git a/skills/firebase-ssr/references/remix.md b/skills/firebase-ssr/references/remix.md index 9c594b3b..04b3bbd5 100644 --- a/skills/firebase-ssr/references/remix.md +++ b/skills/firebase-ssr/references/remix.md @@ -19,12 +19,14 @@ export async function setupFirebaseSSR(request: Request) { // ... }; - // Extract custom Auth token using standard Fetch API Headers + // Extract custom Auth token and App Check token using standard Fetch API Headers const authHeader = request.headers.get('Authorization'); const authIdToken = authHeader?.split('Bearer ')[1]; + const appCheckToken = request.headers.get('x-firebase-appcheck'); const app = initializeServerApp(firebaseConfig, { authIdToken: authIdToken, + appCheckToken: appCheckToken, releaseOnDeref: request });