@prmichaelsen/firebase-client-v8
v1.1.2
Published
Firebase client library optimized for Cloudflare Workers and edge runtimes
Maintainers
Readme
Firebase Client v8 - Cloudflare Workers Compatible
A Firebase client library optimized for Cloudflare Workers and edge runtimes. Uses REST-based Firestore (long polling) instead of WebSocket connections, making it compatible with serverless environments.
Features
- ✅ Cloudflare Workers Compatible - REST-based, no WebSocket connections
- ✅ Firebase Authentication - Email/password, OAuth providers (Google, GitHub, Facebook, Twitter)
- ✅ Firestore Client - Full CRUD operations with merge options, batch writes, and transactions
- ✅ Firebase Storage - File upload/download with progress tracking
- ✅ Auth State Management - Real-time auth state changes
- ✅ Polling-based Listeners - Document and collection watchers for edge environments
- ✅ Unlimited Cache - Optimized for edge environments
- ✅ TypeScript Support - Full type definitions included
Installation
npm install @prmichaelsen/firebase-client-v8Initialization
The library supports two initialization patterns:
Option 1: Explicit Initialization (Recommended)
import { initializeFirebase } from '@prmichaelsen/firebase-client-v8';
// Initialize with explicit configuration
initializeFirebase({
apiKey: import.meta.env.FIREBASE_API_KEY,
authDomain: import.meta.env.FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.FIREBASE_APP_ID,
});Benefits:
- ✅ Explicit and clear
- ✅ Works with any bundler (Vite, Webpack, etc.)
- ✅ Easy to test and mock
- ✅ No magic environment variable reading
Option 2: Auto-initialization from Environment Variables
# Set these environment variables
FIREBASE_API_KEY=AIzaSy...
FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
FIREBASE_PROJECT_ID=your-project-id
FIREBASE_STORAGE_BUCKET=your-project.appspot.com
FIREBASE_MESSAGING_SENDER_ID=123456789
FIREBASE_APP_ID=1:123456789:web:abc123// No initialization needed - auto-loads from process.env on first use
import { signIn } from '@prmichaelsen/firebase-client-v8';
const userCredential = await signIn('[email protected]', 'password123');Note: Auto-initialization uses process.env, which works in Node.js and some bundlers but may not work in all environments (e.g., Vite requires import.meta.env).
Quick Start
import { initializeFirebase, signIn, getDocument, uploadFile } from '@prmichaelsen/firebase-client-v8';
// Initialize (recommended)
initializeFirebase({
apiKey: import.meta.env.FIREBASE_API_KEY,
authDomain: import.meta.env.FIREBASE_AUTH_DOMAIN,
projectId: import.meta.env.FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.FIREBASE_APP_ID,
});
// Sign in
const userCredential = await signIn('[email protected]', 'password123');
console.log('Signed in:', userCredential.user.uid);
// Get a document
const user = await getDocument('users', 'user123');
console.log('User:', user);
// Upload a file
const file = new File(['content'], 'example.txt');
const downloadUrl = await uploadFile('uploads/example.txt', file);
console.log('File uploaded:', downloadUrl);Authentication
Email/Password Authentication
import { signIn, signUp, logout, resetPassword } from '@prmichaelsen/firebase-client-v8';
// Sign in
const userCredential = await signIn('[email protected]', 'password123');
// Sign up
const newUser = await signUp('[email protected]', 'password123');
// Sign out
await logout();
// Reset password
await resetPassword('[email protected]');OAuth Providers
import {
signInWithGoogle,
signInWithGithub,
signInWithFacebook,
signInWithTwitter
} from '@prmichaelsen/firebase-client-v8';
// Sign in with Google
const userCredential = await signInWithGoogle();
// Sign in with GitHub
const userCredential = await signInWithGithub();
// Sign in with Facebook
const userCredential = await signInWithFacebook();
// Sign in with Twitter
const userCredential = await signInWithTwitter();Custom Provider
import { signInWithProvider, GoogleAuthProvider } from '@prmichaelsen/firebase-client-v8';
const provider = new GoogleAuthProvider();
provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
const userCredential = await signInWithProvider(provider);Auth State Listener
import { onAuthChange } from '@prmichaelsen/firebase-client-v8';
const unsubscribe = onAuthChange((user) => {
if (user) {
console.log('User signed in:', user.email);
} else {
console.log('User signed out');
}
});
// Later, stop listening
unsubscribe();User Management
import {
getCurrentUser,
getIdToken,
refreshIdToken,
updateUserProfile,
updateUserEmail,
updateUserPassword,
sendVerificationEmail,
deleteUserAccount
} from '@prmichaelsen/firebase-client-v8';
// Get current user
const user = await getCurrentUser();
// Get ID token
const idToken = await getIdToken();
// Refresh token
const freshToken = await refreshIdToken();
// Update profile
await updateUserProfile({
displayName: 'John Doe',
photoURL: 'https://example.com/photo.jpg'
});
// Update email
await updateUserEmail('[email protected]');
// Update password
await updateUserPassword('newPassword123');
// Send verification email
await sendVerificationEmail();
// Delete account
await deleteUserAccount();Firestore
Basic Operations
import {
getDocument,
addDocument,
setDocument,
updateDocument,
deleteDocument
} from '@prmichaelsen/firebase-client-v8';
// Get document
const user = await getDocument('users', 'user123');
// Add document (auto-generated ID)
const docId = await addDocument('users', {
email: '[email protected]',
name: 'John Doe',
createdAt: Date.now(),
});
// Set document (create or overwrite)
await setDocument('users', 'user123', {
email: '[email protected]',
name: 'John Doe',
});
// Update document
await updateDocument('users', 'user123', {
name: 'Jane Doe',
});
// Delete document
await deleteDocument('users', 'user123');Merge Options
import { setDocument } from '@prmichaelsen/firebase-client-v8';
// Merge with existing document
await setDocument('users', 'user123', {
name: 'Jane Doe',
}, { merge: true });
// Merge specific fields only
await setDocument('users', 'user123', {
name: 'Jane Doe',
age: 30,
}, { mergeFields: ['name'] });Querying Documents
import { getDocuments, queryDocuments } from '@prmichaelsen/firebase-client-v8';
import { where, orderBy, limit } from 'firebase/firestore';
// Get all documents
const allUsers = await getDocuments('users');
// Query with constraints
const activeUsers = await getDocuments(
'users',
where('active', '==', true),
orderBy('createdAt', 'desc'),
limit(10)
);
// Query with helper function
const adults = await queryDocuments(
'users',
[
{ field: 'active', operator: '==', value: true },
{ field: 'age', operator: '>=', value: 18 }
],
'createdAt',
10
);Pagination
import { getDocumentsWithPagination } from '@prmichaelsen/firebase-client-v8';
import { orderBy } from 'firebase/firestore';
// First page
const page1 = await getDocumentsWithPagination(
'users',
10,
undefined,
orderBy('createdAt', 'desc')
);
// Next page
const lastDoc = page1[page1.length - 1];
const page2 = await getDocumentsWithPagination(
'users',
10,
lastDoc,
orderBy('createdAt', 'desc')
);Batch Operations
import { createBatch, commitBatch } from '@prmichaelsen/firebase-client-v8';
import { doc, increment } from 'firebase/firestore';
import { firestore } from '@prmichaelsen/firebase-client-v8';
const batch = createBatch();
const userRef = doc(firestore, 'users', 'user123');
batch.set(userRef, { name: 'John Doe' });
const postRef = doc(firestore, 'posts', 'post456');
batch.update(postRef, { likes: increment(1) });
const commentRef = doc(firestore, 'comments', 'comment789');
batch.delete(commentRef);
await commitBatch(batch);Transactions
import { runFirestoreTransaction } from '@prmichaelsen/firebase-client-v8';
import { doc } from 'firebase/firestore';
import { firestore } from '@prmichaelsen/firebase-client-v8';
const result = await runFirestoreTransaction(async (transaction) => {
const userRef = doc(firestore, 'users', 'user123');
const userDoc = await transaction.get(userRef);
if (!userDoc.exists()) {
throw new Error('User does not exist');
}
const newBalance = userDoc.data().balance + 100;
transaction.update(userRef, { balance: newBalance });
return newBalance;
});Field Values
import { updateDocument } from '@prmichaelsen/firebase-client-v8';
import {
serverTimestamp,
increment,
arrayUnion,
arrayRemove,
deleteField
} from 'firebase/firestore';
await updateDocument('users', 'user123', {
// Set server timestamp
updatedAt: serverTimestamp(),
// Increment a number
loginCount: increment(1),
// Add to array
tags: arrayUnion('premium', 'verified'),
// Remove from array
oldTags: arrayRemove('trial'),
// Delete a field
tempData: deleteField(),
});Utility Functions
import { documentExists, countDocuments } from '@prmichaelsen/firebase-client-v8';
import { where } from 'firebase/firestore';
// Check if document exists
const exists = await documentExists('users', 'user123');
// Count documents
const totalUsers = await countDocuments('users');
const activeUsers = await countDocuments('users', where('active', '==', true));Polling-based Listeners
Since real-time listeners don't work well in serverless environments, use polling-based alternatives:
Document Listener
import { onDocumentChange } from '@prmichaelsen/firebase-client-v8';
const unsubscribe = onDocumentChange(
'users',
'user123',
(doc) => {
if (doc) {
console.log('User updated:', doc.name);
} else {
console.log('User deleted');
}
},
{ interval: 3000, immediate: true }
);
// Stop listening
unsubscribe();Collection Listener
import { onCollectionChange } from '@prmichaelsen/firebase-client-v8';
import { where, orderBy } from 'firebase/firestore';
const unsubscribe = onCollectionChange(
'users',
(docs) => {
console.log('Users updated:', docs.length);
},
{ interval: 5000 },
where('active', '==', true),
orderBy('createdAt', 'desc')
);
// Stop listening
unsubscribe();Document Watcher
import { createDocumentWatcher } from '@prmichaelsen/firebase-client-v8';
const userWatcher = createDocumentWatcher('users', 'user123', {
interval: 3000
});
// Subscribe to changes
const unsubscribe = userWatcher.subscribe((doc) => {
console.log('User changed:', doc);
});
// Get current value
const currentUser = userWatcher.getValue();
// Stop watching
unsubscribe();
// Or destroy completely
userWatcher.destroy();Collection Watcher
import { createCollectionWatcher } from '@prmichaelsen/firebase-client-v8';
import { where } from 'firebase/firestore';
const usersWatcher = createCollectionWatcher(
'users',
{ interval: 5000 },
where('active', '==', true)
);
const unsubscribe = usersWatcher.subscribe((docs) => {
console.log('Active users:', docs.length);
});
// Get current value
const currentUsers = usersWatcher.getValue();
// Stop watching
unsubscribe();Storage
Upload Files
import { uploadFile, uploadFileWithMetadata } from '@prmichaelsen/firebase-client-v8';
// Simple upload
const file = new File(['content'], 'example.txt');
const downloadUrl = await uploadFile('uploads/example.txt', file);
// Upload with metadata
const url = await uploadFileWithMetadata('uploads/example.txt', file, {
contentType: 'text/plain',
customMetadata: { userId: 'user123' }
});Upload with Progress
import { uploadFileWithProgress } from '@prmichaelsen/firebase-client-v8';
const file = new File(['content'], 'example.txt');
const url = await uploadFileWithProgress(
'uploads/example.txt',
file,
(progress) => {
console.log(`Upload: ${progress.percentage}% (${progress.bytesTransferred}/${progress.totalBytes})`);
}
);Resumable Upload
import { createUploadTask } from '@prmichaelsen/firebase-client-v8';
const file = new File(['content'], 'example.txt');
const uploadTask = createUploadTask('uploads/example.txt', file);
uploadTask.on('state_changed',
(snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log('Upload is ' + progress + '% done');
},
(error) => console.error('Upload failed:', error),
async () => {
const url = await getDownloadURL(uploadTask.snapshot.ref);
console.log('File available at', url);
}
);
// Control upload
uploadTask.pause();
uploadTask.resume();
uploadTask.cancel();File Operations
import {
getFileUrl,
deleteFile,
listFiles,
listAllItems,
getFileMetadata,
updateFileMetadata
} from '@prmichaelsen/firebase-client-v8';
// Get download URL
const url = await getFileUrl('uploads/example.txt');
// Delete file
await deleteFile('uploads/example.txt');
// List files
const files = await listFiles('uploads/');
// List files and directories
const { files, directories } = await listAllItems('uploads/');
// Get metadata
const metadata = await getFileMetadata('uploads/example.txt');
console.log('File size:', metadata.size);
// Update metadata
await updateFileMetadata('uploads/example.txt', {
contentType: 'text/plain',
customMetadata: { userId: 'user123' }
});React Hook Example
import { useState, useEffect } from 'react';
import { onAuthChange, type User } from '@prmichaelsen/firebase-client-v8';
export function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const unsubscribe = onAuthChange((user) => {
setUser(user);
setLoading(false);
});
return unsubscribe;
}, []);
return { user, loading };
}Cloudflare Workers Compatibility
Key Configuration
The library is configured for Cloudflare Workers compatibility:
initializeFirestore(app, {
experimentalForceLongPolling: true, // ✅ Use REST instead of WebSocket
experimentalAutoDetectLongPolling: false, // ✅ Don't auto-detect
cacheSizeBytes: CACHE_SIZE_UNLIMITED, // ✅ Unlimited cache
ignoreUndefinedProperties: true, // ✅ Ignore undefined
});Why These Settings?
- Long Polling - Cloudflare Workers don't support persistent WebSocket connections
- REST-based - All Firestore operations use REST API instead of gRPC
- Unlimited Cache - Edge environments benefit from aggressive caching
- No Auto-detect - Prevents fallback attempts that cause errors
API Reference
Authentication
signIn(email, password)- Sign in with email/passwordsignUp(email, password)- Create new user accountsignInWithGoogle()- Sign in with GooglesignInWithGithub()- Sign in with GitHubsignInWithFacebook()- Sign in with FacebooksignInWithTwitter()- Sign in with TwittersignInWithProvider(provider)- Sign in with custom providersignInWithToken(token)- Sign in with custom tokenlogout()- Sign out current userresetPassword(email)- Send password reset emailonAuthChange(callback)- Listen to auth state changesgetCurrentUser()- Get current authenticated usergetIdToken()- Get ID token for current userrefreshIdToken()- Refresh ID tokenupdateUserProfile(profile)- Update user profileupdateUserEmail(email)- Update user emailupdateUserPassword(password)- Update user passwordsendVerificationEmail()- Send email verificationdeleteUserAccount()- Delete user account
Firestore
getDocument(collectionPath, documentId)- Get a document by IDgetDocuments(collectionPath, ...constraints)- Get documents with queryaddDocument(collectionPath, data)- Add new documentsetDocument(collectionPath, documentId, data, options?)- Set document with merge optionsupdateDocument(collectionPath, documentId, data)- Update documentdeleteDocument(collectionPath, documentId)- Delete documentqueryDocuments(collectionPath, filters, orderBy?, limit?)- Query documentsgetDocumentsWithPagination(collectionPath, pageSize, lastDoc?, ...constraints)- Get paginated documentscountDocuments(collectionPath, ...constraints)- Count documentsdocumentExists(collectionPath, documentId)- Check if document existscreateBatch()- Create batch for multiple operationscommitBatch(batch)- Commit batch operationsrunFirestoreTransaction(updateFunction)- Run transaction
Storage
uploadFile(path, file)- Upload fileuploadFileWithMetadata(path, file, metadata)- Upload file with metadatauploadFileWithProgress(path, file, onProgress?, metadata?)- Upload with progress trackingcreateUploadTask(path, file, metadata?)- Create resumable upload taskgetFileUrl(path)- Get download URLdeleteFile(path)- Delete filelistFiles(path)- List files in directorylistAllItems(path)- List files and subdirectoriesgetFileMetadata(path)- Get file metadataupdateFileMetadata(path, metadata)- Update file metadatagetStorageRef(path)- Get storage reference
Listeners (Polling-based)
onDocumentChange(collectionPath, documentId, callback, options?)- Listen to document changesonCollectionChange(collectionPath, callback, options?, ...constraints)- Listen to collection changescreateDocumentWatcher(collectionPath, documentId, options?)- Create document watchercreateCollectionWatcher(collectionPath, options?, ...constraints)- Create collection watcher
TypeScript Support
Full TypeScript support with type definitions:
import type {
User,
FirestoreDocument,
FirestoreFilter,
AuthStateCallback,
DocumentListenerCallback,
CollectionListenerCallback,
UploadProgressCallback
} from '@prmichaelsen/firebase-client-v8';Key Differences from Standard Firebase
| Feature | Standard Firebase | This Library | |---------|------------------|--------------| | Firestore | WebSocket + gRPC | REST + Long Polling | | Real-time Listeners | WebSocket-based | Polling-based | | Environment | Browser, Node.js | Browser, Workers, Edge | | Connections | Persistent | Stateless | | Cache | Limited | Unlimited | | Cold Starts | Slower | Faster | | Batch Operations | ✅ | ✅ | | Transactions | ✅ | ✅ | | OAuth Providers | ✅ | ✅ | | Storage Progress | ✅ | ✅ |
Benefits
- Edge Compatible - Runs in Cloudflare Workers, Deno, Bun
- No WebSocket - REST-based, works in serverless
- Fast Cold Starts - No persistent connections to establish
- Aggressive Caching - Unlimited cache for better performance
- Standard Firebase API - Same API as regular Firebase SDK
- Full Feature Set - Batch operations, transactions, OAuth, progress tracking
Limitations
- No real-time WebSocket listeners (use polling instead)
- No offline persistence (cache only)
- Slightly higher latency for polling-based listeners
- Polling consumes more resources than WebSocket listeners
License
MIT
Related
- firebase-admin-sdk-v8 - Server-side Firebase library
- Firebase Documentation
