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

@ameenahmed/nosqlite

v0.2.0

Published

MongoDB-inspired document database for Bun backed by SQLite JSONB, with BSON, aggregation, indexes, change streams, and a MongoDB command adapter.

Readme

NoSqlite

A MongoDB-inspired TypeScript document database for Bun, storing each collection in a SQLite table with JSONB document columns. It includes an embedded library and a standalone MongoDB command adapter tested with the official Node driver.

This release implements every feature area in the initial roadmap, with an explicit compatibility subset. It is not a full MongoDB replacement: operator coverage, query execution, text search, sessions, and deployment capabilities differ.

Install from npm

Requires Bun 1.4.2 or newer.

bun add @ameenahmed/nosqlite
import { NoSqlite } from '@ameenahmed/nosqlite';
import { serveMongo } from '@ameenahmed/nosqlite/wire';

Start from source

bun install
bun test
bun run typecheck
bun run example
bun run bench
import { NoSqlite, ObjectId } from "./src";

const db = new NoSqlite("app.db");
const users = db.collection<{ name: string; age: number; tags?: string[] }>(
  "users",
);

const { insertedId } = users.insertOne({ name: "Ada", age: 28, tags: ["bun"] });
users.updateOne(
  { _id: insertedId },
  { $inc: { age: 1 }, $push: { tags: "sqlite" } },
);

const results = users
  .find({ tags: "sqlite", age: { $gte: 18 } })
  .sort({ age: -1 })
  .skip(0)
  .limit(20)
  .project({ name: 1, _id: 0 })
  .toArray();

users.updateOne({ name: "Grace" }, { $set: { age: 35 } }, { upsert: true });
users.createIndex({ name: 1 }, { unique: true });

// Transactions run synchronously. Async callbacks are rejected.
db.transaction(() => {
  users.updateOne({ name: "Ada" }, { $inc: { age: 1 } });
  users.insertOne({ name: "Linus", age: 30 });
});

db.close();

IDs may be ObjectId, nonempty strings, or finite numbers. New documents receive an ObjectId by default. Existing documents keep their IDs. Methods are synchronous except change-stream consumption. The original prototype's string-ID schema upgrades automatically when a collection is opened; its old SQL expression indexes are dropped during that upgrade and should be recreated through createIndex.

Supported document API

| Area | Implemented | | ------------ | ------------------------------------------------------------------------------------------------------------------------ | | Reads | find, findOne, countDocuments, estimatedDocumentCount, distinct, explain | | Cursors | sort, skip, limit, project, toArray, synchronous streaming iteration; limit(0) means unlimited | | Inserts | insertOne, atomic insertMany | | Updates | updateOne, updateMany, replaceOne, upsert; findOneAndUpdate, findOneAndReplace, findOneAndDelete | | Bulk | bulkWrite: insert/update/replace/delete operations; ordered and unordered partial-success results via BulkWriteError | | Collections | createCollection, listCollections, collection, drop, setValidator | | Indexes | Single/compound indexes, multikey entries, uniqueness, sparse indexes, listIndexes, dropIndex; TTL and text indexes | | Transactions | Atomic synchronous callbacks, nested operations, rollback of documents/indexes/FTS/change events | | Changes | Collection/database watch, async iteration, next, tryNext, close, durable resume tokens |

Queries support equality, $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists, $all, $size, $elemMatch, $not, $regex, $options, $type, $mod, $and, $or, $nor, $expr, and indexed $text.

Dotted paths traverse embedded objects, numeric array indices, and arrays of documents. Scalar predicates match array elements. Null equality includes missing fields; $exists distinguishes them. Embedded-document equality preserves MongoDB’s field-order sensitivity. Query/projection/update paths reject prototype-related names and null bytes.

Updates support $set, $unset, $inc, $mul, $min, $max, $rename, $setOnInsert, $currentDate (dates), $push, $pull, $pullAll, $pop, and $addToSet. $push accepts $each, $position, $sort, and $slice. Positional $, $[], and $[identifier] with arrayFilters are supported. Positional $ requires a direct predicate on the target array or its dotted fields; complex logical positional predicates and positional upserts without an existing array are rejected. Conflicting paths and _id changes are rejected.

