fire2mongo
v1.0.0
Published
Drop-in TypeScript replacement for firebase/firestore backed by MongoDB
Maintainers
Readme
fire2mongo
Drop-in TypeScript replacement for firebase/firestore backed by MongoDB.
Change one import line — the rest of your code stays identical.
// Before
import { doc, getDoc, query, where } from 'firebase/firestore';
// After
import { doc, getDoc, query, where } from 'fire2mongo';Table of Contents
- Installation
- Setup
- Quick Migration
- API Reference
- Subcollections
- Collection Registry
- Connection Management
- Known Limitations
- Publishing
Installation
npm install fire2mongo mongodb
mongodbis a peer dependency — install it alongside fire2mongo.
Setup
1. Initialize the connection
Call initMongoDB() once at app startup. It is safe to call multiple times — the connection is cached after the first call.
import { initMongoDB } from 'fire2mongo';
await initMongoDB({
uri: process.env.MONGODB_URI!,
dbName: process.env.MONGODB_DB!,
});Next.js App Router — call it in a shared singleton file (e.g. lib/mongodb.ts) imported by your route handlers:
// lib/mongodb.ts
import { initMongoDB } from 'fire2mongo';
let initialized = false;
export async function ensureDb() {
if (initialized) return;
await initMongoDB({ uri: process.env.MONGODB_URI!, dbName: process.env.MONGODB_DB! });
initialized = true;
}2. Register your collections
Every Firebase collection name must be registered before use. Do this once — in the same startup file or a dedicated collections.ts.
import { registerCollections } from 'fire2mongo';
registerCollections({
users: 'users',
orders: 'orders',
products: 'inventory_items', // Firebase name → actual MongoDB collection name
});Or one at a time:
import { registerCollection } from 'fire2mongo';
registerCollection('users');
registerCollection('products', { mongoCollection: 'inventory_items' });If you call getDoc on an unregistered collection, fire2mongo throws a clear error listing every registered name:
[fire2mongo] No collection registered for "items".
Registered: users, orders, products.
Call registerCollection("items") during app startup.Quick Migration
The only thing you need to change in existing Firebase code is the import path (and remove the Firebase db import).
// ── BEFORE ──────────────────────────────────────────────────────────────────
import {
doc, collection, getDoc, getDocs, setDoc, addDoc, updateDoc, deleteDoc,
query, where, orderBy, limit,
Timestamp, arrayUnion, arrayRemove, increment,
} from 'firebase/firestore';
import { db } from '@/lib/firebase';
// ── AFTER ────────────────────────────────────────────────────────────────────
import {
doc, collection, getDoc, getDocs, setDoc, addDoc, updateDoc, deleteDoc,
query, where, orderBy, limit,
Timestamp, arrayUnion, arrayRemove, increment,
} from 'fire2mongo';
import { db } from 'fire2mongo'; // db = {} no-op — kept so existing code compilesEverything below the import line is unchanged.
API Reference
References
doc(db, collection, id)
Returns a DocumentReference for a specific document.
import { doc, db } from 'fire2mongo';
const userRef = doc(db, 'users', userId);
const orderRef = doc(db, 'orders', orderId);doc(collectionRef) — auto-generate ID
Pass a CollectionReference (no ID) to get a reference with an auto-generated ObjectId.
const newRef = doc(collection(db, 'users'));
console.log(newRef.id); // auto-generated ObjectId stringcollection(db, path)
Returns a CollectionReference.
import { collection, db } from 'fire2mongo';
const usersCol = collection(db, 'users');
const ordersCol = collection(db, 'orders');Reading Data
getDoc(ref) — single document
import { doc, getDoc, db } from 'fire2mongo';
const snap = await getDoc(doc(db, 'users', userId));
if (snap.exists()) {
const user = snap.data(); // typed as your document type
console.log(snap.id); // document id string
} else {
console.log('Not found');
}With generics:
interface User { name: string; email: string; role: string; }
const snap = await getDoc<User>(doc(db, 'users', userId));
const user = snap.data(); // User | undefinedgetDocs(queryOrRef) — multiple documents
import { collection, getDocs, db } from 'fire2mongo';
// Entire collection
const snap = await getDocs(collection(db, 'users'));
snap.forEach(doc => {
console.log(doc.id, doc.data());
});
console.log(snap.size); // number of documents
console.log(snap.empty); // booleanWith a query:
import { query, where, orderBy, limit, getDocs, collection, db } from 'fire2mongo';
const q = query(
collection(db, 'orders'),
where('status', '==', 'PENDING'),
orderBy('createdAt', 'desc'),
limit(20),
);
const snap = await getDocs(q);
const orders = snap.docs.map(d => d.data());Writing Data
setDoc(ref, data) — create or overwrite
import { doc, setDoc, db } from 'fire2mongo';
await setDoc(doc(db, 'users', userId), {
name: 'Alice',
email: '[email protected]',
createdAt: Timestamp.now(),
});setDoc(ref, data, { merge: true }) — partial update (merge)
Only the provided fields are updated. Existing fields not in data are left unchanged.
await setDoc(doc(db, 'users', userId), { name: 'Alice Updated' }, { merge: true });addDoc(collectionRef, data) — create with auto-generated ID
import { collection, addDoc, db } from 'fire2mongo';
const ref = await addDoc(collection(db, 'orders'), {
customerId: 'cust_123',
total: 4500,
status: 'PENDING',
createdAt: Timestamp.now(),
});
console.log('New order ID:', ref.id);updateDoc(ref, partialData) — partial update
Only the provided fields are updated. Does not overwrite the entire document.
import { doc, updateDoc, db } from 'fire2mongo';
await updateDoc(doc(db, 'orders', orderId), {
status: 'SHIPPED',
shippedAt: Timestamp.now(),
});deleteDoc(ref) — delete a document
import { doc, deleteDoc, db } from 'fire2mongo';
await deleteDoc(doc(db, 'orders', orderId));Querying
Build queries with query() and any combination of where, orderBy, limit, and offset.
import { query, where, orderBy, limit, offset } from 'fire2mongo';
const q = query(
collection(db, 'orders'),
where('status', '==', 'ACTIVE'),
orderBy('createdAt', 'desc'),
limit(10),
offset(20), // skip first 20
);where(field, operator, value)
| Operator | MongoDB equivalent |
|---|---|
| '==' | { $eq } |
| '!=' | { $ne } |
| '<' | { $lt } |
| '<=' | { $lte } |
| '>' | { $gt } |
| '>=' | { $gte } |
| 'in' | { $in } |
| 'not-in' | { $nin } |
| 'array-contains' | { $elemMatch: { $eq } } |
| 'array-contains-any' | { $in } |
where('status', '==', 'ACTIVE')
where('age', '>=', 18)
where('role', 'in', ['admin', 'manager'])
where('tags', 'array-contains', 'premium')and(...constraints) — explicit AND grouping
import { and } from 'fire2mongo';
const q = query(
collection(db, 'users'),
and(
where('role', '==', 'staff'),
where('branch', '==', 'HQ'),
),
);or(...constraints) — OR grouping
import { or } from 'fire2mongo';
const q = query(
collection(db, 'orders'),
or(
where('status', '==', 'PENDING'),
where('status', '==', 'PROCESSING'),
),
);orderBy(field, direction?)
orderBy('createdAt', 'desc') // newest first
orderBy('name', 'asc') // alphabetical (default)limit(n) / offset(n)
limit(10) // return max 10 documents
offset(20) // skip first 20 documents (use for pagination)FieldValue
Atomic write operations — use inside updateDoc or setDoc.
arrayUnion(...elements) — add to array (no duplicates)
import { arrayUnion, updateDoc, doc, db } from 'fire2mongo';
await updateDoc(doc(db, 'users', userId), {
tags: arrayUnion('premium', 'verified'),
});
// MongoDB: $addToSet: { $each: ['premium', 'verified'] }arrayRemove(...elements) — remove from array
import { arrayRemove } from 'fire2mongo';
await updateDoc(doc(db, 'users', userId), {
tags: arrayRemove('trial'),
});
// MongoDB: $pull: { $in: ['trial'] }increment(n) — increment a numeric field
import { increment } from 'fire2mongo';
await updateDoc(doc(db, 'posts', postId), {
viewCount: increment(1),
likeCount: increment(-1),
});
// MongoDB: $inc: { viewCount: 1, likeCount: -1 }You can mix FieldValue operations with regular field updates in a single call:
await updateDoc(ref, {
status: 'ACTIVE', // regular $set
tags: arrayUnion('vip'), // $addToSet
loginCount: increment(1), // $inc
});Timestamp
Firebase-compatible Timestamp class. Stored as a JavaScript Date in MongoDB.
import { Timestamp } from 'fire2mongo';
// Create
const now = Timestamp.now();
const fromDate = Timestamp.fromDate(new Date('2025-01-01'));
const fromMs = Timestamp.fromMillis(Date.now());
// Convert
now.toDate() // → Date
now.toMillis() // → number (epoch ms)
now.seconds // → number
now.nanoseconds // → number
// Compare
now.isEqual(fromMs) // → boolean
// Use in writes
await setDoc(ref, {
createdAt: Timestamp.now(),
scheduledAt: Timestamp.fromDate(new Date('2025-12-31')),
});Batch Writes
Queue multiple write operations and execute them together. All ops run in parallel via Promise.all.
Note: Batch writes are not atomic. If one operation fails, others may already have applied. For atomic operations, use MongoDB sessions directly.
import { writeBatch, doc, db } from 'fire2mongo';
const batch = writeBatch(db);
batch.set(doc(db, 'users', 'user1'), { name: 'Alice', status: 'ACTIVE' });
batch.update(doc(db, 'orders', 'order1'), { status: 'SHIPPED' });
batch.delete(doc(db, 'drafts', 'draft1'));
await batch.commit();Transactions
Best-effort transaction — reads are immediate, writes are collected and executed in parallel after the callback resolves.
Note: This is not true ACID. There is no rollback on failure. For true atomic transactions use the MongoDB driver directly with sessions.
import { runTransaction, doc, db } from 'fire2mongo';
await runTransaction(db, async (tx) => {
const snap = await tx.get(doc(db, 'counters', 'global'));
const current = snap.data()?.value ?? 0;
tx.update(doc(db, 'counters', 'global'), { value: current + 1 });
});Subcollections
Firebase subcollections are mapped to flat MongoDB collections using _ as a separator. Parent IDs are automatically injected into documents on every write — no manual wiring needed.
Naming rule
Firebase path MongoDB collection Auto-injected fields
────────────────────────────────────── ──────────────────────── ────────────────────
orders/{orderId}/items orders_items ordersId
users/{userId}/sessions users_sessions usersId
users/{userId}/sessions/{id}/logs users_sessions_logs usersId, sessionsIdReading a subcollection
import { collection, getDocs, query, where, db } from 'fire2mongo';
// All items for a specific order
const itemsCol = collection(db, 'orders', orderId, 'items');
const snap = await getDocs(itemsCol);
// With a filter
const q = query(itemsCol, where('status', '==', 'ACTIVE'));
const snap = await getDocs(q);Writing to a subcollection
import { collection, addDoc, doc, setDoc, db } from 'fire2mongo';
// addDoc — auto ID, ordersId injected automatically
const ref = await addDoc(collection(db, 'orders', orderId, 'items'), {
productId: 'prod_123',
quantity: 2,
price: 1500,
});
// Stored as: { _id: <ObjectId>, ordersId: orderId, productId: ..., quantity: ..., price: ... }
// setDoc — specific ID
await setDoc(doc(db, 'orders', orderId, 'items', itemId), {
productId: 'prod_456',
quantity: 1,
});
// Stored as: { _id: itemId, ordersId: orderId, ... }Register subcollection names
Subcollections use the flattened MongoDB collection name for registration:
registerCollections({
orders: 'orders',
orders_items: 'orders_items', // subcollection
});Collection Registry
The registry maps Firebase collection names (used in doc() / collection() calls) to actual MongoDB collection names. This allows your Firebase-style code to stay unchanged even if MongoDB uses different collection names.
import { registerCollection, registerCollections, hasCollection, clearRegistry } from 'fire2mongo';
// Register one
registerCollection('users');
registerCollection('products', { mongoCollection: 'inventory_items' });
// Register many at once
registerCollections({
users: 'users',
orders: 'kms_orders',
products: 'inventory_items',
orders_items: 'orders_items',
});
// Check if registered
hasCollection('users') // true
hasCollection('unknown') // false
// Clear all (useful in tests)
clearRegistry();Connection Management
import { initMongoDB, getDb, closeMongoDB } from 'fire2mongo';
// Initialize (call once at startup)
await initMongoDB({ uri: 'mongodb://...', dbName: 'mydb' });
// Access the raw Db instance (for advanced MongoDB operations)
const db = getDb();
const col = db.collection('my_collection');
// Close (graceful shutdown / tests)
await closeMongoDB();getDb() throws if initMongoDB() has not been called:
[fire2mongo] MongoDB not initialized. Call initMongoDB() before using any firestore functions.Known Limitations
| Limitation | Detail |
|---|---|
| Server-side only | Uses the mongodb Node.js driver. Not compatible with browser or edge runtimes. |
| No real-time listeners | onSnapshot is not implemented. |
| Batch is not atomic | writeBatch.commit() runs ops via Promise.all — no session or rollback. |
| Transaction is not ACID | runTransaction collects writes and runs them in parallel. No rollback on failure. |
| setDoc checks existence | Without { merge: true }, setDoc calls findOne first to decide between insert and replace. |
| No cursor pagination | startAfter() is not supported. Use offset() for page-based pagination. |
| ObjectId IDs | Auto-generated IDs use MongoDB ObjectId. Firebase UID strings are stored as-is in the id field. |
Publishing
# Build CJS + ESM + type declarations
npm run build
# Verify types
npm run typecheck
# Publish to npm
npm publish --access publicBuild output in dist/:
dist/
index.js — CommonJS
index.mjs — ES Module
index.d.ts — TypeScript declarations (CJS)
index.d.mts — TypeScript declarations (ESM)License
MIT
