mongofire 6.5.5 → 6.5.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # 🔥 MongoFire
2
2
 
3
3
  > **Offline-first MongoDB sync** — Local + Atlas feel like ONE database.
4
- > Automatic conflict resolution, Mongoose plugin, interactive CLI, zero boilerplate.
4
+ > Drop-in production setup, automatic connection management, zero boilerplate.
5
5
 
6
6
  [![npm version](https://img.shields.io/npm/v/mongofire)](https://www.npmjs.com/package/mongofire)
7
7
  [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
@@ -11,15 +11,24 @@
11
11
 
12
12
  ## What is MongoFire?
13
13
 
14
- 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 everything else in the background.
14
+ 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.
15
15
 
16
+ > ⚡ **MongoFire manages every MongoDB connection for you.**
17
+ > You never call `mongoose.connect()`. You never call `app.listen()` directly.
18
+ > You import two functions — `startApp` and `plugin` — and everything else is automatic.
19
+
20
+ **Features:**
16
21
  - **Offline-first** — your app never waits for the network
22
+ - **Zero-config connection** — local MongoDB connects automatically on import
23
+ - **`startApp(app, port)`** — replaces `app.listen()`, waits for DB, then opens the server
17
24
  - **Automatic sync** — uploads local changes and downloads remote ones on a configurable interval
18
25
  - **Real-time mode** — optional Atlas Change Streams for near-instant propagation
19
- - **Conflict resolution** — deterministic last-writer-wins with version tracking; conflict events for manual handling when needed
20
- - **Resumable bootstrap** — first sync streams from Atlas in batches; survives crashes and resumes exactly where it left off
21
- - **Self-healing** — detects and recovers lost writes caused by crashes, local DB resets, or partial failures automatically
22
- - **CLI tools** — interactive commands for status, conflict resolution, reconciliation, and safe local reset
26
+ - **Conflict resolution** — deterministic last-writer-wins with version tracking
27
+ - **Per-field merge** — field-level LWW prevents data loss when devices edit different fields simultaneously
28
+ - **Resumable bootstrap** — first sync streams from Atlas in batches, survives crashes
29
+ - **Self-healing** — detects and recovers lost writes caused by crashes or local DB resets
30
+ - **Auto-spawn mongod** — if MongoDB is not running, MongoFire starts it automatically
31
+ - **CLI tools** — interactive commands for status, conflicts, reconciliation, and safe reset
23
32
  - **TypeScript** — full type declarations included
24
33
 
25
34
  ---
@@ -30,7 +39,7 @@ MongoFire keeps a **local MongoDB** and **MongoDB Atlas** in sync — automatica
30
39
  npm install mongofire
31
40
  ```
32
41
 
33
- **Peer dependencies:**
42
+ **Peer dependencies** (install once in your project):
34
43
 
35
44
  ```bash
36
45
  npm install mongodb mongoose dotenv
@@ -38,21 +47,23 @@ npm install mongodb mongoose dotenv
38
47
 
39
48
  ---
40
49
 
41
- ## Quick Start
50
+ ## Quick Start (3 steps)
42
51
 
43
- ### 1. Run the setup wizard
52
+ ### Step 1 Run the setup wizard
44
53
 
45
54
  ```bash
46
55
  npx mongofire init
47
56
  ```
48
57
 
49
- The interactive wizard creates three files:
58
+ This creates three files in your project root:
50
59
 
51
- - `.env` MongoDB connection strings
52
- - `mongofire.config.js` — which collections to sync, intervals, and options
53
- - `mongofire.js` imports config and starts sync
60
+ | File | Purpose |
61
+ |---|---|
62
+ | `.env` | MongoDB connection strings |
63
+ | `mongofire.config.js` | Collections to sync, intervals, options |
64
+ | `mongofire.js` | The MongoFire entry point — **do not delete** |
54
65
 
55
- ### 2. Fill in `.env`
66
+ ### Step 2 Fill in `.env`
56
67
 
57
68
  ```env
58
69
  ATLAS_URI=mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/
@@ -60,434 +71,350 @@ LOCAL_URI=mongodb://127.0.0.1:27017
60
71
  DB_NAME=myapp
61
72
  ```
62
73
 
63
- ### 3. Import mongofire.js in your app entry point
74
+ > `ATLAS_URI` is optional — omit it to run in local-only mode during development.
75
+
76
+ ### Step 3 — Use MongoFire in your project
77
+
78
+ **`server.js` — your Express entry point:**
64
79
 
65
80
  ```js
66
- // CommonJS
67
- require("./mongofire");
68
- const mongofire = require("mongofire");
81
+ // ESM
82
+ import express from 'express';
83
+ import cors from 'cors';
84
+ import cookieParser from 'cookie-parser';
85
+ import { startApp } from './mongofire.js'; // ← import from YOUR mongofire.js
86
+
87
+ import authRoutes from './routes/auth.routes.js';
88
+ import studentRoutes from './routes/student.routes.js';
89
+
90
+ const app = express();
91
+ app.use(express.json());
92
+ app.use(cookieParser());
93
+ app.use(cors({ origin: process.env.FRONTEND_URL, credentials: true }));
94
+
95
+ app.use('/auth', authRoutes);
96
+ app.use('/students', studentRoutes);
97
+
98
+ // ✅ Replaces app.listen() — waits for local DB then starts the server
99
+ startApp(app, process.env.PORT || 3000);
69
100
  ```
70
101
 
71
102
  ```js
72
- // ESM
73
- import "./mongofire.js";
74
- import mongofire from "mongofire";
103
+ // CommonJS
104
+ const express = require('express');
105
+ const { startApp } = require('./mongofire');
106
+
107
+ const app = express();
108
+ app.use(express.json());
109
+
110
+ app.use('/auth', require('./routes/auth.routes'));
111
+
112
+ startApp(app, process.env.PORT || 3000);
75
113
  ```
76
114
 
77
- ### 4. Add the plugin to your Mongoose schemas
115
+ **`models/User.js` attach the plugin to your Mongoose schemas:**
78
116
 
79
117
  ```js
80
- const mongofire = require("mongofire");
118
+ // ESM from a file inside models/
119
+ import mongoose from 'mongoose';
120
+ import { plugin } from '../mongofire.js'; // ← note: ../ because models/ is one level deep
81
121
 
82
- const OrderSchema = new mongoose.Schema({
83
- items: Array,
84
- total: Number,
122
+ const UserSchema = new mongoose.Schema({
123
+ name: String,
124
+ email: { type: String, unique: true },
85
125
  updatedAt: Date,
86
126
  });
87
127
 
88
- OrderSchema.plugin(mongofire.plugin("orders")); // <— must be BEFORE creating the model
128
+ UserSchema.plugin(plugin('users')); // collection name must match mongofire.config.js
89
129
 
90
- const Order = mongoose.model("Order", OrderSchema);
130
+ export default mongoose.model('User', UserSchema);
91
131
  ```
92
132
 
93
- Every `save()`, `create()`, `update()`, and `delete()` is now tracked and synced automatically.
94
-
95
- ---
96
-
97
- ## Config Options
98
-
99
133
  ```js
100
- // mongofire.config.js
101
- module.exports = {
102
- localUri: process.env.LOCAL_URI || "mongodb://127.0.0.1:27017",
103
- atlasUri: process.env.ATLAS_URI,
104
- dbName: process.env.DB_NAME || "myapp",
134
+ // CommonJS — from a file inside models/
135
+ const mongoose = require('mongoose');
136
+ const { plugin } = require('../mongofire'); // ← note: ../ one level up
105
137
 
106
- collections: ["orders", "products", "users"],
138
+ const StudentSchema = new mongoose.Schema({
139
+ name: String,
140
+ grade: Number,
141
+ updatedAt: Date,
142
+ });
107
143
 
108
- syncInterval: 30000, // ms between sync cycles (default: 30 s)
109
- batchSize: 200, // documents per batch
110
- syncOwner: "*", // '*' = sync all data (default)
111
- realtime: false, // enable Atlas Change Streams
144
+ StudentSchema.plugin(plugin('students'));
112
145
 
113
- onSync(result) {
114
- if (result.uploaded + result.downloaded + result.deleted > 0) {
115
- console.log(`Synced: up:${result.uploaded} down:${result.downloaded}`);
116
- }
117
- },
118
- onError(err) {
119
- console.error("Sync error:", err.message);
120
- },
121
- };
146
+ module.exports = mongoose.model('Student', StudentSchema);
122
147
  ```
123
148
 
124
- ### All config fields
125
-
126
- | Option | Type | Default | Description |
127
- | ------------------- | -------------- | ----------------------------- | ------------------------------------------------ |
128
- | `collections` | `string[]` | required | Collection names to sync |
129
- | `localUri` | `string` | `'mongodb://localhost:27017'` | Local MongoDB URI |
130
- | `atlasUri` | `string` | `null` | Atlas URI. Omit for local-only mode |
131
- | `dbName` | `string` | `'mongofire'` | Database name |
132
- | `syncInterval` | `number` | `30000` | Polling interval in ms |
133
- | `batchSize` | `number` | `200` | Documents per upload/download batch |
134
- | `syncOwner` | `string \| fn` | `'*'` | Owner filter. See [Multi-Tenant](#multi-tenant) |
135
- | `realtime` | `boolean` | `false` | Enable Atlas Change Streams for instant sync |
136
- | `onSync` | `function` | `null` | Called after each sync cycle with a `SyncResult` |
137
- | `onError` | `function` | `null` | Called when a sync cycle throws |
138
- | `reconcileOnStart` | `boolean` | `true` | Scan for lost writes at startup |
139
- | `reconcileFullScan` | `boolean` | `true` | Include deep phase of reconciliation |
149
+ > **Critical:** The import path is always a **relative path** to `mongofire.js` in your project root.
150
+ > It is **never** `'mongofire'` (the npm package name).
151
+ > From `server.js` (root) → `'./mongofire.js'`
152
+ > From `models/User.js` (one level deep) `'../mongofire.js'`
140
153
 
141
154
  ---
142
155
 
143
- ## Events
156
+ ## Project folder structure
144
157
 
145
- ```js
146
- mongofire.on("ready", () => console.log("MongoFire started"));
147
- mongofire.on("online", () => console.log("Atlas connected"));
148
- mongofire.on("offline", () => console.log("Working locally"));
149
- mongofire.on("sync", (r) => console.log("Sync result:", r));
150
- mongofire.on("conflict", (c) => console.warn("Conflict:", c));
151
- mongofire.on("conflictResolved", (d) => console.log("Resolved:", d.opId));
152
- mongofire.on("reconcileComplete", (r) =>
153
- console.log("Re-queued:", r.totalQueued),
154
- );
155
- mongofire.on("realtimeStarted", () => console.log("Change streams active"));
156
- mongofire.on("realtimeStopped", () => console.log("Realtime stopped"));
157
- mongofire.on("stopped", () => console.log("Shut down cleanly"));
158
- mongofire.on("error", (e) => console.error("Error:", e));
159
158
  ```
160
-
161
- | Event | Payload | When emitted |
162
- | ------------------- | ------------------------------ | ------------------------------------------ |
163
- | `ready` | — | `start()` completed |
164
- | `online` | — | Atlas connection established |
165
- | `offline` | — | Atlas becomes unreachable |
166
- | `sync` | `SyncResult` | After each sync cycle |
167
- | `conflict` | `ConflictData` | Local write conflicts with remote |
168
- | `conflictResolved` | `{ opId, resolution }` | After `retryConflict` or `dismissConflict` |
169
- | `reconcileComplete` | `{ collections, totalQueued }` | After reconciliation scan |
170
- | `realtimeStarted` | — | Change streams activated |
171
- | `realtimeStopped` | — | Change streams stopped |
172
- | `stopped` | — | `stop()` finished |
173
- | `error` | `Error` | Unexpected sync error |
159
+ backend/
160
+ ├── mongofire.js ← Generated by init — the bridge between your app and MongoFire
161
+ ├── mongofire.config.js ← Your sync configuration
162
+ ├── .env ← Connection strings (never commit this)
163
+ ├── server.js ← import { startApp } from './mongofire.js'
164
+ ├── models/
165
+ │ ├── User.js ← import { plugin } from '../mongofire.js'
166
+ │ └── Student.js ← import { plugin } from '../mongofire.js'
167
+ └── routes/
168
+ ├── auth.routes.js
169
+ └── student.routes.js
170
+ ```
174
171
 
175
172
  ---
176
173
 
177
- ## API
174
+ ## What `startApp()` does
178
175
 
179
- ### `mongofire.start(config)` `Promise<MongoFire>`
176
+ `startApp(app, port)` replaces `app.listen()`. It:
180
177
 
181
- Connect to local MongoDB and Atlas, run the initial sync, start background polling. Safe to call multiple times — concurrent calls share the same init promise.
178
+ 1. Waits for local MongoDB to be fully connected
179
+ 2. Calls `app.listen(port)` only after the DB is ready
180
+ 3. Logs `🚀 [MongoFire] Server ready on port <port>` on success
181
+ 4. If the local DB fails: logs a descriptive error and exits with code 1
182
182
 
183
- ### `mongofire.stop(timeoutMs?)` `Promise<void>`
183
+ **Console output on startup:**
184
184
 
185
- Flush any in-flight operations and close all connections. Default timeout: **10,000 ms**.
185
+ ```
186
+ ✅ [MongoFire] Local MongoDB connected
187
+ 🚀 [MongoFire] Server ready on port 3000
188
+ 🌐 [MongoFire] Atlas connected — sync active
189
+ 🔄 [MongoFire] Sync complete — ↑2 uploaded ↓5 downloaded 🗑 0 deleted
190
+ ```
186
191
 
187
- ### `mongofire.sync(type?)` → `Promise<SyncResult>`
192
+ ---
188
193
 
189
- Manually trigger a sync. Returns `{ error: 'offline', pending }` when Atlas is unreachable. Throttled to a minimum of 500 ms between calls.
194
+ ## Config Options (`mongofire.config.js`)
190
195
 
191
- | `type` | Behaviour |
192
- | ------------ | --------------------------------------------------- |
193
- | `'required'` | Upload pending ops + download new changes (default) |
194
- | `'all'` | Full bi-directional sync |
196
+ ```js
197
+ export default {
198
+ localUri: process.env.LOCAL_URI || 'mongodb://127.0.0.1:27017',
199
+ atlasUri: process.env.ATLAS_URI,
200
+ dbName: process.env.DB_NAME || 'myapp',
195
201
 
196
- ### `mongofire.status()` `Promise<SyncStatus>`
202
+ collections: ['users', 'students', 'orders'], // every collection your app uses
197
203
 
198
- ```ts
199
- interface SyncStatus {
200
- online: boolean;
201
- pending: number; // total unsynced operations
202
- creates: number;
203
- updates: number;
204
- deletes: number;
205
- realtime: boolean;
206
- }
204
+ syncInterval: 30000, // ms between sync cycles (minimum 500)
205
+ batchSize: 200,
206
+ syncOwner: '*', // '*' = sync all | 'userId' = per-user isolation
207
+ realtime: false, // true = Atlas Change Streams
208
+ cleanDays: 7,
209
+
210
+ onSync(result) {
211
+ if (result.uploaded + result.downloaded + result.deleted > 0)
212
+ console.log(`Synced: ↑${result.uploaded} ↓${result.downloaded}`);
213
+ },
214
+ onError(err) { console.error('Sync error:', err.message); },
215
+ };
207
216
  ```
208
217
 
209
- ### `mongofire.clean(days?, opts?)` `Promise<number>`
218
+ | Option | Type | Default | Description |
219
+ |---------------------|----------------|--------------------------------------|---------------------------------------------------------|
220
+ | `collections` | `string[]` | **required** | Collection names to sync |
221
+ | `localUri` | `string` | `'mongodb://127.0.0.1:27017'` | Local MongoDB URI |
222
+ | `atlasUri` | `string` | `null` | Atlas URI. Omit for local-only mode |
223
+ | `dbName` | `string` | `'myapp'` | Database name |
224
+ | `syncInterval` | `number` | `30000` (`5000` if `realtime:true`) | Polling interval in ms (minimum: 500) |
225
+ | `batchSize` | `number` | `200` | Documents per batch (1–10 000) |
226
+ | `syncOwner` | `string\|fn` | `'*'` | Owner filter — see Multi-Tenant section |
227
+ | `realtime` | `boolean` | `false` | Enable Atlas Change Streams |
228
+ | `cleanDays` | `number` | `7` | Auto-clean synced records older than N days |
229
+ | `onSync` | `function` | `null` | Called after each sync cycle |
230
+ | `onError` | `function` | `null` | Called when a sync cycle throws |
231
+ | `reconcileOnStart` | `boolean` | `true` | Scan for lost writes at startup |
210
232
 
211
- Delete old synced records to keep the local database tidy.
233
+ ---
212
234
 
213
- | Parameter | Default | Description |
214
- | ------------------- | -------------- | ----------------------------------------------- |
215
- | `days` | `7` | Delete synced records older than N days |
216
- | `opts.conflictDays` | same as `days` | Delete stale conflict records older than N days |
235
+ ## Common import mistakes
217
236
 
218
- ### `mongofire.conflicts(collection?)` `Promise<ConflictRecord[]>`
237
+ ### Wrong — using the npm package name
219
238
 
220
239
  ```js
221
- const list = await mongofire.conflicts();
222
- for (const c of list) {
223
- console.log(`${c.collection}/${c.docId} op:${c.type} v${c.version}`);
224
- console.log("Error:", c.lastError);
225
- }
240
+ import { plugin } from 'mongofire'; // ❌ ERR_MODULE_NOT_FOUND
241
+ const { plugin } = require('mongofire'); // ❌ wrong unless mongofire is installed globally
226
242
  ```
227
243
 
228
- ### `mongofire.retryConflict(opId)` → `Promise<void>`
244
+ ### ✅ Correct — relative path to your local mongofire.js
229
245
 
230
- Reset a conflict back to pending so the next sync retries it. Emits `conflictResolved` with `resolution: 'retried'`.
246
+ ```js
247
+ // server.js (same folder as mongofire.js)
248
+ import { startApp, plugin } from './mongofire.js';
231
249
 
232
- ### `mongofire.dismissConflict(opId)` `Promise<void>`
250
+ // models/User.js (one folder deep)
251
+ import { plugin } from '../mongofire.js';
233
252
 
234
- Dismiss a conflict — remote version wins and the local change is discarded. Emits `conflictResolved` with `resolution: 'dismissed'`.
253
+ // routes/user.routes.js (one folder deep)
254
+ import { plugin } from '../mongofire.js';
255
+ ```
235
256
 
236
- ### `mongofire.reconcile(collectionOrOpts?, opts?)` → `Promise<ReconcileResult[]>`
257
+ ---
237
258
 
238
- Scan for writes lost in a crash and re-queue them for sync.
259
+ ## Events
239
260
 
240
261
  ```js
241
- await mongofire.reconcile(); // all collections
242
- await mongofire.reconcile({ fullScan: false }); // fast scan only
243
- await mongofire.reconcile("orders"); // single collection
262
+ import { mongofire } from './mongofire.js';
263
+
264
+ mongofire.on('localReady', () => console.log('Local DB connected'));
265
+ mongofire.on('online', () => console.log('Atlas connected'));
266
+ mongofire.on('offline', () => console.log('Working offline'));
267
+ mongofire.on('sync', (r) => console.log('Synced:', r));
268
+ mongofire.on('conflict', (c) => console.warn('Conflict:', c));
269
+ mongofire.on('error', (e) => console.error('Error:', e));
270
+ mongofire.on('stopped', () => console.log('Shut down cleanly'));
244
271
  ```
245
272
 
246
- | Phase | What it checks |
247
- | ------- | ------------------------------------------------------- |
248
- | Phase 1 | Metadata rows with no matching operation entry |
249
- | Phase 2 | Data documents with no metadata entry (`fullScan` only) |
273
+ | Event | Payload | When emitted |
274
+ |--------------------|--------------------------------|----------------------------------------|
275
+ | `localReady` | `Db` | Local MongoDB connected (before Atlas) |
276
+ | `ready` | | `start()` fully completed |
277
+ | `online` | — | Atlas connected |
278
+ | `offline` | — | Atlas becomes unreachable |
279
+ | `sync` | `SyncResult` | After each sync cycle |
280
+ | `conflict` | `ConflictData` | Local write conflicts with remote |
281
+ | `conflictResolved` | `{ opId, resolution }` | After retry or dismiss |
282
+ | `stopped` | — | `stop()` finished |
283
+ | `error` | `Error` | Unexpected sync error |
250
284
 
251
- ### `mongofire.resetLocal()` → `Promise<{ dropped: number }>`
285
+ ---
286
+
287
+ ## API Reference
288
+
289
+ ### `startApp(app, port)` → `Promise<http.Server>`
252
290
 
253
- Safely wipe the entire local database and all MongoFire state. The next `start()` re-bootstraps from Atlas cleanly.
291
+ Replaces `app.listen()`. Waits for local DB, then opens the server port. Exits with code 1 on DB failure.
254
292
 
255
293
  ```js
256
- // Check for unsynced changes first
257
- const { pending } = await mongofire.status();
258
- if (pending > 0) {
259
- console.warn(`${pending} unsynced operations will be lost`);
260
- }
261
-
262
- const { dropped } = await mongofire.resetLocal();
263
- console.log(
264
- `Wiped ${dropped} collections. Restart to re-bootstrap from Atlas.`,
265
- );
294
+ import { startApp } from './mongofire.js';
295
+ startApp(app, process.env.PORT || 3000);
266
296
  ```
267
297
 
268
- > **Warning:** Any unsynced local changes are permanently lost. Use `mongofire.status()` first if you need to verify there is nothing pending.
298
+ ### `plugin(collectionName, options?)` Mongoose plugin function
269
299
 
270
- ### `mongofire.plugin(collectionName, options?)`
300
+ Attaches change-tracking to a schema. Apply **before** `mongoose.model()`.
271
301
 
272
302
  ```js
273
- // Basic
274
- OrderSchema.plugin(mongofire.plugin("orders"));
275
-
276
- // With options
277
- UserSchema.plugin(
278
- mongofire.plugin("users", {
279
- ownerField: "userId", // required only for multi-tenant
280
- batchSize: 200,
281
- concurrency: 8,
282
- }),
283
- );
303
+ import { plugin } from '../mongofire.js';
304
+ UserSchema.plugin(plugin('users'));
305
+ UserSchema.plugin(plugin('users', { ownerField: 'userId' })); // multi-tenant
284
306
  ```
285
307
 
286
- | Option | Type | Default | Description |
287
- | ------------- | -------- | ------- | ----------------------------------------------------- |
288
- | `ownerField` | `string` | `null` | Dot-path to owner field. Only needed for multi-tenant |
289
- | `batchSize` | `number` | `200` | Batch size for bulk operations |
290
- | `concurrency` | `number` | `8` | Concurrent tracking calls per batch |
308
+ ### `localReady` `Promise<Db>`
291
309
 
292
- ---
293
-
294
- ## Real-Time Sync
295
-
296
- Enable Atlas Change Streams for near-instant propagation between devices:
310
+ Resolves as soon as local MongoDB is connected, before Atlas.
297
311
 
298
312
  ```js
299
- await mongofire.start({
300
- atlasUri: process.env.ATLAS_URI,
301
- collections: ["orders"],
302
- realtime: true, // requires Atlas M10+ or a local replica set
303
- syncInterval: 5000, // polling fallback interval
304
- });
305
-
306
- mongofire.on("realtimeStarted", () => console.log("Changes appear instantly"));
313
+ import { localReady } from './mongofire.js';
314
+ await localReady; // DB is guaranteed ready after this
307
315
  ```
308
316
 
309
- Falls back to polling automatically if Change Streams are unavailable.
317
+ ### `ready` `Promise<MongoFire>`
310
318
 
311
- ---
319
+ Resolves after Atlas connect and the first sync.
312
320
 
313
- ## Multi-Tenant
314
-
315
- > **Most apps do not need this.**
316
- > If all users share the same data — a café, a team app, a single company — use the default `syncOwner: '*'` and skip this section entirely.
317
-
318
- Multi-tenant mode is for apps where **each user must only sync their own private data**.
321
+ ### `mongofire.sync(type?)` → `Promise<SyncResult>`
319
322
 
320
- ### Do you need it?
323
+ Manually trigger a sync (`'required'` or `'all'`).
321
324
 
322
- | App type | Need multi-tenant? |
323
- | ---------------------------------- | ------------------------- |
324
- | Café / restaurant system | No — staff share data |
325
- | Single-company team app | No — everyone shares data |
326
- | SaaS with per-tenant isolation | **Yes** |
327
- | Per-user notes / tasks | **Yes** |
328
- | Ride-hailing — each driver's data | **Yes** |
329
- | Multi-school, each school isolated | **Yes** |
325
+ ### `mongofire.status()` `Promise<SyncStatus>`
330
326
 
331
- ### Setup (4 steps)
327
+ Returns `{ online, pending, creates, updates, deletes, realtime }`.
332
328
 
333
- **Step 1 Add an owner field to every synced model**
329
+ ### `mongofire.conflicts()` / `retryConflict(opId)` / `dismissConflict(opId)`
334
330
 
335
- ```js
336
- const OrderSchema = new mongoose.Schema({
337
- items: Array,
338
- total: Number,
339
- userId: { type: mongoose.Types.ObjectId, required: true },
340
- updatedAt: Date,
341
- });
331
+ View and resolve sync conflicts.
342
332
 
343
- OrderSchema.plugin(mongofire.plugin("orders", { ownerField: "userId" }));
344
- ```
333
+ ### `mongofire.reconcile(opts?)` `Promise<ReconcileResult[]>`
345
334
 
346
- **Step 2 Set `syncOwner` in config**
335
+ Scan for and recover writes lost in a crash.
347
336
 
348
- ```js
349
- module.exports = {
350
- collections: ["orders", "products"],
351
- syncOwner: "userId",
352
- // ...
353
- };
354
- ```
337
+ ### `mongofire.resetLocal()` → `Promise<{ dropped: number }>`
355
338
 
356
- **Step 3 Pass the current user's ID when starting sync**
339
+ Wipe the local DB. Next startup re-bootstraps from Atlas. **Unsynced changes are lost.**
357
340
 
358
- ```js
359
- async function login(req, res) {
360
- const user = await User.findOne({ email: req.body.email });
361
- // ... password check ...
362
-
363
- await mongofire.start({
364
- ...config,
365
- syncOwner: user._id.toString(),
366
- });
367
-
368
- res.json({ token, user });
369
- }
370
-
371
- async function logout(req, res) {
372
- await mongofire.stop();
373
- res.json({ message: "Logged out" });
374
- }
375
- ```
341
+ ### `mongofire.stop(timeoutMs?)` → `Promise<void>`
376
342
 
377
- **Step 4 Always set the owner field when creating documents**
343
+ Flush in-flight ops and close connections. Called automatically on `SIGINT`/`SIGTERM`.
378
344
 
379
- ```js
380
- const order = await Order.create({
381
- items: req.body.items,
382
- total: req.body.total,
383
- userId: req.user._id,
384
- });
385
- ```
345
+ ---
386
346
 
387
- ### Dynamic owner using a function
347
+ ## Real-Time Sync
388
348
 
389
349
  ```js
390
- await mongofire.start({
391
- ...config,
392
- syncOwner: () => getCurrentUser()?.id ?? null,
393
- });
350
+ // mongofire.config.js
351
+ export default {
352
+ atlasUri: process.env.ATLAS_URI,
353
+ collections: ['orders'],
354
+ realtime: true, // requires Atlas M10+ or a local replica set
355
+ syncInterval: 5000, // polling fallback
356
+ };
394
357
  ```
395
358
 
396
- > **Security note:** If `syncOwner` is a function and it throws, the sync is **aborted** and an `error` event is emitted. MongoFire never falls back to syncing all data on error.
359
+ 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).
397
360
 
398
361
  ---
399
362
 
400
- ## Using the plugin directly (without the MongoFire instance)
401
-
402
- ```js
403
- // CommonJS
404
- const mongofirePlugin = require("mongofire/plugin");
405
- OrderSchema.plugin(mongofirePlugin, { collection: "orders" });
363
+ ## Multi-Tenant
406
364
 
407
- // CommonJS factory
408
- const { factory } = require("mongofire/plugin");
409
- OrderSchema.plugin(factory("orders"));
410
- ```
365
+ For apps where each user must only sync their own data:
411
366
 
412
367
  ```js
413
- // ESM
414
- import mongofirePlugin, { factory } from "mongofire/plugin";
415
- OrderSchema.plugin(factory("orders"));
368
+ // mongofire.config.js
369
+ export default {
370
+ collections: ['notes'],
371
+ syncOwner: () => currentUserId, // returns the current user's ID
372
+ };
416
373
  ```
417
374
 
418
- ---
419
-
420
- ## Safe Local Reset
421
-
422
- If the local database is cleared or corrupted, MongoFire automatically detects and resolves any stale pending operations during the next bootstrap — no manual conflict resolution, no stuck queues.
423
-
424
- For a deliberate clean reset, use either:
425
-
426
- ```bash
427
- # Interactive CLI — confirms before wiping
428
- npx mongofire reset-local
375
+ ```js
376
+ // model
377
+ NoteSchema.plugin(plugin('notes', { ownerField: 'userId' }));
429
378
  ```
430
379
 
431
380
  ```js
432
- // Programmatic
433
- const { dropped } = await mongofire.resetLocal();
381
+ // create — always set the owner field
382
+ await Note.create({ title: 'My note', userId: req.user._id });
434
383
  ```
435
384
 
436
- Both drop all local data and MongoFire state so the next startup re-bootstraps from Atlas cleanly.
437
-
438
385
  ---
439
386
 
440
387
  ## CLI Reference
441
388
 
442
389
  ```bash
443
- npx mongofire init # Interactive setup wizard
444
- npx mongofire init --force # Overwrite existing files
445
- npx mongofire init --esm # Force ESM output
446
- npx mongofire init --cjs # Force CommonJS output
447
- npx mongofire config # Update an existing config
448
- npx mongofire status # Show pending sync counts
449
- npx mongofire clean # Delete old records (interactive)
450
- npx mongofire clean --days=14 # Delete records older than 14 days
451
- npx mongofire conflicts # View and resolve conflicts
452
- npx mongofire reconcile # Recover writes lost from crashes
453
- npx mongofire reconcile --no-full-scan # Fast scan (Phase 1 only)
454
- npx mongofire reconcile --collection=orders # Single collection
455
- npx mongofire reset-local # Safely wipe local DB and re-bootstrap
390
+ npx mongofire init # Setup wizard (creates mongofire.js, config, .env)
391
+ npx mongofire init --force # Overwrite existing files
392
+ npx mongofire init --esm # Force ESM output
393
+ npx mongofire init --cjs # Force CJS output
394
+ npx mongofire config # Update config interactively
395
+ npx mongofire status # Show pending sync counts
396
+ npx mongofire clean --days=7 # Delete records older than 7 days
397
+ npx mongofire conflicts # View and resolve conflicts
398
+ npx mongofire reconcile # Recover writes lost from crashes
399
+ npx mongofire reconcile --collection=users # Single collection
400
+ npx mongofire reset-local # Wipe local DB and re-bootstrap
456
401
  ```
457
402
 
458
- | Command | Description | TTY required? | Key flags |
459
- | ------------- | --------------------------------------------------------- | ------------- | ------------------------------------- |
460
- | `init` | Setup wizard | Optional | `--esm`, `--cjs`, `--force` |
461
- | `config` | Update an existing config | Yes | — |
462
- | `status` | Show pending ops and online state | No | — |
463
- | `clean` | Delete old sync records | Optional | `--days=N` (1–3650, default 7) |
464
- | `conflicts` | View and resolve conflicts interactively | Yes | — |
465
- | `reconcile` | Recover writes lost from crashes | No | `--no-full-scan`, `--collection=NAME` |
466
- | `reset-local` | Wipe local DB and all sync state for a clean re-bootstrap | Yes | — |
467
-
468
- > **Tip:** Set `MONGOFIRE_DEBUG=1` for full error stack traces in any command.
403
+ > Set `MONGOFIRE_DEBUG=1` for full error stack traces.
469
404
 
470
405
  ---
471
406
 
472
407
  ## TypeScript
473
408
 
474
409
  ```ts
475
- import mongofire, { SyncConfig, SyncResult, ConflictData } from "mongofire";
476
-
477
- const config: SyncConfig = {
478
- collections: ["orders", "products"],
479
- atlasUri: process.env.ATLAS_URI,
480
- realtime: true,
481
- };
482
-
483
- await mongofire.start(config);
410
+ import { startApp, plugin, mongofire } from './mongofire.js';
411
+ import type { SyncResult, ConflictData } from 'mongofire';
484
412
 
485
- mongofire.on("sync", (result: SyncResult) => {
486
- console.log(`up:${result.uploaded} down:${result.downloaded}`);
487
- });
413
+ UserSchema.plugin(plugin('users'));
414
+ startApp(app, 3000);
488
415
 
489
- mongofire.on("conflict", (c: ConflictData) => {
490
- console.warn(`Conflict: ${c.collection}/${c.docId} op:${c.op}`);
416
+ mongofire.on('sync', (result: SyncResult) => {
417
+ console.log(`↑${result.uploaded} ↓${result.downloaded}`);
491
418
  });
492
419
  ```
493
420
 
@@ -495,24 +422,51 @@ mongofire.on("conflict", (c: ConflictData) => {
495
422
 
496
423
  ## Environment Variables
497
424
 
498
- | Variable | Default | Description |
499
- | ---------------------------------- | ------- | ---------------------------------------------------- |
500
- | `MONGOFIRE_DEBUG` | unset | Set to `1` for full error stack traces |
501
- | `MONGOFIRE_VERIFY_REMOTE` | `0` | Set to `1` to checksum-verify each uploaded document |
502
- | `MONGOFIRE_COLLECTION_CONCURRENCY` | `4` | Collections synced in parallel (capped at 32) |
425
+ | Variable | Default | Description |
426
+ |------------------------------------|--------------------------|------------------------------------------------------|
427
+ | `ATLAS_URI` | | MongoDB Atlas connection string |
428
+ | `LOCAL_URI` | `mongodb://127.0.0.1:27017` | Local MongoDB URI |
429
+ | `DB_NAME` | `myapp` | Database name |
430
+ | `MONGOFIRE_DEBUG` | unset | Set to `1` for full stack traces |
431
+ | `MONGOFIRE_VERIFY_REMOTE` | `0` | Set to `1` to checksum-verify each uploaded document |
432
+ | `MONGOFIRE_COLLECTION_CONCURRENCY` | `4` | Collections synced in parallel (max 32) |
433
+ | `MONGOFIRE_DBPATH` | `~/.mongofire/<dbName>` | Data directory for auto-spawned mongod |
503
434
 
504
435
  ---
505
436
 
506
- ## Collection Name Rules
437
+ ## Troubleshooting
438
+
439
+ ### `ERR_MODULE_NOT_FOUND: Cannot find package 'mongofire'`
440
+
441
+ You are importing `from 'mongofire'` instead of `from './mongofire.js'`.
442
+
443
+ ```js
444
+ // ❌ Wrong
445
+ import { plugin } from 'mongofire';
446
+
447
+ // ✅ Correct (from project root)
448
+ import { plugin } from './mongofire.js';
449
+
450
+ // ✅ Correct (from models/ folder)
451
+ import { plugin } from '../mongofire.js';
452
+ ```
453
+
454
+ ### `mongoose.connect() should not be called`
455
+
456
+ Remove any `mongoose.connect()` calls from your code. MongoFire manages the connection through `localUri` in `mongofire.config.js`.
457
+
458
+ ### `Local MongoDB failed to connect`
459
+
460
+ - MongoDB is not installed — [download here](https://www.mongodb.com/try/download/community)
461
+ - `mongod` is not in your system PATH
462
+ - `LOCAL_URI` in `.env` is incorrect
463
+ - Port 27017 is blocked by a firewall
507
464
 
508
- Names must:
465
+ MongoFire tries to auto-spawn `mongod`. Set `MONGOFIRE_DBPATH` to a writable directory if the default fails.
509
466
 
510
- - Start with a letter or digit
511
- - Contain only letters, digits, `_`, `-`, or `.`
512
- - **Not** contain `:` — causes internal key collisions
513
- - **Not** start with `_mf_` — reserved prefix
467
+ ### `Cannot find module './mongofire.config.js'`
514
468
 
515
- Invalid names are rejected at startup with a clear error message.
469
+ Run `npx mongofire init` to regenerate the config file.
516
470
 
517
471
  ---
518
472