Projections support inclusion, exclusion, nested arrays, $slice, and $elemMatch. Inclusion and exclusion cannot be mixed except for _id. Computed projections are available through aggregation. Projected results use a general document type because fields, including _id, may be removed.

Aggregation

const totals = db
  .collection("orders")
  .aggregate([
    { $match: { total: { $gte: 10 } } },
    {
      $group: {
        _id: "$customer",
        total: { $sum: "$total" },
        count: { $sum: 1 },
      },
    },
    { $sort: { total: -1 } },
  ])
  .toArray();

Stages: $match, $project, $set, $addFields, $unset, $unwind, $group, $count, $lookup (equality join), $facet, $replaceRoot, $replaceWith, $sortByCount, $sample, $sort, $skip, $limit.

Group accumulators: $sum, $avg, $min, $max, $first, $last, $push, $addToSet, $count.

Expressions: field/variable references, $literal, $cond, $ifNull, $map, $filter, $add, $multiply, $subtract, $divide, $mod, comparison operators, $and, $or, $not, $in, $concat, $toLower, $toUpper, $toString, $size, $arrayElemAt, $concatArrays, $mergeObjects.

Aggregation runs in memory without disk spilling. Arithmetic/group numeric accumulators use JavaScript numbers; they do not retain arbitrary Long/Decimal128 precision. Numeric document updates preserve Long/Int32/Double types where possible and use decimal arithmetic for Decimal128. Unsupported stages/operators fail explicitly. There are no window functions, graph lookup, pipeline lookup, $out, $merge, or server-side JavaScript.

BSON and JSONB storage

Documents remain SQLite JSONB BLOBs. Normal JSON values retain their JSON representation. ObjectId, Date, Binary/UUID, Long, Int32, Double, Decimal128, Timestamp, BSONRegExp, MinKey/MaxKey and other BSON values use a collision-escaped typed envelope with canonical Extended JSON. Dates, regular expressions, and BSON classes reconstruct on read; Uint8Array inputs become BSON Binary. Plain user objects cannot impersonate typed envelopes.

undefined, functions, symbols, bigint primitives, cyclic objects, invalid dates, and nonfinite plain JS numbers are rejected. Use BSON Long/Decimal128 for extended numeric values. Documents are limited to 16 MiB of serialized BSON. db.sqlite exposes the database for inspection; direct SQL mutations bypass validation, secondary-index maintenance, FTS, and change recording.

Validation, search, and expiry

const profiles = db.collection("profiles", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["name"],
      properties: {
        name: { bsonType: "string", minLength: 1 },
        age: { bsonType: "number", minimum: 0 },
      },
    },
  },
});
profiles.insertOne({ name: "Ada", age: 28 });

const articles = db.collection("articles");
articles.createIndex({ title: "text", body: "text" });
articles.insertOne({ title: "SQLite", body: "Document storage with Bun" });
const matches = articles
  .find({ $text: { $search: 'SQLite "document storage"' } })
  .toArray();

const sessions = db.collection("sessions");
sessions.createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
sessions.insertOne({ expiresAt: new Date(Date.now() + 60_000) });
// Optional explicit sweep, useful in tests and jobs:
db.sweepTTL();

Validators may combine supported query predicates with $jsonSchema. Schema keywords: bsonType, type, required, properties, additionalProperties (boolean), items, enum, minimum, maximum, minLength, maxLength, pattern, minItems, maxItems, description, title. Validation applies to inserts, updates, replacements, and upserts. Updating a validator does not revalidate existing rows. Unsupported schema keywords fail explicitly; warning/moderate validation modes are not implemented.

Text search uses SQLite FTS5 with unicode61 tokenization. Search terms use OR semantics, quoted phrases and excluded -terms are supported. One text index per collection, optionally on $**. There is no MongoDB language stemming, ranking projection, weights, language override, or case/diacritic options.

TTL requires a single-field date index; arrays of dates expire at their earliest qualifying date. Expiry runs every 60 seconds by default and records deletes in change streams. Configure new NoSqlite(file, { ttlIntervalMS: 0 }) to disable automatic sweeps; lastTTLError records a background failure. TTL index metadata persists across reopen.

