mongofire 6.5.3 → 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
- > **Offline-first MongoDB sync** — Local + Atlas feel like ONE database.
4
- > Automatic conflict resolution, Mongoose plugin, zero boilerplate.
3
+ > **Offline-first MongoDB sync** — Local + Atlas feel like ONE database.
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)
@@ -9,15 +9,27 @@
9
9
 
10
10
  ---
11
11
 
12
- ## What it does
12
+ ## What is MongoFire?
13
13
 
14
- MongoFire keeps a **local MongoDB** and **MongoDB Atlas** in sync automatically.
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
- - Your app reads/writes to local MongoDB always fast, works offline
17
- - MongoFire tracks every change and uploads to Atlas when online
18
- - Downloads remote changes from Atlas in the background
19
- - If you go offline, changes queue up and sync when you reconnect
20
- - Conflicts resolved automatically (last-writer-wins with version vectors)
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:**
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
24
+ - **Automatic sync** — uploads local changes and downloads remote ones on a configurable interval
25
+ - **Real-time mode** — optional Atlas Change Streams for near-instant propagation
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
32
+ - **TypeScript** — full type declarations included
21
33
 
22
34
  ---
23
35
 
@@ -27,28 +39,31 @@ MongoFire keeps a **local MongoDB** and **MongoDB Atlas** in sync automatically.
27
39
  npm install mongofire
28
40
  ```
29
41
 
30
- **Peer dependencies** (install the ones you use):
42
+ **Peer dependencies** (install once in your project):
31
43
 
32
44
  ```bash
33
- npm install mongodb mongoose
45
+ npm install mongodb mongoose dotenv
34
46
  ```
35
47
 
36
48
  ---
37
49
 
38
- ## Quick Start
50
+ ## Quick Start (3 steps)
39
51
 
40
- ### 1. Init config files
52
+ ### Step 1 Run the setup wizard
41
53
 
42
54
  ```bash
43
55
  npx mongofire init
44
56
  ```
45
57
 
46
- This creates:
47
- - `.env` — MongoDB connection strings
48
- - `mongofire.config.js` which collections to sync
49
- - `mongofire.js` — app entry point
58
+ This creates three files in your project root:
59
+
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** |
50
65
 
51
- ### 2. Fill in `.env`
66
+ ### Step 2 Fill in `.env`
52
67
 
53
68
  ```env
54
69
  ATLAS_URI=mongodb+srv://user:pass@cluster0.xxxxx.mongodb.net/
@@ -56,234 +71,402 @@ LOCAL_URI=mongodb://127.0.0.1:27017
56
71
  DB_NAME=myapp
57
72
  ```
58
73
 
59
- ### 3. Start sync in your app
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:**
79
+
80
+ ```js
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);
100
+ ```
60
101
 
61
102
  ```js
62
103
  // CommonJS
63
- const mongofire = require('mongofire');
64
- const config = require('./mongofire.config');
104
+ const express = require('express');
105
+ const { startApp } = require('./mongofire');
65
106
 
66
- await mongofire.start(config);
107
+ const app = express();
108
+ app.use(express.json());
67
109
 
68
- // ESM
69
- import mongofire from 'mongofire';
70
- import config from './mongofire.config.js';
110
+ app.use('/auth', require('./routes/auth.routes'));
71
111
 
72
- await mongofire.start(config);
112
+ startApp(app, process.env.PORT || 3000);
73
113
  ```
74
114
 
75
- ### 4. Add the plugin to your Mongoose schema
115
+ **`models/User.js` attach the plugin to your Mongoose schemas:**
76
116
 
77
117
  ```js
78
- 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
79
121
 
80
122
  const UserSchema = new mongoose.Schema({
81
- name: String,
82
- email: String,
83
- userId: mongoose.Types.ObjectId,
123
+ name: String,
124
+ email: { type: String, unique: true },
125
+ updatedAt: Date,
84
126
  });
85
127
 
86
- // Track changes on the 'users' collection
87
- // ownerField: field used for per-user data isolation (multi-tenant)
88
- UserSchema.plugin(mongofire.plugin('users', { ownerField: 'userId' }));
128
+ UserSchema.plugin(plugin('users')); // collection name must match mongofire.config.js
89
129
 
90
- const User = mongoose.model('User', UserSchema);
130
+ export default mongoose.model('User', UserSchema);
91
131
  ```
92
132
 
133
+ ```js
134
+ // CommonJS — from a file inside models/
135
+ const mongoose = require('mongoose');
136
+ const { plugin } = require('../mongofire'); // ← note: ../ one level up
137
+
138
+ const StudentSchema = new mongoose.Schema({
139
+ name: String,
140
+ grade: Number,
141
+ updatedAt: Date,
142
+ });
143
+
144
+ StudentSchema.plugin(plugin('students'));
145
+
146
+ module.exports = mongoose.model('Student', StudentSchema);
147
+ ```
148
+
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'`
153
+
93
154
  ---
