AgentSkillsCN

Firebase Nextjs Patterns

Firebase Next.js 模式

SKILL.md

Firebase + Next.js Patterns

Firebase Client SDK Setup

typescript
// lib/firebase/client.ts
import { initializeApp, getApps, FirebaseApp } from 'firebase/app'
import { getAuth, Auth } from 'firebase/auth'
import { getFirestore, Firestore } from 'firebase/firestore'

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
}

// Initialize Firebase (client-side singleton)
let app: FirebaseApp
let auth: Auth
let db: Firestore

export function getFirebaseClient() {
  if (!getApps().length) {
    app = initializeApp(firebaseConfig)
    auth = getAuth(app)
    db = getFirestore(app)
  }
  return { app, auth, db }
}

export { app, auth, db }

Firebase Admin SDK Setup

typescript
// lib/firebase/admin.ts
import { initializeApp, getApps, cert, App } from 'firebase-admin/app'
import { getAuth, Auth } from 'firebase-admin/auth'
import { getFirestore, Firestore } from 'firebase-admin/firestore'

// Central database (for owners registry)
let adminApp: App
let adminAuth: Auth
let adminDb: Firestore

export function getFirebaseAdmin() {
  if (!getApps().length) {
    adminApp = initializeApp({
      credential: cert({
        projectId: process.env.FIREBASE_PROJECT_ID,
        clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
        privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
      }),
    })
    adminAuth = getAuth(adminApp)
    adminDb = getFirestore(adminApp)
  }
  return { adminApp, adminAuth, adminDb }
}

export { adminApp, adminAuth, adminDb }

Multi-Tenant Pattern

typescript
// lib/firebase/admin.ts (continued)
import { initializeApp, getApp, deleteApp, App } from 'firebase-admin/app'
import { getFirestore, Firestore } from 'firebase-admin/firestore'

interface OwnerFirebaseConfig {
  projectId: string
  clientEmail: string
  privateKey: string
}

// Cache for owner database connections
const ownerDbCache = new Map<string, Firestore>()

export async function getOwnerDatabase(ownerId: string): Promise<Firestore> {
  // Check cache first
  if (ownerDbCache.has(ownerId)) {
    return ownerDbCache.get(ownerId)!
  }

  // Get owner's Firebase config from central database
  const { adminDb } = getFirebaseAdmin()
  const ownerDoc = await adminDb.collection('owners').doc(ownerId).get()

  if (!ownerDoc.exists) {
    throw new Error('Owner not found')
  }

  const ownerConfig = ownerDoc.data()?.firebaseConfig as OwnerFirebaseConfig

  // Initialize owner's Firebase app
  const appName = `owner-${ownerId}`
  let ownerApp: App

  try {
    ownerApp = getApp(appName)
  } catch {
    ownerApp = initializeApp({
      credential: cert({
        projectId: ownerConfig.projectId,
        clientEmail: ownerConfig.clientEmail,
        privateKey: ownerConfig.privateKey.replace(/\\n/g, '\n'),
      }),
    }, appName)
  }

  const ownerDb = getFirestore(ownerApp)
  ownerDbCache.set(ownerId, ownerDb)

  return ownerDb
}

// Cleanup function (optional, for memory management)
export async function disconnectOwnerDatabase(ownerId: string) {
  const appName = `owner-${ownerId}`
  try {
    const app = getApp(appName)
    await deleteApp(app)
    ownerDbCache.delete(ownerId)
  } catch {
    // App doesn't exist, ignore
  }
}

Firestore CRUD Patterns

typescript
// lib/firebase/firestore/clients.ts
import {
  collection,
  doc,
  getDoc,
  getDocs,
  addDoc,
  updateDoc,
  deleteDoc,
  query,
  where,
  orderBy,
  Timestamp,
  Firestore,
} from 'firebase/firestore'
import { Client } from '@/types'

const COLLECTION = 'clients'

// Create
export async function createClient(
  db: Firestore,
  data: Omit<Client, 'id' | 'createdAt' | 'updatedAt'>
): Promise<string> {
  const docRef = await addDoc(collection(db, COLLECTION), {
    ...data,
    createdAt: Timestamp.now(),
    updatedAt: Timestamp.now(),
    deletedAt: null,
  })
  return docRef.id
}

