npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@semics-tech/mongolite

v1.4.0

Published

A MongoDB-like client using SQLite as a persistent store, written in TypeScript.

Readme

MongoLite

CI NPM version Codecov License: MIT

A MongoDB-like client backed by SQLite. Use a familiar MongoDB API with the simplicity of a local file-based database — no server required.

Why MongoLite?

  • 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

Features

  • MongoDB-compatible API — insertOne, findOne, updateOne, deleteOne, find, aggregate, and more
  • SQLite persistence — single file, zero configuration, works offline
  • Automatic _id generation — 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

Installation

npm install @semics-tech/mongolite

Requires 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.

Quick Start

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();

Explore a database from the terminal

npx mongolite

Picks 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.

How does it compare?

| | MongoLite | lowdb | better-sqlite3 (raw) | NeDB | PouchDB | MongoDB | | -------------------------------------------------------- | ----------- | ------------------------ | -------------------- | -------------------- | ------------------------ | ------- | | MongoDB query API ($set, $elemMatch, aggregation...) | ✅ | ❌ (plain object access) | ❌ (raw SQL) | ✅ | ❌ (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 | ✅ | ✅ | ✅ | ⚠️ (community types) | ⚠️ (community types) | ✅ | | Actively maintained | ✅ | ✅ | ✅ | ❌ (unmaintained) | ⚠️ (slow-moving) | ✅ |

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.

Documentation

| 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 |

Backend Examples

SQLite file (Node.js / Bun)

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();

Connection options

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,
});

Sharing one connection across a process

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 it

Connections 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.

In-memory (tests / ephemeral)

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();

better-sqlite3 (optional, for Node.js <22.5 or Bun)

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-sqlite3
import { 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-sqlite3 is the older, more battle-tested native addon — reach for it if you're stuck on Node.js <22.5, targeting Bun without a node:sqlite polyfill, or want to avoid depending on an experimental Node.js API in production. Both implement the same IDatabaseAdapter interface, so switching between them is a one-line import change.

Browser (via sql.js)

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();

Cloudflare Durable Objects

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.

Syncing to MongoDB

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:[email protected]/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 shutdown

Changes 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.

Development

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 code

Contributing

Contributions are welcome! Please read CONTRIBUTING.md before submitting a pull request.

License

MIT