94
155
 
95
- ## Config Options
96
-
97
- | Option | Type | Default | Description |
98
- |---|---|---|---|
99
- | `collections` | `string[]` | *required* | Collection names to sync |
100
- | `localUri` | `string` | `mongodb://localhost:27017` | Local MongoDB URI |
101
- | `atlasUri` | `string` | `null` | Atlas connection string |
102
- | `dbName` | `string` | `'mongofire'` | Database name |
103
- | `syncInterval` | `number` | `30000` (polling) / `5000` (realtime) | Polling interval in ms |
104
- | `batchSize` | `number` | `200` | Docs per upload/download batch |
105
- | `syncOwner` | `string\|fn` | `'*'` | Owner key for multi-tenant filtering. If a function, throwing will **abort** the sync to prevent unintended data access |
106
- | `realtime` | `boolean` | `false` | Use Atlas Change Streams for instant sync |
107
- | `onSync` | `function` | `null` | Called after each sync cycle |
108
- | `onError` | `function` | `null` | Called on sync errors |
156
+ ## Project folder structure
157
+
158
+ ```
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
+ ```
109
171
 
110
172
  ---
111
173
 
112
- ## Events
174
+ ## What `startApp()` does
113
175
 
114
- ```js
115
- mongofire.on('ready', () => console.log('MongoFire started'));
116
- mongofire.on('online', () => console.log('Atlas connected'));
117
- mongofire.on('offline', () => console.log('Working locally'));
118
- mongofire.on('sync', (r) => console.log('Sync result:', r));
119
- mongofire.on('conflict', (c) => console.warn('Conflict detected:', c));
120
- mongofire.on('realtimeStarted', () => console.log('Change streams active'));
121
- mongofire.on('stopped', () => console.log('Shut down cleanly'));
122
- mongofire.on('error', (e) => console.error('Sync error:', e));
176
+ `startApp(app, port)` replaces `app.listen()`. It:
177
+
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
+
183
+ **Console output on startup:**
184
+
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
123
190
  ```
124
191
 
125
- ### `conflict` event
192
+ ---
126
193
 
127
- Emitted when a local write conflicts with a concurrent remote change. Use this to notify users or trigger a re-fetch:
194
+ ## Config Options (`mongofire.config.js`)
128
195
 
129
196
  ```js
130
- mongofire.on('conflict', ({ collection, docId, localVersion, remoteVersion, op }) => {
131
- console.warn(`Conflict on ${collection}/${docId}`);
132
- console.warn(` Local was at version ${localVersion}, Atlas is at ${remoteVersion}`);
133
- // Fetch the latest remote doc and re-apply your changes if needed
134
- });
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',
201
+
202
+ collections: ['users', 'students', 'orders'], // every collection your app uses
203
+
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
+ };
135
216
  ```
136
217
 
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 |
232
+
137
233
  ---
138
234
 
139
- ## API
235
+ ## Common import mistakes
140
236
 
141
- ### `mongofire.start(config)` `Promise<MongoFire>`
142
- Connect and start background sync. Concurrent calls are safe — all await the same init.
237
+ ### Wrong — using the npm package name
143
238
 
144
- ### `mongofire.stop(timeoutMs?)` → `Promise<void>`
145
- Flush pending ops, wait for active sync, close all connections. Default timeout: 10 seconds.
239
+ ```js
240
+ import { plugin } from 'mongofire'; // ERR_MODULE_NOT_FOUND
241
+ const { plugin } = require('mongofire'); // ❌ wrong unless mongofire is installed globally
242
+ ```
146
243
 
147
- ### `mongofire.sync(type?)` → `Promise<SyncResult>`
148
- Manually trigger a sync. `type` can be `'required'` (default) or `'all'`. Rapid successive calls are throttled automatically.
244
+ ### ✅ Correct — relative path to your local mongofire.js
149
245
 
150
- ### `mongofire.status()` → `Promise<SyncStatus>`
151
- Get pending op counts and online/realtime status.
246
+ ```js
247
+ // server.js (same folder as mongofire.js)
248
+ import { startApp, plugin } from './mongofire.js';
152
249
 
153
- ### `mongofire.clean(days?)` `Promise<number>`
154
- Delete old sync records older than `days` days (default: **7**). Returns count of deleted records.
250
+ // models/User.js (one folder deep)
251
+ import { plugin } from '../mongofire.js';
155
252
 
156
- ### `mongofire.plugin(collectionName, options?)`
157
- Returns a Mongoose schema plugin. Options:
253
+ // routes/user.routes.js (one folder deep)
254
+ import { plugin } from '../mongofire.js';
255
+ ```
158
256
 