// Read one
export async function getClient(db: Firestore, id: string): Promise<Client | null> {
  const docRef = doc(db, COLLECTION, id)
  const docSnap = await getDoc(docRef)

  if (!docSnap.exists() || docSnap.data().deletedAt !== null) {
    return null
  }

  return {
    id: docSnap.id,
    ...docSnap.data(),
    createdAt: docSnap.data().createdAt.toDate().toISOString(),
    updatedAt: docSnap.data().updatedAt.toDate().toISOString(),
  } as Client
}

// Read all (with soft delete filter)
export async function getClients(db: Firestore): Promise<Client[]> {
  const q = query(
    collection(db, COLLECTION),
    where('deletedAt', '==', null),
    orderBy('createdAt', 'desc')
  )
  const querySnapshot = await getDocs(q)

  return querySnapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
    createdAt: doc.data().createdAt.toDate().toISOString(),
    updatedAt: doc.data().updatedAt.toDate().toISOString(),
  })) as Client[]
}

// Update
export async function updateClient(
  db: Firestore,
  id: string,
  data: Partial<Omit<Client, 'id' | 'createdAt'>>
): Promise<void> {
  const docRef = doc(db, COLLECTION, id)
  await updateDoc(docRef, {
    ...data,
    updatedAt: Timestamp.now(),
  })
}

// Soft delete
export async function deleteClient(db: Firestore, id: string): Promise<void> {
  const docRef = doc(db, COLLECTION, id)
  await updateDoc(docRef, {
    deletedAt: Timestamp.now(),
    updatedAt: Timestamp.now(),
  })
}

// Query by field
export async function getClientsByEstablishment(
  db: Firestore,
  establishmentId: string
): Promise<Client[]> {
  const q = query(
    collection(db, COLLECTION),
    where('establishmentId', '==', establishmentId),
    where('deletedAt', '==', null),
    orderBy('name', 'asc')
  )
  const querySnapshot = await getDocs(q)

  return querySnapshot.docs.map(doc => ({
    id: doc.id,
    ...doc.data(),
    createdAt: doc.data().createdAt.toDate().toISOString(),
    updatedAt: doc.data().updatedAt.toDate().toISOString(),
  })) as Client[]
}

Real-time Subscriptions (Client-side)

typescript
// hooks/useClientsRealtime.ts
'use client'

import { useState, useEffect } from 'react'
import { collection, query, where, orderBy, onSnapshot, Firestore } from 'firebase/firestore'
import { Client } from '@/types'

export function useClientsRealtime(db: Firestore, establishmentId: string) {
  const [clients, setClients] = useState<Client[]>([])
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const q = query(
      collection(db, 'clients'),
      where('establishmentId', '==', establishmentId),
      where('deletedAt', '==', null),
      orderBy('name', 'asc')
    )

    const unsubscribe = onSnapshot(
      q,
      (snapshot) => {
        const data = snapshot.docs.map(doc => ({
          id: doc.id,
          ...doc.data(),
          createdAt: doc.data().createdAt?.toDate().toISOString(),
          updatedAt: doc.data().updatedAt?.toDate().toISOString(),
        })) as Client[]

        setClients(data)
        setLoading(false)
      },
      (err) => {
        setError(err.message)
        setLoading(false)
      }
    )

    return () => unsubscribe()
  }, [db, establishmentId])

  return { clients, loading, error }
}

Authentication Patterns

Client-side Auth Hook

typescript
// hooks/useAuth.ts
'use client'

import { useState, useEffect } from 'react'
import {
  onAuthStateChanged,
  signInWithEmailAndPassword,
  signOut as firebaseSignOut,
  User,
} from 'firebase/auth'
import { getFirebaseClient } from '@/lib/firebase/client'

export function useAuth() {
  const [user, setUser] = useState<User | null>(null)
  const [loading, setLoading] = useState(true)
  const { auth } = getFirebaseClient()

  useEffect(() => {
    const unsubscribe = onAuthStateChanged(auth, (user) => {
      setUser(user)
      setLoading(false)
    })
    return () => unsubscribe()
  }, [auth])

  const signIn = async (email: string, password: string) => {
    return signInWithEmailAndPassword(auth, email, password)
  }

  const signOut = async () => {
    await firebaseSignOut(auth)
  }

  return { user, loading, signIn, signOut }
}

