Offline-first MongoDB sync — Local + Atlas feel like ONE database. Drop-in production setup, automatic connection management, zero boilerplate.
MongoFire keeps a local MongoDB and MongoDB Atlas in sync — automatically, reliably, and with zero boilerplate. Your app reads and writes to a local MongoDB instance that is always fast and always available, even when offline. MongoFire handles the rest in the background.
⚡ MongoFire manages every MongoDB connection for you. You never call
mongoose.connect(). You never callapp.listen()directly. You import two functions —startAppandplugin— and everything else is automatic.
Features:
- Offline-first — your app never waits for the network
- Zero-config connection — local MongoDB connects automatically on import
startApp(app, port)— replacesapp.listen(), waits for DB, then opens the server- Automatic sync — uploads local changes and downloads remote ones on a configurable interval
- Real-time mode — optional Atlas Change Streams for near-instant propagation
- Conflict resolution — deterministic last-writer-wins with version tracking
- Per-field merge — field-level LWW prevents data loss when devices edit different fields simultaneously
- Resumable bootstrap — first sync streams from Atlas in batches, survives crashes
- Self-healing — detects and recovers lost writes caused by crashes or local DB resets
- Auto-spawn mongod — if MongoDB is not running, MongoFire starts it automatically
- CLI tools — interactive commands for status, conflicts, reconciliation, and safe reset
- TypeScript — full type declarations included
npm install mongofirePeer dependencies (install once in your project):
npm install mongodb mongoose dotenvnpx mongofire initThis creates three files in your project root:
| File | Purpose |
|---|---|
.env |
MongoDB connection strings |
mongofire.config.js |
Collections to sync, intervals, options |
mongofire.js |
The MongoFire entry point — do not delete |
ATLAS_URI=mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/
LOCAL_URI=mongodb://127.0.0.1:27017
DB_NAME=myapp
ATLAS_URIis optional — omit it to run in local-only mode during development.
server.js — your Express entry point:
// ESM
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import { startApp } from "./mongofire.js"; // ← import from YOUR mongofire.js
import authRoutes from "./routes/auth.routes.js";
import studentRoutes from "./routes/student.routes.js";
const app = express();
app.use(express.json());
app.use(cookieParser());
app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }));
app.use("/auth", authRoutes);
app.use("/students", studentRoutes);
// ✅ Replaces app.listen() — waits for local DB then starts the server
startApp(app, process.env.PORT || 3000);// CommonJS
const express = require("express");
const { startApp } = require("./mongofire");
const app = express();
app.use(express.json());
app.use("/auth", require("./routes/auth.routes"));
startApp(app, process.env.PORT || 3000);models/User.js — attach the plugin to your Mongoose schemas:
// ESM — from a file inside models/
import mongoose from "mongoose";
import { plugin } from "../mongofire.js"; // ← note: ../ because models/ is one level deep
const UserSchema = new mongoose.Schema({
name: String,
email: { type: String, unique: true },
updatedAt: Date,
});
UserSchema.plugin(plugin("users")); // collection name must match mongofire.config.js
export default mongoose.model("User", UserSchema);// CommonJS — from a file inside models/
const mongoose = require("mongoose");
const { plugin } = require("../mongofire"); // ← note: ../ one level up
const StudentSchema = new mongoose.Schema({
name: String,
grade: Number,
updatedAt: Date,
});
StudentSchema.plugin(plugin("students"));
module.exports = mongoose.model("Student", StudentSchema);Critical: The import path is always a relative path to
mongofire.jsin your project root. It is never'mongofire'(the npm package name). Fromserver.js(root) →'./mongofire.js'Frommodels/User.js(one level deep) →'../mongofire.js'
backend/
├── mongofire.js ← Generated by init — the bridge between your app and MongoFire
├── mongofire.config.js ← Your sync configuration
├── .env ← Connection strings (never commit this)
├── server.js ← import { startApp } from './mongofire.js'
├── models/
│ ├── User.js ← import { plugin } from '../mongofire.js'
│ └── Student.js ← import { plugin } from '../mongofire.js'
└── routes/
├── auth.routes.js
└── student.routes.js
startApp(app, port) replaces app.listen(). It:
- Waits for local MongoDB to be fully connected
- Calls
app.listen(port)only after the DB is ready - Logs
🚀 [MongoFire] Server ready on port <port>on success - If the local DB fails: logs a descriptive error and exits with code 1
Console output on startup:
✅ [MongoFire] Local MongoDB connected
🚀 [MongoFire] Server ready on port 3000
🌐 [MongoFire] Atlas connected — sync active
🔄 [MongoFire] Sync complete — ↑2 uploaded ↓5 downloaded 🗑 0 deleted
export default {
localUri: process.env.LOCAL_URI || "mongodb://127.0.0.1:27017",
atlasUri: process.env.ATLAS_URI,
dbName: process.env.DB_NAME || "myapp",
collections: ["users", "students", "orders"], // every collection your app uses
syncInterval: 30000, // ms between sync cycles (minimum 500)
batchSize: 200,
syncOwner: "*", // '*' = sync all | 'userId' = per-user isolation
realtime: false, // true = Atlas Change Streams
cleanDays: 7,
onSync(result) {
if (result.uploaded + result.downloaded + result.deleted > 0)
console.log(`Synced: ↑${result.uploaded} ↓${result.downloaded}`);
},
onError(err) {
console.error("Sync error:", err.message);
},
};| Option | Type | Default | Description |
|---|---|---|---|
collections |
string[] |
required | Collection names to sync |
localUri |
string |
'mongodb://127.0.0.1:27017' |
Local MongoDB URI |
atlasUri |
string |
null |
Atlas URI. Omit for local-only mode |
dbName |
string |
'myapp' |
Database name |
syncInterval |
number |
30000 (5000 if realtime:true) |
Polling interval in ms (minimum: 500) |
batchSize |
number |
200 |
Documents per batch (1–10 000) |
syncOwner |
string|fn |
'*' |
Owner filter — see Multi-Tenant section |
realtime |
boolean |
false |
Enable Atlas Change Streams |
cleanDays |
number |
7 |
Auto-clean synced records older than N days |
onSync |
function |
null |
Called after each sync cycle |
onError |
function |
null |
Called when a sync cycle throws |
reconcileOnStart |
boolean |
true |
Scan for lost writes at startup |
import { plugin } from "mongofire"; // ❌ ERR_MODULE_NOT_FOUND
const { plugin } = require("mongofire"); // ❌ wrong unless mongofire is installed globally// server.js (same folder as mongofire.js)
import { startApp, plugin } from "./mongofire.js";
// models/User.js (one folder deep)
import { plugin } from "../mongofire.js";
// routes/user.routes.js (one folder deep)
import { plugin } from "../mongofire.js";import { mongofire } from "./mongofire.js";
mongofire.on("localReady", () => console.log("Local DB connected"));
mongofire.on("online", () => console.log("Atlas connected"));
mongofire.on("offline", () => console.log("Working offline"));
mongofire.on("sync", (r) => console.log("Synced:", r));
mongofire.on("conflict", (c) => console.warn("Conflict:", c));
mongofire.on("error", (e) => console.error("Error:", e));
mongofire.on("stopped", () => console.log("Shut down cleanly"));| Event | Payload | When emitted |
|---|---|---|
localReady |
Db |
Local MongoDB connected (before Atlas) |
ready |
— | start() fully completed |
online |
— | Atlas connected |
offline |
— | Atlas becomes unreachable |
sync |
SyncResult |
After each sync cycle |
conflict |
ConflictData |
Local write conflicts with remote |
conflictResolved |
{ opId, resolution } |
After retry or dismiss |
stopped |
— | stop() finished |
error |
Error |
Unexpected sync error |
Replaces app.listen(). Waits for local DB, then opens the server port. Exits with code 1 on DB failure.
import { startApp } from "./mongofire.js";
startApp(app, process.env.PORT || 3000);Attaches change-tracking to a schema. Apply before mongoose.model().
import { plugin } from "../mongofire.js";
UserSchema.plugin(plugin("users"));
UserSchema.plugin(plugin("users", { ownerField: "userId" })); // multi-tenantResolves as soon as local MongoDB is connected, before Atlas.
import { localReady } from "./mongofire.js";
await localReady; // DB is guaranteed ready after thisResolves after Atlas connect and the first sync.
Manually trigger a sync ('required' or 'all').
Returns { online, pending, creates, updates, deletes, realtime }.
View and resolve sync conflicts.
Scan for and recover writes lost in a crash.
Wipe the local DB. Next startup re-bootstraps from Atlas. Unsynced changes are lost.
Flush in-flight ops and close connections. Called automatically on SIGINT/SIGTERM.
// mongofire.config.js
export default {
atlasUri: process.env.ATLAS_URI,
collections: ["orders"],
realtime: true, // requires Atlas M10+ or a local replica set
syncInterval: 5000, // polling fallback
};Falls back to polling if Change Streams are unavailable. Saves a resume token — restarts pick up exactly where they left off. Restarts use exponential backoff (2 s → 60 s).
For apps where each user must only sync their own data:
// mongofire.config.js
export default {
collections: ["notes"],
syncOwner: () => currentUserId, // returns the current user's ID
};// model
NoteSchema.plugin(plugin("notes", { ownerField: "userId" }));// create — always set the owner field
await Note.create({ title: "My note", userId: req.user._id });npx mongofire init # Setup wizard (creates mongofire.js, config, .env)
npx mongofire init --force # Overwrite existing files
npx mongofire init --esm # Force ESM output
npx mongofire init --cjs # Force CJS output
npx mongofire config # Update config interactively
npx mongofire status # Show pending sync counts
npx mongofire clean --days=7 # Delete records older than 7 days
npx mongofire conflicts # View and resolve conflicts
npx mongofire reconcile # Recover writes lost from crashes
npx mongofire reconcile --collection=users # Single collection
npx mongofire reset-local # Wipe local DB and re-bootstrapSet
MONGOFIRE_DEBUG=1for full error stack traces.
import { startApp, plugin, mongofire } from "./mongofire.js";
import type { SyncResult, ConflictData } from "mongofire";
UserSchema.plugin(plugin("users"));
startApp(app, 3000);
mongofire.on("sync", (result: SyncResult) => {
console.log(`↑${result.uploaded} ↓${result.downloaded}`);
});| Variable | Default | Description |
|---|---|---|
ATLAS_URI |
— | MongoDB Atlas connection string |
LOCAL_URI |
mongodb://127.0.0.1:27017 |
Local MongoDB URI |
DB_NAME |
myapp |
Database name |
MONGOFIRE_DEBUG |
unset | Set to 1 for full stack traces |
MONGOFIRE_VERIFY_REMOTE |
0 |
Set to 1 to checksum-verify each uploaded document |
MONGOFIRE_COLLECTION_CONCURRENCY |
4 |
Collections synced in parallel (max 32) |
MONGOFIRE_DBPATH |
~/.mongofire/<dbName> |
Data directory for auto-spawned mongod |
You are importing from 'mongofire' instead of from './mongofire.js'.
// ❌ Wrong
import { plugin } from "mongofire";
// ✅ Correct (from project root)
import { plugin } from "./mongofire.js";
// ✅ Correct (from models/ folder)
import { plugin } from "../mongofire.js";Remove any mongoose.connect() calls from your code. MongoFire manages the connection through localUri in mongofire.config.js.
- MongoDB is not installed — download here
mongodis not in your system PATHLOCAL_URIin.envis incorrect- Port 27017 is blocked by a firewall
MongoFire tries to auto-spawn mongod. Set MONGOFIRE_DBPATH to a writable directory if the default fails.
Run npx mongofire init to regenerate the config file.
MIT — see LICENSE