159
- | Option | Type | Default | Description |
160
- |---|---|---|---|
161
- | `ownerField` | `string` | `null` | Dot-path to owner key field (e.g. `'userId'` or `'org._id'`) |
162
- | `batchSize` | `number` | `200` | Batch size for bulk operations |
163
- | `concurrency` | `number` | `8` | Concurrent change tracking calls per batch |
257
+ ---
258
+
259
+ ## Events
260
+
261
+ ```js
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'));
271
+ ```
272
+
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 |
164
284
 
165
285
  ---
166
286
 
167
- ## Using the plugin directly
287
+ ## API Reference
168
288
 
169
- If you prefer not to use the MongoFire singleton, import the plugin directly:
289
+ ### `startApp(app, port)` `Promise<http.Server>`
170
290
 
171
- ```js
172
- // Raw Mongoose plugin
173
- const mongofirePlugin = require('mongofire/plugin');
174
- UserSchema.plugin(mongofirePlugin, { collection: 'users', ownerField: 'userId' });
291
+ Replaces `app.listen()`. Waits for local DB, then opens the server port. Exits with code 1 on DB failure.
175
292
 
176
- // Or use the factory helper (same signature as mongofire.plugin())
177
- const { factory } = require('mongofire/plugin');
178
- UserSchema.plugin(factory('users', { ownerField: 'userId' }));
293
+ ```js
294
+ import { startApp } from './mongofire.js';
295
+ startApp(app, process.env.PORT || 3000);
179
296
  ```
180
297
 
181
- ---
298
+ ### `plugin(collectionName, options?)` → Mongoose plugin function
182
299
 
183
- ## Real-Time Sync
300
+ Attaches change-tracking to a schema. Apply **before** `mongoose.model()`.
301
+
302
+ ```js
303
+ import { plugin } from '../mongofire.js';
304
+ UserSchema.plugin(plugin('users'));
305
+ UserSchema.plugin(plugin('users', { ownerField: 'userId' })); // multi-tenant
306
+ ```
184
307
 
185
- Enable instant sync via MongoDB Atlas Change Streams:
308
+ ### `localReady` `Promise<Db>`
309
+
310
+ Resolves as soon as local MongoDB is connected, before Atlas.
186
311
 
187
312
  ```js
188
- await mongofire.start({
189
- // ...
190
- realtime: true, // requires Atlas cluster or local replica set
191
- });
313
+ import { localReady } from './mongofire.js';
314
+ await localReady; // DB is guaranteed ready after this
192
315
  ```
193
316
 
194
- If Change Streams are unavailable, MongoFire automatically falls back to polling — no crash, no config needed.
317
+ ### `ready` `Promise<MongoFire>`
318
+
319
+ Resolves after Atlas connect and the first sync.
320
+
321
+ ### `mongofire.sync(type?)` → `Promise<SyncResult>`
322
+
323
+ Manually trigger a sync (`'required'` or `'all'`).
324
+
325
+ ### `mongofire.status()` → `Promise<SyncStatus>`
326
+
327
+ Returns `{ online, pending, creates, updates, deletes, realtime }`.
328
+
329
+ ### `mongofire.conflicts()` / `retryConflict(opId)` / `dismissConflict(opId)`
330
+
331
+ View and resolve sync conflicts.
332
+
333
+ ### `mongofire.reconcile(opts?)` → `Promise<ReconcileResult[]>`
334
+
335
+ Scan for and recover writes lost in a crash.
336
+
337
+ ### `mongofire.resetLocal()` → `Promise<{ dropped: number }>`
338
+
339
+ Wipe the local DB. Next startup re-bootstraps from Atlas. **Unsynced changes are lost.**
340
+
341
+ ### `mongofire.stop(timeoutMs?)` → `Promise<void>`
342
+
343
+ Flush in-flight ops and close connections. Called automatically on `SIGINT`/`SIGTERM`.
195
344
 
196
345
  ---
197
346
 
198
- ## Multi-Tenant Usage
347
+ ## Real-Time Sync
199
348
 
200
349
  ```js
201
- // Sync only data belonging to a specific user
202
- await mongofire.start({
203
- collections: ['notes', 'tasks'],
204
- syncOwner: () => currentUser.id, // dynamic — re-evaluated on each sync cycle
205
- // ...
206
- });
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
+ };
207
357
  ```
208
358
 
209
- > **Note:** If `syncOwner` throws, the sync is **aborted** and an `error` event is emitted. This prevents unintended access to other users' data.
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).
210
360
 
211
361
  ---
212
362
 
213
- ## CLI
363
+ ## Multi-Tenant
214
364
 
215
- ```bash
216
- # Create config files in current project
217
- npx mongofire init
365
+ For apps where each user must only sync their own data:
218
366
 