Server-side Auth (API Routes)

typescript
// lib/auth/verifyToken.ts
import { getFirebaseAdmin } from '@/lib/firebase/admin'
import { DecodedIdToken } from 'firebase-admin/auth'

export async function verifyIdToken(token: string): Promise<DecodedIdToken | null> {
  try {
    const { adminAuth } = getFirebaseAdmin()
    return await adminAuth.verifyIdToken(token)
  } catch {
    return null
  }
}

// Usage in API route
export async function GET(request: Request) {
  const authHeader = request.headers.get('Authorization')

  if (!authHeader?.startsWith('Bearer ')) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  const token = authHeader.split('Bearer ')[1]
  const decodedToken = await verifyIdToken(token)

  if (!decodedToken) {
    return Response.json({ error: 'Invalid token' }, { status: 401 })
  }

  // Use decodedToken.uid to identify the user
  const userId = decodedToken.uid
  // ...
}

Session Cookies (for SSR)

typescript
// lib/auth/session.ts
import { cookies } from 'next/headers'
import { getFirebaseAdmin } from '@/lib/firebase/admin'

const SESSION_COOKIE_NAME = 'session'
const COOKIE_MAX_AGE = 60 * 60 * 24 * 5 * 1000 // 5 days

export async function createSessionCookie(idToken: string) {
  const { adminAuth } = getFirebaseAdmin()
  const sessionCookie = await adminAuth.createSessionCookie(idToken, {
    expiresIn: COOKIE_MAX_AGE,
  })

  const cookieStore = await cookies()
  cookieStore.set(SESSION_COOKIE_NAME, sessionCookie, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'lax',
    maxAge: COOKIE_MAX_AGE / 1000,
    path: '/',
  })
}

export async function verifySessionCookie() {
  const cookieStore = await cookies()
  const sessionCookie = cookieStore.get(SESSION_COOKIE_NAME)?.value

  if (!sessionCookie) return null

  try {
    const { adminAuth } = getFirebaseAdmin()
    return await adminAuth.verifySessionCookie(sessionCookie, true)
  } catch {
    return null
  }
}

export async function clearSessionCookie() {
  const cookieStore = await cookies()
  cookieStore.delete(SESSION_COOKIE_NAME)
}

Environment Variables

env
# .env.local

# Firebase Client SDK (public)
NEXT_PUBLIC_FIREBASE_API_KEY=your-api-key
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=your-project-id
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=123456789
NEXT_PUBLIC_FIREBASE_APP_ID=1:123456789:web:abc123

# Firebase Admin SDK (private - never expose)
FIREBASE_PROJECT_ID=your-project-id
FIREBASE_CLIENT_EMAIL=firebase-adminsdk-xxx@your-project.iam.gserviceaccount.com
FIREBASE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYour-Private-Key\n-----END PRIVATE KEY-----\n"

Firestore Indexes

json
// firestore.indexes.json
{
  "indexes": [
    {
      "collectionGroup": "clients",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "establishmentId", "order": "ASCENDING" },
        { "fieldPath": "deletedAt", "order": "ASCENDING" },
        { "fieldPath": "name", "order": "ASCENDING" }
      ]
    },
    {
      "collectionGroup": "appointments",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "establishmentId", "order": "ASCENDING" },
        { "fieldPath": "date", "order": "ASCENDING" },
        { "fieldPath": "deletedAt", "order": "ASCENDING" }
      ]
    }
  ]
}

Security Rules

javascript
// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Owners can only access their own data
    match /clients/{clientId} {
      allow read, write: if request.auth != null
        && request.auth.uid == resource.data.ownerId;
    }

    match /appointments/{appointmentId} {
      allow read, write: if request.auth != null
        && request.auth.uid == resource.data.ownerId;
    }
  }
}

Timestamp Helpers

typescript
// lib/utils/timestamp.ts
import { Timestamp } from 'firebase/firestore'

export function toFirestoreTimestamp(date: Date | string): Timestamp {
  const d = typeof date === 'string' ? new Date(date) : date
  return Timestamp.fromDate(d)
}

export function fromFirestoreTimestamp(timestamp: Timestamp): string {
  return timestamp.toDate().toISOString()
}

export function serverTimestamp() {
  return Timestamp.now()
}