mongofire 6.5.3 → 6.5.5

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
+ > Automatic conflict resolution, Mongoose plugin, interactive CLI, 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,18 @@
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 everything else 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
+ - **Offline-first** your app never waits for the network
17
+ - **Automatic sync** uploads local changes and downloads remote ones on a configurable interval
18
+ - **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
23
+ - **TypeScript** — full type declarations included
21
24
 
22
25
  ---
23
26
 
@@ -27,26 +30,27 @@ MongoFire keeps a **local MongoDB** and **MongoDB Atlas** in sync automatically.
27
30
  npm install mongofire
28
31
  ```
29
32
 
30
- **Peer dependencies** (install the ones you use):
33
+ **Peer dependencies:**
31
34
 
32
35
  ```bash
33
- npm install mongodb mongoose
36
+ npm install mongodb mongoose dotenv
34
37
  ```
35
38
 
36
39
  ---
37
40
 
38
41
  ## Quick Start
39
42
 
40
- ### 1. Init config files
43
+ ### 1. Run the setup wizard
41
44
 
42
45
  ```bash
43
46
  npx mongofire init
44
47
  ```
45
48
 
46
- This creates:
49
+ The interactive wizard creates three files:
50
+
47
51
  - `.env` — MongoDB connection strings
48
- - `mongofire.config.js` — which collections to sync
49
- - `mongofire.js` — app entry point
52
+ - `mongofire.config.js` — which collections to sync, intervals, and options
53
+ - `mongofire.js` — imports config and starts sync
50
54
 
51
55
  ### 2. Fill in `.env`
52
56
 
@@ -56,234 +60,459 @@ LOCAL_URI=mongodb://127.0.0.1:27017
56
60
  DB_NAME=myapp
57
61
  ```
58
62
 
59
- ### 3. Start sync in your app
63
+ ### 3. Import mongofire.js in your app entry point
60
64
 
61
65
  ```js
62
66
  // CommonJS
63
- const mongofire = require('mongofire');
64
- const config = require('./mongofire.config');
65
-
66
- await mongofire.start(config);
67
+ require("./mongofire");
68
+ const mongofire = require("mongofire");
69
+ ```
67
70
 
71
+ ```js
68
72
  // ESM
69
- import mongofire from 'mongofire';
70
- import config from './mongofire.config.js';
71
-
72
- await mongofire.start(config);
73
+ import "./mongofire.js";
74
+ import mongofire from "mongofire";
73
75
  ```
74
76
 
75
- ### 4. Add the plugin to your Mongoose schema
77
+ ### 4. Add the plugin to your Mongoose schemas
76
78
 
77
79
  ```js
78
- const mongofire = require('mongofire');
80
+ const mongofire = require("mongofire");
79
81
 
80
- const UserSchema = new mongoose.Schema({
81
- name: String,
82
- email: String,
83
- userId: mongoose.Types.ObjectId,
82
+ const OrderSchema = new mongoose.Schema({
83
+ items: Array,
84
+ total: Number,
85
+ updatedAt: Date,
84
86
  });
85
87
 
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' }));
88
+ OrderSchema.plugin(mongofire.plugin("orders")); // <— must be BEFORE creating the model
89
89
 
90
- const User = mongoose.model('User', UserSchema);
90
+ const Order = mongoose.model("Order", OrderSchema);
91
91
  ```
92
92
 
93
+ Every `save()`, `create()`, `update()`, and `delete()` is now tracked and synced automatically.
94
+
93
95
  ---
94
96
 
95
97
  ## Config Options
96
98
 
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 |
99
+ ```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",
105
+
106
+ collections: ["orders", "products", "users"],
107
+
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
112
+
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
+ };
122
+ ```
123
+
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 |
109
140
 
110
141
  ---
111
142
 
112
143
  ## Events
113
144
 
114
145
  ```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));
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));
123
159
  ```
124
160
 
125
- ### `conflict` event
126
-
127
- Emitted when a local write conflicts with a concurrent remote change. Use this to notify users or trigger a re-fetch:
128
-
129
- ```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
- });
135
- ```
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 |
136
174
 
137
175
  ---
138
176
 
139
177
  ## API
140
178
 
141
179
  ### `mongofire.start(config)` → `Promise<MongoFire>`
142
- Connect and start background sync. Concurrent calls are safe — all await the same init.
180
+
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.
143
182
 
144
183
  ### `mongofire.stop(timeoutMs?)` → `Promise<void>`
145
- Flush pending ops, wait for active sync, close all connections. Default timeout: 10 seconds.
184
+
185
+ Flush any in-flight operations and close all connections. Default timeout: **10,000 ms**.
146
186
 
