@prmichaelsen/firebase-admin-sdk-v8
v2.9.0
Published
Firebase Admin SDK for Cloudflare Workers and edge runtimes using REST APIs
Maintainers
Readme
Firebase Admin SDK v8
Firebase Admin SDK for Cloudflare Workers and edge runtimes using REST APIs
This library provides Firebase Admin SDK functionality for Cloudflare Workers and other edge runtimes. It uses REST APIs and JWT token generation instead of the Node.js Admin SDK, making it compatible with environments that don't support Node.js.
✨ Features
- ✅ Zero Dependencies - No external dependencies, pure Web APIs (crypto.subtle, fetch)
- ✅ JWT Token Generation - Service account authentication
- ✅ ID Token Verification - Verify Firebase ID tokens (supports v9 and v10 formats)
- ✅ Session Cookies - Create and verify long-lived session cookies (up to 14 days)
- ✅ User Management - Create, read, update, delete users via Admin API
- ✅ Firebase v10 Compatible - Supports both old and new token issuer formats
- ✅ Firestore REST API - Full CRUD operations via REST
- ✅ Field Value Operations - increment, arrayUnion, arrayRemove, serverTimestamp, delete
- ✅ Advanced Queries - where, orderBy, limit, offset, cursor pagination
- ✅ Batch Operations - Atomic multi-document writes
- ✅ Merge Operations - Merge documents with existing data
- ✅ OAuth Access Tokens - Generate admin API access tokens
- ✅ Token Caching - Automatic token refresh before expiry
- ✅ TypeScript - Full type definitions included
📦 Installation
npm install @prmichaelsen/firebase-admin-sdk-v8🚀 Quick Start
1. Initialize the SDK
Option A: Cloudflare Workers / Edge Runtimes (Recommended)
import { initializeApp, verifyIdToken } from '@prmichaelsen/firebase-admin-sdk-v8';
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// Initialize with env variables
initializeApp({
serviceAccount: env.FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY,
projectId: env.FIREBASE_PROJECT_ID
});
// Now use the SDK
const token = request.headers.get('authorization')?.split('Bearer ')[1];
const user = await verifyIdToken(token);
return new Response(JSON.stringify({ user }));
}
};Option B: Node.js / Traditional Environments
import { initializeApp } from '@prmichaelsen/firebase-admin-sdk-v8';
// Option 1: Explicit initialization
initializeApp({
serviceAccount: JSON.parse(process.env.FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY!),
projectId: process.env.FIREBASE_PROJECT_ID
});
// Option 2: Auto-detect from process.env (no initialization needed)
// The SDK will automatically use process.env if initializeApp() is not calledEnvironment Variables (if not using initializeApp):
FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY='{"type":"service_account",...}'
FIREBASE_PROJECT_ID=your-project-id2. Verify ID Tokens
import { verifyIdToken, getUserFromToken } from '@prmichaelsen/firebase-admin-sdk-v8';
const authHeader = request.headers.get('authorization');
const idToken = authHeader?.split('Bearer ')[1];
try {
const user = await getUserFromToken(idToken);
console.log('User:', user.email, user.displayName);
} catch (error) {
return new Response('Invalid token', { status: 401 });
}3. Session Cookies (Long-Lived Sessions)
import { createSessionCookie, verifySessionCookie } from '@prmichaelsen/firebase-admin-sdk-v8';
// Create 14-day session cookie from ID token
const sessionCookie = await createSessionCookie(idToken, {
expiresIn: 60 * 60 * 24 * 14 * 1000
});
// Set as HTTP-only cookie
response.headers.set('Set-Cookie',
`session=${sessionCookie}; Max-Age=1209600; HttpOnly; Secure; SameSite=Strict`
);
// Verify session cookie
const user = await verifySessionCookie(cookie);4. Basic Firestore Operations
import { setDocument, getDocument, updateDocument, FieldValue } from '@prmichaelsen/firebase-admin-sdk-v8';
// Set a document (create or overwrite)
await setDocument('users', 'user123', {
name: 'John Doe',
email: '[email protected]',
createdAt: FieldValue.serverTimestamp(),
});
// Get a document
const user = await getDocument('users', 'user123');
// Update with field values
await updateDocument('users', 'user123', {
loginCount: FieldValue.increment(1),
lastLogin: FieldValue.serverTimestamp(),
});4. Advanced Queries
import { queryDocuments } from '@prmichaelsen/firebase-admin-sdk-v8';
const activeUsers = await queryDocuments('users', {
where: [
{ field: 'active', op: '==', value: true },
{ field: 'age', op: '>=', value: 18 }
],
orderBy: [{ field: 'createdAt', direction: 'DESCENDING' }],
limit: 10
});📚 API Reference
Authentication
verifyIdToken(idToken: string): Promise<DecodedIdToken>
Verify a Firebase ID token and return the decoded token.
const decoded = await verifyIdToken(idToken);
console.log('User ID:', decoded.uid);getUserFromToken(idToken: string): Promise<UserInfo>
Get user information from a verified ID token.
const user = await getUserFromToken(idToken);
// Returns: { uid, email, emailVerified, displayName, photoURL }User Management
getUserByEmail(email: string): Promise<UserRecord | null>
Look up a Firebase user by email address.
const user = await getUserByEmail('[email protected]');
if (user) {
console.log('User ID:', user.uid);
console.log('Email verified:', user.emailVerified);
}getUserByUid(uid: string): Promise<UserRecord | null>
Look up a Firebase user by UID.
const user = await getUserByUid('user123');
if (user) {
console.log('Email:', user.email);
console.log('Display name:', user.displayName);
}createUser(properties: CreateUserRequest): Promise<UserRecord>
Create a new Firebase user.
const newUser = await createUser({
email: '[email protected]',
password: 'securePassword123',
displayName: 'New User',
emailVerified: false,
});
console.log('Created user:', newUser.uid);updateUser(uid: string, properties: UpdateUserRequest): Promise<UserRecord>
Update an existing Firebase user.
const updatedUser = await updateUser('user123', {
displayName: 'Updated Name',
photoURL: 'https://example.com/photo.jpg',
emailVerified: true,
});deleteUser(uid: string): Promise<void>
Delete a Firebase user.
await deleteUser('user123');listUsers(maxResults?: number, pageToken?: string): Promise<ListUsersResult>
List all users with pagination.
// List first 100 users
const result = await listUsers(100);
console.log('Users:', result.users.length);
// Get next page
if (result.pageToken) {
const nextPage = await listUsers(100, result.pageToken);
}setCustomUserClaims(uid: string, customClaims: Record<string, any> | null): Promise<void>
Set custom claims on a user's ID token for role-based access control.
// Set custom claims
await setCustomUserClaims('user123', {
role: 'admin',
premium: true,
permissions: ['read', 'write', 'delete'],
});
// Clear custom claims
await setCustomUserClaims('user123', null);Firestore - Basic Operations
setDocument(collectionPath, documentId, data, options?): Promise<void>
Create or overwrite a document. Supports merge options.
// Overwrite
await setDocument('users', 'user123', { name: 'John', age: 30 });
// Merge with existing
await setDocument('users', 'user123', { age: 31 }, { merge: true });
// Merge specific fields
await setDocument('users', 'user123', { age: 31, city: 'NYC' }, {
mergeFields: ['age']
});addDocument(collectionPath, data, documentId?): Promise<DocumentReference>
Add a document with auto-generated or custom ID. Returns a DocumentReference with id and path properties.
const docRef = await addDocument('posts', { title: 'Hello' });
console.log('Created:', docRef.id); // Auto-generated ID
const customDocRef = await addDocument('posts', { title: 'Hi' }, 'custom-id');
console.log('Created:', customDocRef.id); // 'custom-id'getDocument(collectionPath, documentId): Promise<DataObject | null>
Get a document by ID.
const user = await getDocument('users', 'user123');updateDocument(collectionPath, documentId, data): Promise<void>
Update specific fields in a document.
await updateDocument('users', 'user123', { lastLogin: new Date() });deleteDocument(collectionPath, documentId): Promise<void>
Delete a document.
await deleteDocument('users', 'user123');Firestore - Field Values
FieldValue.serverTimestamp()
Set field to server timestamp.
await setDocument('posts', 'post1', {
createdAt: FieldValue.serverTimestamp()
});FieldValue.increment(n)
Increment a numeric field.
await updateDocument('users', 'user1', {
loginCount: FieldValue.increment(1),
points: FieldValue.increment(10)
});FieldValue.arrayUnion(...elements)
Add elements to an array (no duplicates).
await updateDocument('posts', 'post1', {
tags: FieldValue.arrayUnion('javascript', 'typescript')
});FieldValue.arrayRemove(...elements)
Remove elements from an array.
await updateDocument('posts', 'post1', {
tags: FieldValue.arrayRemove('outdated')
});FieldValue.delete()
Delete a field from a document.
await updateDocument('users', 'user1', {
temporaryField: FieldValue.delete()
});Firestore - Queries
queryDocuments(collectionPath, options?): Promise<Array<{ id, data }>>
Query documents with advanced filtering.
Query Options:
where: Array of filters{ field, op, value }orderBy: Array of orders{ field, direction }limit: Maximum number of resultsoffset: Number of results to skipstartAt,startAfter,endAt,endBefore: Cursor pagination
Where Operators:
==,!=,<,<=,>,>=array-contains,array-contains-anyin,not-in
// Simple query
const users = await queryDocuments('users');
// With filters
const activeAdults = await queryDocuments('users', {
where: [
{ field: 'active', op: '==', value: true },
{ field: 'age', op: '>=', value: 18 }
]
});
// With ordering and limit
const topUsers = await queryDocuments('users', {
where: [{ field: 'active', op: '==', value: true }],
orderBy: [{ field: 'points', direction: 'DESCENDING' }],
limit: 10
});
// Cursor pagination
const results = await queryDocuments('users', {
orderBy: [{ field: 'createdAt', direction: 'ASCENDING' }],
startAfter: [lastCreatedAt],
limit: 20
});Firestore - Batch Operations
batchWrite(operations): Promise<BatchWriteResult>
Perform multiple write operations atomically.
await batchWrite([
{
type: 'set',
collectionPath: 'users',
documentId: 'user1',
data: { name: 'John' }
},
{
type: 'update',
collectionPath: 'users',
documentId: 'user2',
data: { lastLogin: FieldValue.serverTimestamp() }
},
{
type: 'delete',
collectionPath: 'users',
documentId: 'user3'
}
]);Token Generation
getAdminAccessToken(): Promise<string>
Get an OAuth access token for Firebase Admin API. Automatically cached and refreshed.
const token = await getAdminAccessToken();clearTokenCache(): void
Clear the cached access token.
clearTokenCache();Storage - Resumable Uploads
uploadFileResumable(bucket, path, data, contentType, options?): Promise<FileMetadata>
Upload large files with resumable upload support. Suitable for files >10MB, unreliable networks, or when progress tracking is needed.
Features:
- ✅ Chunked uploads (configurable chunk size)
- ✅ Progress tracking with callbacks
- ✅ Resume interrupted uploads
- ✅ Memory efficient (doesn't load entire file at once)
- ✅ Automatic retry on chunk failure
import { uploadFileResumable } from '@prmichaelsen/firebase-admin-sdk-v8';
// Upload large file with progress tracking
const data = await fetch('https://example.com/large-video.mp4');
const buffer = await data.arrayBuffer();
const metadata = await uploadFileResumable(
'my-bucket.appspot.com',
'videos/large.mp4',
buffer,
'video/mp4',
{
chunkSize: 512 * 1024, // 512KB chunks (default: 256KB)
onProgress: (uploaded, total) => {
const percent = (uploaded / total * 100).toFixed(2);
console.log(`Upload progress: ${percent}%`);
},
metadata: { userId: '123', category: 'videos' },
}
);
console.log('Upload complete:', metadata);Resume interrupted upload:
let sessionUri: string;
try {
const metadata = await uploadFileResumable(
bucket,
path,
data,
contentType,
{
onProgress: (uploaded, total) => {
// Save session URI for resume
sessionUri = /* get from response */;
},
}
);
} catch (error) {
// Resume from where it left off
const metadata = await uploadFileResumable(
bucket,
path,
data,
contentType,
{
resumeToken: sessionUri, // Resume from previous session
}
);
}When to use:
- Files larger than 10MB
- Unreliable network conditions
- Need progress reporting
- Files that may exceed memory limits
When to use simple uploadFile() instead:
- Small files (<10MB)
- Reliable network
- No progress tracking needed
- Edge runtime with memory constraints
💡 Examples
See EXAMPLES.md for comprehensive examples including:
- Authentication patterns
- Field value operations
- Complex queries
- Batch operations
- Real-world use cases
🔧 Advanced Usage
Cloudflare Workers Example
export default {
async fetch(request: Request, env: Env): Promise<Response> {
process.env.FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY = env.FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY;
process.env.PUBLIC_FIREBASE_PROJECT_ID = env.PUBLIC_FIREBASE_PROJECT_ID;
const authHeader = request.headers.get('authorization');
const idToken = authHeader?.split('Bearer ')[1];
if (!idToken) {
return new Response('Unauthorized', { status: 401 });
}
try {
const user = await getUserFromToken(idToken);
// Track user activity
await updateDocument('users', user.uid, {
lastSeen: FieldValue.serverTimestamp(),
visitCount: FieldValue.increment(1)
});
return new Response(JSON.stringify({ user }), {
headers: { 'Content-Type': 'application/json' },
});
} catch (error) {
return new Response('Error: ' + error.message, { status: 500 });
}
},
};Leaderboard Example
import { queryDocuments, updateDocument, FieldValue } from '@prmichaelsen/firebase-admin-sdk-v8';
async function getTopPlayers(limit = 10) {
return await queryDocuments('players', {
where: [{ field: 'active', op: '==', value: true }],
orderBy: [{ field: 'score', direction: 'DESCENDING' }],
limit
});
}
async function updatePlayerScore(playerId: string, points: number) {
await updateDocument('players', playerId, {
score: FieldValue.increment(points),
lastPlayed: FieldValue.serverTimestamp()
});
}Bulk Operations Example
import { batchWrite, FieldValue } from '@prmichaelsen/firebase-admin-sdk-v8';
async function bulkUpdateUsers(userIds: string[], updates: any) {
const operations = userIds.map(userId => ({
type: 'update' as const,
collectionPath: 'users',
documentId: userId,
data: {
...updates,
updatedAt: FieldValue.serverTimestamp()
}
}));
// Process in chunks of 500 (Firestore batch limit)
for (let i = 0; i < operations.length; i += 500) {
const chunk = operations.slice(i, i + 500);
await batchWrite(chunk);
}
}🆚 Comparison with Node.js Admin SDK
| Feature | Node.js Admin SDK | This Library | |---------|------------------|--------------| | Environment | Node.js only | Cloudflare Workers, Edge, Deno, Bun | | Auth | Admin SDK methods | JWT + REST API | | Firestore | Native SDK | REST API | | Field Values | ✅ Full support | ✅ Full support | | Queries | ✅ Full support | ✅ Full support | | Batch Writes | ✅ Full support | ✅ Full support | | Token Verification | Built-in | firebase-auth-cloudflare-workers | | Dependencies | Heavy (Node.js) | Lightweight (Web APIs) | | Cold Starts | Slower | Faster | | Bundle Size | Large | Small |
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
📄 License
MIT
🔗 Related Projects
- firebase-auth-cloudflare-workers - ID token verification library
- Firebase REST API Documentation
📝 Notes
- This library is designed for server-side use only (admin operations)
- For client-side Firebase, use the official Firebase JS SDK
- Service account credentials should be kept secure and never exposed to clients
- Token caching is automatic and refreshes 1 minute before expiry
- Batch operations support up to 500 operations per batch (Firestore limit)
🐛 Troubleshooting
"FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY not set"
Make sure you've set the environment variable with your service account JSON:
process.env.FIREBASE_ADMIN_SERVICE_ACCOUNT_KEY = JSON.stringify(serviceAccount);"Failed to verify ID token"
Ensure the token is:
- A valid Firebase ID token (not an access token)
- Not expired
- From the correct Firebase project
TypeScript Errors
Make sure you have the required dev dependencies:
npm install -D @types/node typescriptQuery Performance
For better query performance:
- Create composite indexes for multi-field queries
- Use cursor pagination instead of offset for large datasets
- Limit query results to reasonable sizes
📊 Feature Comparison Table
| Feature | Supported | Notes | |---------|-----------|-------| | ID Token Verification | ✅ | Supports v9 and v10 token formats | | Custom Token Creation | ✅ | createCustomToken() | | Custom Token Exchange | ✅ | signInWithCustomToken() | | User Management | ✅ | Create, read, update, delete, list users | | Firestore CRUD | ✅ | Full support | | Firestore Queries | ✅ | where, orderBy, limit, cursors | | Firestore Batch | ✅ | Up to 500 operations | | Firestore Transactions | ❌ | Not yet implemented | | Realtime Listeners | ❌ | See explanation below | | Field Values | ✅ | increment, arrayUnion, serverTimestamp, etc. | | Realtime Database | ❌ | Not planned | | Cloud Storage | ✅ | Upload, download, delete, signed URLs | | Cloud Messaging | ❌ | Not yet implemented |
⚠️ Realtime Listeners Not Supported
This library does not support Firestore realtime listeners (onSnapshot()). Here's why:
Technical Limitation
This library uses the Firestore REST API, which is:
- ✅ Stateless (request/response only)
- ✅ Compatible with edge runtimes (Cloudflare Workers, Vercel Edge)
- ❌ No persistent connections
- ❌ No server-push capabilities
- ❌ No streaming support
Realtime listeners require:
- Persistent connections (WebSocket or gRPC)
- Bidirectional streaming
- Server-push architecture
The Firestore REST API simply doesn't provide these capabilities.
Why Not Implement gRPC?
While Firestore does offer a gRPC API with streaming support, implementing it would require:
Complex Protocol Implementation
- HTTP/2 framing
- gRPC message framing
- Protobuf encoding/decoding
- Authentication flow
- Reconnection logic
- ~100+ hours of development
Runtime Limitations
- Cloudflare Workers doesn't support full gRPC (only gRPC-Web)
- gRPC-Web requires a proxy server
- Can't connect directly to Firestore's gRPC endpoint
- Would only work in Durable Objects, not regular Workers
Maintenance Burden
- Must keep up with Firestore protocol changes
- Complex debugging and error handling
- High ongoing maintenance cost
Alternatives
If you need realtime updates, consider these approaches:
1. Polling (Simple)
// Poll for changes every 5 seconds
setInterval(async () => {
const doc = await getDocument('users', 'user123');
// Handle updates
}, 5000);Pros: Simple, works everywhere Cons: 5-second delay, polling costs
2. Durable Objects + Polling (Better)
// Durable Object polls once, broadcasts to many clients
export class FirestoreSync {
async poll() {
const doc = await getDocument('users', 'user123');
// Broadcast to all connected WebSocket clients
for (const ws of this.sessions) {
ws.send(JSON.stringify(doc));
}
}
}Pros: One poll serves many clients, WebSocket push to clients Cons: Still polling-based, Cloudflare-specific
3. Hybrid Architecture (Best)
// Use firebase-admin-node for realtime in Node.js
import admin from 'firebase-admin';
admin.firestore().collection('users').doc('user123')
.onSnapshot((snapshot) => {
// True realtime updates
console.log('Update:', snapshot.data());
});
// Use this library for CRUD in edge functions
import { getDocument } from '@prmichaelsen/firebase-admin-sdk-v8';
const doc = await getDocument('users', 'user123');Pros: True realtime where needed, edge performance for CRUD Cons: Requires separate Node.js service
4. Firebase Client SDK (Frontend)
// Use Firebase Client SDK in browser/mobile
import { onSnapshot, doc } from 'firebase/firestore';
onSnapshot(doc(db, 'users', 'user123'), (snapshot) => {
console.log('Update:', snapshot.data());
});Pros: True realtime, built-in, well-supported Cons: Client-side only, requires Firebase Auth
Recommendation
- For edge runtimes: Use polling or Durable Objects pattern
- For true realtime: Use
firebase-admin-nodein Node.js - For client apps: Use Firebase Client SDK
- For hybrid: Use this library for CRUD + Node.js for realtime
Related
- firebase-admin-node - Full Admin SDK with realtime support
- Firebase Client SDK - Client-side realtime listeners
🗺️ Roadmap
- [x] Custom token creation ✅ (v2.2.0)
- [x] Custom token exchange ✅ (v2.2.0)
- [x] Cloud Storage operations ✅ (v2.2.0)
- [ ] User management (create, update, delete users)
- [ ] Firestore transactions
- [ ] More comprehensive error handling
- [ ] Rate limiting helpers
- [ ] Retry logic for failed operations
- [ ] Storage unit tests (currently only e2e)
