Skip to main content

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.