A MongoDB-like client backed by SQLite. Use a familiar MongoDB API with the simplicity of a local file-based database — no server required.
- You want a MongoDB-style API without running a MongoDB server
- You need a lightweight, embedded database for local apps, CLIs, or testing
- You want simple file-based persistence with zero infrastructure overhead
- You want a persistent, high-performance local cache in front of a remote MongoDB, one that survives restarts and network interruptions, without giving up Mongo's query semantics (including things that are natural in a document model but awkward in SQL, like layered/override queries)
- MongoDB-compatible API —
insertOne,findOne,updateOne,deleteOne,find,aggregate(a subset of stages:$match,$project,$addFields,$sort,$limit,$skip,$count,$group,$unwind), and more - SQLite persistence — single file, zero configuration, works offline
- Automatic
_idgeneration — UUID assigned on insert if not provided - WAL mode — Write-Ahead Logging for better concurrent read access, on by default
- Shared connections — opt into one pooled connection per file per process
- Rich query operators —
$eq,$gt,$in,$and,$or,$elemMatch,$regex, and more - Update operators —
$set,$inc,$push,$pull,$addToSet,$mul, and more - Indexing — create, list, and drop indexes including unique and compound indexes
- Change streams — real-time change tracking via
collection.watch() - MongoDB sync — replicate local writes upstream, surviving restarts and offline periods
- HTTP sync — post the same changes to a remote API when MongoDB is unreachable
- JSON safety — validates documents before insert, and a corrupted row can't break queries for the rest of the collection
- TypeScript — fully typed with strict mode
npm install @semics-tech/mongoliteRequires Node.js 22.5.0+ by default — the main entry point is backed by
Node's built-in node:sqlite module, so there's no native addon to install or
build. On an older Node.js runtime, use the better-sqlite3-backed adapter
instead; see Native node:sqlite vs. better-sqlite3 below.
Ships both ESM (import) and CommonJS (require) builds — use whichever your project already uses.
import { MongoLite } from '@semics-tech/mongolite';
async function main() {
const client = new MongoLite('./myapp.sqlite');
// Use ':memory:' for an ephemeral in-memory database
const users = client.collection('users');
// Insert
const result = await users.insertOne({ name: 'Alice', age: 30 });
// Find
const user = await users.findOne({ name: 'Alice' });
// Update
await users.updateOne({ name: 'Alice' }, { $set: { age: 31 } });
// Delete
await users.deleteOne({ name: 'Alice' });
await client.close();
}
main();npx mongolitePicks up the SQLite files around you, lists the collections it finds inside, and builds queries out of plain-English choices — "age is at least 30", "skills contains SQL" — using fields inferred from your actual documents. It prints the MongoDB filter it built alongside the results, and raw filters and raw SQL are a menu item away when you want them. See the CLI docs.
| MongoLite | lowdb | better-sqlite3 (raw) | NeDB | PouchDB | MongoDB | |
|---|---|---|---|---|---|---|
MongoDB-style query API ($set, $elemMatch, ...) |
✅ | ❌ (plain object access) | ❌ (raw SQL) | ✅ (no aggregation) | ❌ (Mango/CouchDB-style) | ✅ |
| Runs in the browser | ✅ (sql.js) | ✅ | ❌ (native binding) | ✅ | ✅ (IndexedDB) | ❌ |
| Runs on the edge (Cloudflare Durable Objects) | ✅ | ❌ | ❌ (native binding) | ❌ | ❌ | ❌ |
| Zero infrastructure (no server process) | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| TypeScript, strict mode | ✅ | ✅ | ✅ | ✅ | ||
| Actively maintained | ✅ | ✅ | ✅ | ✅ |
Comparison reflects our understanding of each project's documentation at the time of writing and may be out of date; check each project for current details. MongoLite supports a subset of MongoDB's aggregation stages and has no multi-document transaction API (API reference).
MongoLite's niche: the MongoDB query API you already know, running anywhere SQLite runs — a local file, an in-memory test database, the browser, or a Cloudflare Durable Object — without standing up a MongoDB server.
| Topic | Description |
|---|---|
| API Reference | Full API docs: methods, query operators, update operators |
| Change Streams | Real-time change tracking with collection.watch() |
| Syncing to MongoDB | Replication to an upstream MongoDB, directly or through an HTTP API |
| JSON Safety | Document validation and corrupted data recovery |
| CLI | npx mongolite — guided explorer for browsing and querying a database |
| Query Debugger | The older command-driven REPL (npx mongolite --repl) |
| Benchmarks | Performance benchmarks and storage characteristics |
| Cloudflare Durable Objects | Using MongoLite inside a Cloudflare Durable Object |
import { MongoLite } from '@semics-tech/mongolite';
const client = new MongoLite('./myapp.sqlite');
await client.connect();
const users = client.collection('users');
await users.insertOne({ name: 'Alice', age: 30 });
await client.close();const client = new MongoLite({
filePath: './myapp.sqlite',
WAL: true, // Write-Ahead Logging. Default: true
busyTimeout: 5000, // ms to wait for a lock before SQLITE_BUSY. Default: 5000
shared: true, // reuse one connection per file in this process. Default: false
readOnly: false,
verbose: false,
});Applications often end up with several clients pointing at the same database file
— a module-level client, a background job, a request handler — each opening its
own connection, and each then hand-rolling an instance cache to avoid the
resulting lock contention. shared: true moves that into the library:
// Both resolve to the same file, so they share a single SQLite connection.
const a = new MongoLite({ filePath: './app.sqlite', shared: true });
const b = new MongoLite({ filePath: './app.sqlite', shared: true });
await a.close(); // b keeps working — the handle is reference counted
await b.close(); // last holder out actually closes itConnections are only shared when the resolved path and every setting that
changes the handle's behaviour match. :memory: databases are never shared,
since two :memory: opens are two unrelated databases.
This dedupes connections within a process only. Separate processes each get
their own, so cross-process contention is handled by WAL mode and busyTimeout
instead.
import { MongoLite } from '@semics-tech/mongolite';
const client = new MongoLite(':memory:');
await client.connect();
const users = client.collection('users');
await users.insertOne({ name: 'Alice', age: 30 });
// Data is discarded when the process exits
await client.close();The default backend above uses Node's built-in node:sqlite module, which
requires Node.js 22.5.0+. If you're on an older Node.js runtime, or you
simply prefer the more battle-tested native addon, import from the
@semics-tech/mongolite/better-sqlite3 entry point instead. better-sqlite3 is an
optional dependency — install it yourself if it wasn't already pulled in:
npm install better-sqlite3import { MongoLite } from '@semics-tech/mongolite/better-sqlite3';
const client = new MongoLite('./myapp.sqlite');
await client.connect();
const users = client.collection('users');
await users.insertOne({ name: 'Alice', age: 30 });
await client.close();Which one should I use?
node:sqlite(the default) is still marked experimental upstream but requires no native build step, which is why it's the default here.better-sqlite3is the older, more battle-tested native addon — reach for it if you're stuck on Node.js <22.5, targeting Bun without anode:sqlitepolyfill, or want to avoid depending on an experimental Node.js API in production. Both implement the sameIDatabaseAdapterinterface, so switching between them is a one-line import change.
Requires sql.js (npm install sql.js).
import initSqlJs from 'sql.js';
import { MongoLite, BrowserSqliteAdapter } from '@semics-tech/mongolite';
const SQL = await initSqlJs({
locateFile: (file) => `https://cdn.jsdelivr.net/npm/sql.js/dist/${file}`,
});
const sqlJsDb = new SQL.Database(); // in-memory; use OPFS/IndexedDB for persistence
const client = new MongoLite(new BrowserSqliteAdapter(sqlJsDb));
await client.connect();
const users = client.collection('users');
await users.insertOne({ name: 'Alice', age: 30 });
console.log(await users.findOne({ name: 'Alice' }));
await client.close();import { DurableObject } from 'cloudflare:workers';
import { MongoLite, CloudflareDurableObjectAdapter } from '@semics-tech/mongolite/cloudflare';
export class MyDurableObject extends DurableObject {
private client: MongoLite;
constructor(ctx: DurableObjectState, env: Env) {
super(ctx, env);
// Pass ctx.storage.sql — no file path needed
this.client = new MongoLite(new CloudflareDurableObjectAdapter(ctx.storage.sql));
ctx.blockConcurrencyWhile(() => this.client.collection('users').ensureTable());
}
async fetch(request: Request) {
const users = this.client.collection('users');
await users.insertOne({ name: 'Alice', age: 30 });
return Response.json(await users.findOne({ name: 'Alice' }));
}
}See docs/CLOUDFLARE.md for the full guide, supported operations, and limitations.
Point MongoLite at an upstream MongoDB deployment and every insert, update and delete is replicated to it. The local database becomes a local-first write layer that converges upstream — through restarts, network outages and offline periods.
Requires the optional mongodb peer dependency (npm install mongodb).
const client = new MongoLite('./app.sqlite');
await client.connect();
const sync = client.syncToMongo({
connectionString: 'mongodb+srv://user:pass@cluster.example.com/app',
collections: ['users', 'orders'], // Omit to replicate everything
});
await sync.start();
// Ordinary writes — replication happens in the background.
await client.collection('users').insertOne({ name: 'Alice', age: 30 });
await sync.stop({ flush: true }); // Push anything still queued on shutdownChanges are captured by SQLite triggers into a durable outbox inside the same transaction as the write itself, so a change is either committed locally and queued for replication, or neither. A background worker coalesces the queue, applies it upstream, and only then advances a persisted checkpoint — so an outage parks the backlog rather than losing it, and a restart resumes exactly where it stopped.
Upstream is treated as the source of truth. Each push is a conditional write against the version it last saw, and carries only the fields that actually changed, so a concurrent edit by another writer is detected rather than silently overwritten.
Where the local instance cannot reach MongoDB directly, syncToHttp posts the same
operations to a remote API instead, and @semics-tech/mongolite/server applies them on
the far side:
const sync = client.syncToHttp({
baseUrl: 'https://api.example.com',
database: 'app',
getAuthHeaders: async () => ({ Authorization: `Bearer ${await getAccessToken()}` }),
});The version predicates travel with each operation and conflicts come back in the response, so the HTTP hop is pure transport — replication behaves the same either way.
See docs/SYNC.md for the full guide: syncing through an HTTP API, advanced authentication (X.509, mTLS, AWS IAM, Azure managed identity), filtering and reshaping documents, dead-letter handling, multiple upstreams, and custom sinks.
git clone https://github.com/semics-tech/mongolite.git
cd mongolite
npm install
npm test # Run tests
npm run build # Compile TypeScript
npm run lint # Lint codeContributions are welcome! Please read CONTRIBUTING.md before submitting a pull request.