219
- # Force overwrite existing config
220
- npx mongofire init --force
367
+ ```js
368
+ // mongofire.config.js
369
+ export default {
370
+ collections: ['notes'],
371
+ syncOwner: () => currentUserId, // returns the current user's ID
372
+ };
373
+ ```
221
374
 
222
- # Check pending sync status
223
- npx mongofire status
375
+ ```js
376
+ // model
377
+ NoteSchema.plugin(plugin('notes', { ownerField: 'userId' }));
378
+ ```
224
379
 
225
- # Delete old sync records
226
- npx mongofire clean
227
- npx mongofire clean --days=7
380
+ ```js
381
+ // create — always set the owner field
382
+ await Note.create({ title: 'My note', userId: req.user._id });
228
383
  ```
229
384
 
230
385
  ---
231
386
 
232
- ## TypeScript
387
+ ## CLI Reference
233
388
 
234
- Full TypeScript support is included:
389
+ ```bash
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
401
+ ```
235
402
 
236
- ```ts
237
- import mongofire, { MongoFire, SyncConfig, SyncResult, ConflictData } from 'mongofire';
403
+ > Set `MONGOFIRE_DEBUG=1` for full error stack traces.
238
404
 
239
- const config: SyncConfig = {
240
- collections: ['users'],
241
- atlasUri: process.env.ATLAS_URI,
242
- realtime: true,
243
- };
405
+ ---
244
406
 
245
- await mongofire.start(config);
407
+ ## TypeScript
246
408
 
247
- mongofire.on('sync', (result: SyncResult) => {
248
- console.log(`Uploaded: ${result.uploaded}, Downloaded: ${result.downloaded}`);
249
- });
409
+ ```ts
410
+ import { startApp, plugin, mongofire } from './mongofire.js';
411
+ import type { SyncResult, ConflictData } from 'mongofire';
412
+
413
+ UserSchema.plugin(plugin('users'));
414
+ startApp(app, 3000);
250
415
 
251
- mongofire.on('conflict', (data: ConflictData) => {
252
- console.warn(`Conflict on ${data.collection}/${data.docId}`);
416
+ mongofire.on('sync', (result: SyncResult) => {
417
+ console.log(`↑${result.uploaded} ↓${result.downloaded}`);
253
418
  });
254
419
  ```
255
420
 
256
421
  ---
257
422
 
258
- ## How it Works
423
+ ## Environment Variables
259
424
 
260
- 1. **Change Tracking** — Every `save`/`update`/`delete` via Mongoose hooks is recorded locally before syncing to Atlas
261
- 2. **Upload** — Pending local changes are uploaded to Atlas inside MongoDB transactions, with automatic retry and idempotency
262
- 3. **Download (Bootstrap)** First sync streams all remote docs in batches. Resumable — a crash mid-bootstrap picks up from where it left off
263
- 4. **Download (Delta)** Subsequent syncs fetch only changes newer than the last seen position, with no gaps even at millisecond boundaries
264
- 5. **Conflict Resolution** Version number → timestamp → deviceId tiebreaker (deterministic, no coin flip)
265
- 6. **Offline** All reads/writes work locally. Changes queue up and upload automatically when Atlas reconnects
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 |
266
434
 
267
435
  ---
268
436
 
269
- ## Collection Name Rules
437
+ ## Troubleshooting
270
438
 
271
- Collection names passed to `mongofire.plugin()` and `config.collections` must:
272
- - Start with a letter or digit
273
- - Contain only letters, digits, underscores (`_`), hyphens (`-`), or dots (`.`)
274
- - **Not** contain a colon (`:`)
275
- - **Not** start with `_mf_` (reserved for MongoFire internal use)
439
+ ### `ERR_MODULE_NOT_FOUND: Cannot find package 'mongofire'`
276
440
 
277
- Invalid names throw a clear error at startup.
441
+ You are importing `from 'mongofire'` instead of `from './mongofire.js'`.
278
442
 
279
- ---
443
+ ```js
444
+ // ❌ Wrong
445
+ import { plugin } from 'mongofire';
280
446
 
281
- ## Environment Variables
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
464
+
465
+ MongoFire tries to auto-spawn `mongod`. Set `MONGOFIRE_DBPATH` to a writable directory if the default fails.
466
+
467
+ ### `Cannot find module './mongofire.config.js'`
282
468
 
283
- | Variable | Default | Description |
284
- |---|---|---|
285
- | `MONGOFIRE_VERIFY_REMOTE` | `0` | Set to `1` to verify each uploaded doc with a checksum round-trip |
286
- | `MONGOFIRE_COLLECTION_CONCURRENCY` | `4` | Number of collections synced in parallel |
469
+ Run `npx mongofire init` to regenerate the config file.
287
470
 
288
471
  ---
289
472