Unique indexes enforce duplicate null/missing keys, unlike the initial SQLite-only prototype. Sparse indexes omit documents missing all indexed fields. Array indexes deduplicate repeated elements within a document and reject parallel arrays in compound indexes. This implementation also rejects compound paths traversing multiple array-valued fields, including some same-array combinations MongoDB permits. Empty-array index semantics and BSON sort ordering have remaining differences from MongoDB. There are no collation, partial, geospatial, hashed, or wildcard query indexes.

Change streams

const stream = users.watch([{ $match: { operationType: "insert" } }]);
users.insertOne({ name: "New user", age: 20 });
const change = await stream.next();
const token = stream.resumeToken;
stream.close();

// Works after closing/reopening the database too:
const resumed = users.watch([], { resumeAfter: token });
resumed.close();

Events and resume tokens persist in a SQLite change log. Writes record insert/update/replace/delete events in the same transaction; rolled-back writes produce no events. Change-stream pipelines support $match, $project, $set, $addFields, $unset. pollIntervalMS defaults to 50. Closing a database closes its streams.

fullDocument is the document snapshot at the write, including for updates; it is not a later lookup of the document's current state. Streams do not require a replica set. They do not emit DDL/invalidate events or implement preimage options, cluster timestamps, or automatic log retention. The change log grows until the database is managed externally; writes through other NoSqlite instances are visible through polling, while raw SQL writes are not captured.

MongoDB command adapter

bun run server

The example uses nosqlite.db, database name nosqlite, and loopback port 27017. Configure NOSQLITE_FILE or NOSQLITE_PORT for the example.

import { MongoClient } from "mongodb";

const client = new MongoClient(
  "mongodb://127.0.0.1:27017/nosqlite?directConnection=true",
);
await client.connect();
const users = client.db("nosqlite").collection("users");
await users.insertOne({ name: "Ada" });
console.log(await users.find({ name: "Ada" }).toArray());
await client.close();

For programmatic startup, import serveMongo from ./src/wire (package subpath @ameenahmed/nosqlite/wire) and pass an existing NoSqlite. serveMongo(db, { port: 0 }) selects an ephemeral port and returns the connection URL, port, and stop().

The adapter implements OP_MSG plus the legacy handshake, BSON document sequences, fragmented frames, checksums, unacknowledged writes, cursor batches, getMore, killCursors, and bounded output buffering. Commands: hello/isMaster, ping, buildInfo, insert, update, delete, find, aggregate (including collection change streams), count, distinct, findAndModify, create, collMod, drop, createIndexes, listIndexes, dropIndexes, listCollections, explain (find), endSessions.

It hosts one configured database, defaults to loopback, and has no authentication or TLS. Compression, session transactions, retryable writes, replica sets, sharding, and the remaining MongoDB commands/options are unsupported. Unsupported options generally return errors rather than being silently ignored. Change-stream updateLookup returns the stored write snapshot as described above. Normal wire cursors expire after 10 minutes of inactivity; server shutdown releases cursors and sockets. Library transactions remain available independently of the adapter.

Execution and verification

Unsorted find cursors iterate SQLite rows lazily with independent statements and release them when exhausted or explicitly returned. Sorting and aggregation materialize their inputs. Equality predicates on declared secondary indexes use persisted index-entry tables to select candidates; other predicates run through the BSON/array matcher and may scan the collection. explain() shows the actual SQLite candidate-selection plan and flags the residual matcher.

File databases use WAL and a five-second busy timeout. SQLite supports one writer at a time. BSON serialization is provided by bson; Decimal128 document-update arithmetic uses decimal.js. Bun must bundle SQLite JSONB and FTS5 (tested on Bun 1.4.2).

Tests cover embedded features, rollback, persistence, original-schema migration, independent cursors, typed values, multikey constraints, FTS maintenance, TTL, resumable streams, official MongoDB Node driver operations, and raw wire framing. bun run bench measures writes, index creation, indexed reads, streaming array scans, and grouping over 10,000 documents by default; set NOSQLITE_BENCH_SIZE to change the count. This is a local smoke benchmark, not a production load test or evidence of complete MongoDB compatibility.