Firebase Service Library
The Firebase Service Library provides comprehensive integration with Firebase services for the Ring platform, including authentication, Firestore database operations, file storage, and cloud functions.
Overviewβ
The Firebase library enables:
- Authentication - Multi-provider authentication with NextAuth.js integration
- Firestore Database - Real-time document database operations
- Cloud Storage - File upload and management
- Server-Side Operations - Firebase Admin SDK integration
- Real-time Subscriptions - Live data updates
- Security Rules - Role-based access control
Architectureβ
Client-Side Firebase (lib/firebase-service.ts
)β
The client-side Firebase service handles browser operations:
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
import { getStorage } from 'firebase/storage'
import { getAuth } from 'firebase/auth'
// Firebase configuration
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
const app = initializeApp(firebaseConfig)
// Export services
export const db = getFirestore(app)
export const storage = getStorage(app)
export const auth = getAuth(app)
Server-Side Firebase (lib/firebase-admin.server.ts
)β
The server-side Firebase service handles secure operations:
import { initializeApp, getApps, cert } from 'firebase-admin/app'
import { getFirestore } from 'firebase-admin/firestore'
import { getStorage } from 'firebase-admin/storage'
import { getAuth } from 'firebase-admin/auth'
// Initialize Firebase Admin
const firebaseAdminConfig = {
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n')
}),
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET
}
// Singleton pattern for admin app
export const getAdminApp = () => {
if (getApps().length === 0) {
return initializeApp(firebaseAdminConfig)
}
return getApps()[0]
}
// Export admin services
export const getAdminDb = () => getFirestore(getAdminApp())
export const getAdminStorage = () => getStorage(getAdminApp())
export const getAdminAuth = () => getAuth(getAdminApp())
Core Operationsβ
Document Operationsβ
Create Documentβ
import { collection, addDoc, serverTimestamp } from 'firebase/firestore'
import { db } from '@/lib/firebase-service'
export async function createDocument<T>(
collectionName: string,
data: Omit<T, 'id' | 'created_at' | 'updated_at'>
): Promise<string> {
const docRef = await addDoc(collection(db, collectionName), {
...data,
created_at: serverTimestamp(),
updated_at: serverTimestamp()
})
return docRef.id
}
Read Documentβ
import { doc, getDoc } from 'firebase/firestore'
import { db } from '@/lib/firebase-service'
export async function getDocument<T>(
collectionName: string,
docId: string
): Promise<T | null> {
const docRef = doc(db, collectionName, docId)
const docSnap = await getDoc(docRef)
if (docSnap.exists()) {
return { id: docSnap.id, ...docSnap.data() } as T
}
return null
}
Update Documentβ
import { doc, updateDoc, serverTimestamp } from 'firebase/firestore'
import { db } from '@/lib/firebase-service'
export async function updateDocument(
collectionName: string,
docId: string,
data: Partial<any>
): Promise<void> {
const docRef = doc(db, collectionName, docId)
await updateDoc(docRef, {
...data,
updated_at: serverTimestamp()
})
}
Delete Documentβ
import { doc, deleteDoc } from 'firebase/firestore'
import { db } from '@/lib/firebase-service'
export async function deleteDocument(
collectionName: string,
docId: string
): Promise<void> {
const docRef = doc(db, collectionName, docId)
await deleteDoc(docRef)
}
Query Operationsβ
Basic Queryβ
import { collection, query, where, orderBy, limit, getDocs } from 'firebase/firestore'
import { db } from '@/lib/firebase-service'
export async function queryDocuments<T>(
collectionName: string,
filters: QueryFilter[],
sorting?: { field: string; direction: 'asc' | 'desc' },
limitCount?: number
): Promise<T[]> {
let q = collection(db, collectionName)
// Apply filters
filters.forEach(filter => {
q = query(q, where(filter.field, filter.operator, filter.value))
})
// Apply sorting
if (sorting) {
q = query(q, orderBy(sorting.field, sorting.direction))
}
// Apply limit
if (limitCount) {
q = query(q, limit(limitCount))
}
const querySnapshot = await getDocs(q)
return querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
} as T))
}
Paginated Queryβ
import {
collection, query, where, orderBy, limit,
startAfter, getDocs, QueryDocumentSnapshot
} from 'firebase/firestore'
import { db } from '@/lib/firebase-service'
export async function queryDocumentsPaginated<T>(
collectionName: string,
pageSize: number = 10,
lastDoc?: QueryDocumentSnapshot,
filters?: QueryFilter[]
): Promise<{ documents: T[]; lastDoc?: QueryDocumentSnapshot; hasMore: boolean }> {
let q = query(collection(db, collectionName))
// Apply filters
if (filters) {
filters.forEach(filter => {
q = query(q, where(filter.field, filter.operator, filter.value))
})
}
// Apply ordering and pagination
q = query(q, orderBy('created_at', 'desc'), limit(pageSize + 1))
if (lastDoc) {
q = query(q, startAfter(lastDoc))
}
const querySnapshot = await getDocs(q)
const docs = querySnapshot.docs
const hasMore = docs.length > pageSize
if (hasMore) {
docs.pop() // Remove the extra document
}
return {
documents: docs.map(doc => ({ id: doc.id, ...doc.data() } as T)),
lastDoc: docs[docs.length - 1],
hasMore
}
}
Real-time Subscriptionsβ
Document Subscriptionβ
import { doc, onSnapshot, Unsubscribe } from 'firebase/firestore'
import { db } from '@/lib/firebase-service'
export function subscribeToDocument<T>(
collectionName: string,
docId: string,
callback: (data: T | null) => void,
errorCallback?: (error: Error) => void
): Unsubscribe {
const docRef = doc(db, collectionName, docId)
return onSnapshot(
docRef,
(doc) => {
if (doc.exists()) {
callback({ id: doc.id, ...doc.data() } as T)
} else {
callback(null)
}
},
errorCallback
)
}
Collection Subscriptionβ
import { collection, query, onSnapshot, Unsubscribe } from 'firebase/firestore'
import { db } from '@/lib/firebase-service'
export function subscribeToCollection<T>(
collectionName: string,
queryConstraints: any[] = [],
callback: (data: T[]) => void,
errorCallback?: (error: Error) => void
): Unsubscribe {
const q = query(collection(db, collectionName), ...queryConstraints)
return onSnapshot(
q,
(querySnapshot) => {
const documents = querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
} as T))
callback(documents)
},
errorCallback
)
}
File Upload Serviceβ
Client-Side Uploadβ
import { ref, uploadBytes, getDownloadURL, uploadBytesResumable } from 'firebase/storage'
import { storage } from '@/lib/firebase-service'
export async function uploadFile(
file: File,
path: string,
onProgress?: (progress: number) => void
): Promise<string> {
const storageRef = ref(storage, path)
if (onProgress) {
// Upload with progress tracking
const uploadTask = uploadBytesResumable(storageRef, file)
return new Promise((resolve, reject) => {
uploadTask.on(
'state_changed',
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100
onProgress(progress)
},
reject,
async () => {
const downloadURL = await getDownloadURL(uploadTask.snapshot.ref)
resolve(downloadURL)
}
)
})
} else {
// Simple upload
await uploadBytes(storageRef, file)
return getDownloadURL(storageRef)
}
}
Server-Side Uploadβ
import { getAdminStorage } from '@/lib/firebase-admin.server'
export async function uploadFileServer(
fileBuffer: Buffer,
fileName: string,
contentType: string,
folder: string = 'uploads'
): Promise<string> {
const bucket = getAdminStorage().bucket()
const file = bucket.file(`${folder}/${fileName}`)
await file.save(fileBuffer, {
metadata: {
contentType,
metadata: {
uploaded_at: new Date().toISOString()
}
}
})
// Make file publicly accessible
await file.makePublic()
return `https://storage.googleapis.com/${bucket.name}/${file.name}`
}
Authentication Integrationβ
NextAuth.js Integrationβ
import { getAdminAuth } from '@/lib/firebase-admin.server'
import { NextAuthOptions } from 'next-auth'
export async function verifyFirebaseToken(token: string) {
try {
const auth = getAdminAuth()
const decodedToken = await auth.verifyIdToken(token)
return decodedToken
} catch (error) {
console.error('Firebase token verification failed:', error)
throw new Error('Invalid token')
}
}
export async function createFirebaseUser(userData: {
email: string
displayName?: string
photoURL?: string
}) {
try {
const auth = getAdminAuth()
const userRecord = await auth.createUser(userData)
return userRecord
} catch (error) {
console.error('Firebase user creation failed:', error)
throw error
}
}
Security Rulesβ
Firestore Security Rulesβ
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can read/write their own data
match /users/{userId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Public entities are readable by all, writable by authenticated users
match /entities/{entityId} {
allow read: if resource.data.visibility == 'public' || request.auth != null;
allow write: if request.auth != null &&
(request.auth.uid == resource.data.created_by || hasAdminRole());
}
// Opportunities have role-based access
match /opportunities/{opportunityId} {
allow read: if canReadOpportunity();
allow write: if request.auth != null &&
(request.auth.uid == resource.data.created_by || hasAdminRole());
}
// Helper functions
function hasAdminRole() {
return request.auth.token.role == 'ADMIN';
}
function canReadOpportunity() {
return resource.data.visibility == 'public' ||
(request.auth != null && (
resource.data.visibility == 'members' ||
request.auth.token.role in ['CONFIDENTIAL', 'ADMIN']
));
}
}
}
Storage Security Rulesβ
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// Users can upload to their own folders
match /users/{userId}/{allPaths=**} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
// Public uploads (logos, etc.)
match /public/{allPaths=**} {
allow read: if true;
allow write: if request.auth != null;
}
// Entity uploads
match /entities/{entityId}/{allPaths=**} {
allow read: if true;
allow write: if request.auth != null && canManageEntity(entityId);
}
function canManageEntity(entityId) {
return request.auth.token.role == 'ADMIN' ||
get(/databases/$(database)/documents/entities/$(entityId)).data.created_by == request.auth.uid;
}
}
}
Error Handlingβ
Firebase Error Handlerβ
import { FirebaseError } from 'firebase/app'
export interface FirebaseErrorInfo {
code: string
message: string
userFriendlyMessage: string
}
export function handleFirebaseError(error: FirebaseError): FirebaseErrorInfo {
const errorMap: Record<string, string> = {
'permission-denied': 'You do not have permission to perform this action',
'not-found': 'The requested resource was not found',
'already-exists': 'This resource already exists',
'resource-exhausted': 'Too many requests. Please try again later',
'unauthenticated': 'You must be signed in to perform this action',
'unavailable': 'Service is temporarily unavailable'
}
return {
code: error.code,
message: error.message,
userFriendlyMessage: errorMap[error.code] || 'An unexpected error occurred'
}
}
Performance Optimizationβ
Caching Strategyβ
import { enableNetwork, disableNetwork } from 'firebase/firestore'
// Enable offline persistence
export async function enableOfflineSupport() {
try {
await enableNetwork(db)
} catch (error) {
console.error('Failed to enable offline support:', error)
}
}
// Disable network for testing
export async function goOffline() {
await disableNetwork(db)
}
Batch Operationsβ
import { writeBatch, doc } from 'firebase/firestore'
import { db } from '@/lib/firebase-service'
export async function batchWrite(operations: BatchOperation[]) {
const batch = writeBatch(db)
operations.forEach(operation => {
const docRef = doc(db, operation.collection, operation.id)
switch (operation.type) {
case 'set':
batch.set(docRef, operation.data)
break
case 'update':
batch.update(docRef, operation.data)
break
case 'delete':
batch.delete(docRef)
break
}
})
await batch.commit()
}
Monitoring & Analyticsβ
Performance Monitoringβ
import { getPerformance, trace } from 'firebase/performance'
import { initializeApp } from 'firebase/app'
const app = initializeApp(firebaseConfig)
const perf = getPerformance(app)
export function measureOperation<T>(
operationName: string,
operation: () => Promise<T>
): Promise<T> {
const traceInstance = trace(perf, operationName)
traceInstance.start()
return operation()
.then(result => {
traceInstance.stop()
return result
})
.catch(error => {
traceInstance.stop()
throw error
})
}
The Firebase Service Library provides a comprehensive, type-safe, and performant integration with Firebase services, enabling all the real-time and cloud functionality of the Ring platform.