147
187
  ### `mongofire.sync(type?)` → `Promise<SyncResult>`
148
- Manually trigger a sync. `type` can be `'required'` (default) or `'all'`. Rapid successive calls are throttled automatically.
188
+
189
+ Manually trigger a sync. Returns `{ error: 'offline', pending }` when Atlas is unreachable. Throttled to a minimum of 500 ms between calls.
190
+
191
+ | `type` | Behaviour |
192
+ | ------------ | --------------------------------------------------- |
193
+ | `'required'` | Upload pending ops + download new changes (default) |
194
+ | `'all'` | Full bi-directional sync |
149
195
 
150
196
  ### `mongofire.status()` → `Promise<SyncStatus>`
151
- Get pending op counts and online/realtime status.
152
197
 
153
- ### `mongofire.clean(days?)` → `Promise<number>`
154
- Delete old sync records older than `days` days (default: **7**). Returns count of deleted records.
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
+ }
207
+ ```
155
208
 
156
- ### `mongofire.plugin(collectionName, options?)`
157
- Returns a Mongoose schema plugin. Options:
209
+ ### `mongofire.clean(days?, opts?)` → `Promise<number>`
158
210
 
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 |
211
+ Delete old synced records to keep the local database tidy.
164
212
 
165
- ---
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 |
166
217
 
167
- ## Using the plugin directly
218
+ ### `mongofire.conflicts(collection?)` `Promise<ConflictRecord[]>`
168
219
 
169
- If you prefer not to use the MongoFire singleton, import the plugin directly:
220
+ ```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
+ }
226
+ ```
227
+
228
+ ### `mongofire.retryConflict(opId)` → `Promise<void>`
229
+
230
+ Reset a conflict back to pending so the next sync retries it. Emits `conflictResolved` with `resolution: 'retried'`.
231
+
232
+ ### `mongofire.dismissConflict(opId)` → `Promise<void>`
233
+
234
+ Dismiss a conflict — remote version wins and the local change is discarded. Emits `conflictResolved` with `resolution: 'dismissed'`.
235
+
236
+ ### `mongofire.reconcile(collectionOrOpts?, opts?)` → `Promise<ReconcileResult[]>`
237
+
238
+ Scan for writes lost in a crash and re-queue them for sync.
170
239
 
171
240
  ```js
172
- // Raw Mongoose plugin
173
- const mongofirePlugin = require('mongofire/plugin');
174
- UserSchema.plugin(mongofirePlugin, { collection: 'users', ownerField: 'userId' });
241
+ await mongofire.reconcile(); // all collections
242
+ await mongofire.reconcile({ fullScan: false }); // fast scan only
243
+ await mongofire.reconcile("orders"); // single collection
244
+ ```
175
245
 
176
- // Or use the factory helper (same signature as mongofire.plugin())
177
- const { factory } = require('mongofire/plugin');
178
- UserSchema.plugin(factory('users', { ownerField: 'userId' }));
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) |
250
+
251
+ ### `mongofire.resetLocal()` → `Promise<{ dropped: number }>`
252
+
253
+ Safely wipe the entire local database and all MongoFire state. The next `start()` re-bootstraps from Atlas cleanly.
254
+
255
+ ```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
+ );
179
266
  ```
180
267
 
268
+ > **Warning:** Any unsynced local changes are permanently lost. Use `mongofire.status()` first if you need to verify there is nothing pending.
269
+
270
+ ### `mongofire.plugin(collectionName, options?)`
271
+
272
+ ```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
+ );
284
+ ```
285
+
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 |
291
+
181
292
  ---
182
293
 
183
294
  ## Real-Time Sync
184
295
 
185
- Enable instant sync via MongoDB Atlas Change Streams:
296
+ Enable Atlas Change Streams for near-instant propagation between devices:
186
297
 
187
298
  ```js
188
299
  await mongofire.start({
189
- // ...
190
- realtime: true, // requires Atlas cluster or local replica set
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
191
304
  });
305
+
306
+ mongofire.on("realtimeStarted", () => console.log("Changes appear instantly"));
192
307
  ```
193
308
 
194
- If Change Streams are unavailable, MongoFire automatically falls back to polling no crash, no config needed.
309
+ Falls back to polling automatically if Change Streams are unavailable.
195
310
 
196
311
  ---
197
312
 
198
- ## Multi-Tenant Usage
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**.
319
+
320
+ ### Do you need it?
321
+
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** |
330
+
331
+ ### Setup (4 steps)
332
+
333
+ **Step 1 — Add an owner field to every synced model**
199
334
 
200
335
  ```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
336
+ const OrderSchema = new mongoose.Schema({
337
+ items: Array,
338
+ total: Number,
339
+ userId: { type: mongoose.Types.ObjectId, required: true },
340
+ updatedAt: Date,
341
+ });
342
+
343
+ OrderSchema.plugin(mongofire.plugin("orders", { ownerField: "userId" }));
344
+ ```
345
+
346
+ **Step 2 — Set `syncOwner` in config**
347
+
348
+ ```js
349
+ module.exports = {
350
+ collections: ["orders", "products"],
351
+ syncOwner: "userId",
205
352
  // ...
353
+ };
354
+ ```
355
+
356
+ **Step 3 — Pass the current user's ID when starting sync**
357
+
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
+ ```
376
+
377
+ **Step 4 — Always set the owner field when creating documents**
378
+
379
+ ```js
380
+ const order = await Order.create({
381
+ items: req.body.items,
382
+ total: req.body.total,
383
+ userId: req.user._id,
206
384
  });
207
385
  ```
208
386
 
209
- > **Note:** If `syncOwner` throws, the sync is **aborted** and an `error` event is emitted. This prevents unintended access to other users' data.
387
+ ### Dynamic owner using a function
388
+
389
+ ```js
390
+ await mongofire.start({
391
+ ...config,
392
+ syncOwner: () => getCurrentUser()?.id ?? null,
393
+ });
394
+ ```
395
+
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.
397
+
398
+ ---
399
+
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" });
406
+
407
+ // CommonJS factory
408
+ const { factory } = require("mongofire/plugin");
409
+ OrderSchema.plugin(factory("orders"));
410
+ ```
411
+
412
+ ```js
413
+ // ESM
414
+ import mongofirePlugin, { factory } from "mongofire/plugin";
415
+ OrderSchema.plugin(factory("orders"));
416
+ ```
210
417
 
211
418
  ---
212
419
 
213
- ## CLI
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:
214
425
 
215
426
  ```bash
216
- # Create config files in current project
217
- npx mongofire init
427
+ # Interactive CLI confirms before wiping
428
+ npx mongofire reset-local
429
+ ```
218
430
 
219
- # Force overwrite existing config
220
- npx mongofire init --force
431
+ ```js
432
+ // Programmatic
433
+ const { dropped } = await mongofire.resetLocal();
434
+ ```
221
435
 
222
- # Check pending sync status
223
- npx mongofire status
436
+ Both drop all local data and MongoFire state so the next startup re-bootstraps from Atlas cleanly.
224
437
 
225
- # Delete old sync records
226
- npx mongofire clean
227
- npx mongofire clean --days=7
438
+ ---
439
+
440
+ ## CLI Reference
441
+
442
+ ```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
228
456
  ```
229
457
 
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.
469
+
230
470
  ---
231
471
 
232
472
  ## TypeScript
233
473
 
234
- Full TypeScript support is included:
235
-
236
474
  ```ts
237
- import mongofire, { MongoFire, SyncConfig, SyncResult, ConflictData } from 'mongofire';
475
+ import mongofire, { SyncConfig, SyncResult, ConflictData } from "mongofire";
238
476
 
239
477
  const config: SyncConfig = {
240
- collections: ['users'],
478
+ collections: ["orders", "products"],
241
479
  atlasUri: process.env.ATLAS_URI,
242
480
  realtime: true,
243
481
  };
244
482
 
245
483
  await mongofire.start(config);
246
484
 
247
- mongofire.on('sync', (result: SyncResult) => {
248
- console.log(`Uploaded: ${result.uploaded}, Downloaded: ${result.downloaded}`);
485
+ mongofire.on("sync", (result: SyncResult) => {
486
+ console.log(`up:${result.uploaded} down:${result.downloaded}`);
249
487
  });
250
488
 
251
- mongofire.on('conflict', (data: ConflictData) => {
252
- console.warn(`Conflict on ${data.collection}/${data.docId}`);
489
+ mongofire.on("conflict", (c: ConflictData) => {
490
+ console.warn(`Conflict: ${c.collection}/${c.docId} op:${c.op}`);
253
491
  });
254
492
  ```
255
493
 
256
494
  ---
257
495
 
258
- ## How it Works
496
+ ## Environment Variables
259
497
 
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
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) |
266
503
 
267
504
  ---
268
505
 
269
506
  ## Collection Name Rules
270
507
 
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)
276
-
277
- Invalid names throw a clear error at startup.
278
-
279
- ---
508
+ Names must:
280
509
 
281
- ## Environment Variables
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
282
514
 
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 |
515
+ Invalid names are rejected at startup with a clear error message.
287
516
 
288
517
  ---
289
518