docs-src/docs/articles/alternatives/lokijs-alternative.md
Developers reach for LokiJS when they want a small, MongoDB-like JavaScript database that lives in memory and feels instant. The trade-off shows up later: writes only become durable when an adapter flushes them on a timer or before the process exits, replication across tabs and devices is missing, and the project itself has slowed down. Teams that ship real applications eventually look for a database that keeps the same ergonomic API but stores data safely, syncs across clients, and is still under active development.
This page explains how RxDB fits that role and how it covers both the in-memory speed scenarios that drew people to LokiJS and the persistence and sync gaps that pushed them away.
<center> <a href="https://rxdb.info/"></a>
LokiJS was started around 2014 by Joe Minichino as an embeddable JavaScript document store with a MongoDB-style query API. It kept the entire dataset in a JavaScript object graph, which is what made queries and mutations feel fast: there was no IO on the hot path. To survive a page reload, LokiJS shipped persistence adapters for IndexedDB, the file system in Node.js, and other targets. Those adapters serialize the in-memory state and write it out, either after a configurable autosave interval or when the process is asked to shut down.
Around 2020 the pace of releases dropped sharply. The project is closer to "feature complete" than to "actively developed", and unresolved issues have piled up. For new projects the question is no longer "is LokiJS fast enough" but "will the database I pick today still be maintained and safe to use in three years".
RxDB is a local-first, reactive, NoSQL database for JavaScript. It runs in the browser, in Node.js, in Electron, and in React Native. Documents are validated against a JSON schema, queries are MongoDB-style, and every RxQuery is observable so the UI can re-render when data changes. The storage layer is pluggable, so the same application code can run on top of IndexedDB, OPFS, Dexie, SQLite, or a pure in-memory store.
On top of that, RxDB ships a replication protocol that keeps clients in sync with each other and with a backend, with proper conflict handling.
The shortcomings below are the ones that show up most often when teams move off LokiJS:
RxDB was designed around the assumption that the database has to outlive a tab crash and stay consistent across many clients.
The example below mirrors what a LokiJS user would write to insert a document and observe a query, but with RxDB on top of IndexedDB so writes survive a reload.
import { createRxDatabase } from 'rxdb/plugins/core';
import { getRxStorageIndexedDB } from 'rxdb/plugins/storage-indexeddb';
const db = await createRxDatabase({
name: 'appdb',
storage: getRxStorageIndexedDB(),
multiInstance: true,
eventReduce: true
});
await db.addCollections({
notes: {
schema: {
title: 'notes schema',
version: 0,
primaryKey: 'id',
type: 'object',
properties: {
id: { type: 'string', maxLength: 100 },
title: { type: 'string' },
body: { type: 'string' },
updatedAt: { type: 'number' }
},
required: ['id', 'title', 'updatedAt'],
indexes: ['updatedAt']
}
}
});
await db.notes.insert({
id: 'note-1',
title: 'First note',
body: 'Hello RxDB',
updatedAt: Date.now()
});
// Observe a live query: the subscription fires on every change
const query = db.notes
.find()
.sort({ updatedAt: 'desc' });
query.$.subscribe(notes => {
console.log('current notes:', notes.map(n => n.title));
});
Compared to LokiJS, the meaningful difference is not the API surface, it is what happens under the hood: each insert is durable in IndexedDB, the query is reactive through RxJS, and other tabs of the same origin see the change automatically.
When the workload truly is "load some data, query it many times, throw it away", swap the storage for the Memory RxStorage. The rest of the code stays the same.
import { createRxDatabase } from 'rxdb/plugins/core';
import { getRxStorageMemory } from 'rxdb/plugins/storage-memory';
const cache = await createRxDatabase({
name: 'cache',
storage: getRxStorageMemory()
});
await cache.addCollections({
products: {
schema: {
title: 'products schema',
version: 0,
primaryKey: 'id',
type: 'object',
properties: {
id: { type: 'string', maxLength: 100 },
name: { type: 'string' },
price: { type: 'number' }
}
}
}
});
await cache.products.bulkInsert([
{ id: 'p1', name: 'Pen', price: 2 },
{ id: 'p2', name: 'Notebook', price: 6 }
]);
const cheap = await cache.products
.find({ selector: { price: { $lt: 5 } } })
.exec();
The Memory storage is also useful as a fast tier in front of a persistent RxCollection when you want both speed and durability.
Earlier RxDB versions shipped a lokijs RxStorage that wrapped LokiJS as a backing store. It was removed in RxDB version 16. Two reasons drove the decision:
If you previously used the LokiJS RxStorage, the migration is to pick whichever of those storages matches your durability needs and switch the storage option of createRxDatabase. Schemas and collection definitions stay the same.
LokiJS is no longer actively maintained, and bugs that affected RxDB users were not getting fixed upstream. RxDB v16 removed the LokiJS storage and points users at the Memory storage for in-memory use and at IndexedDB, OPFS, or Dexie for persistent browser storage.
</details> <details> <summary>Can RxDB give me LokiJS-like in-memory speed?</summary>Yes. The Memory RxStorage keeps the dataset in RAM and runs queries against in-memory indexes, which gives the same query latency profile as LokiJS without the broken persistence story.
</details> <details> <summary>Is LokiJS still maintained?</summary>Activity on the project has been minimal since around 2020. New issues and pull requests sit for long periods. For new projects, treating it as feature-frozen is the safer assumption.
</details> <details> <summary>How does RxDB persist data safely?</summary>Each write goes through the configured RxStorage, and storages like IndexedDB, OPFS, Dexie, and SQLite persist that write before acknowledging it. There is no autosave interval that can drop committed data when the tab is closed.
</details> <details> <summary>How do I migrate from LokiJS to RxDB?</summary>Define an RxCollection with a JSON schema that matches your LokiJS collection, read the existing LokiJS data once on startup, and bulkInsert it into the RxDB collection. From that point on, write through RxDB and use a replication plugin if you also need to sync the data with a server.
| Capability | LokiJS | RxDB |
|---|---|---|
| Primary storage model | In-memory object graph | Pluggable RxStorage (IndexedDB, OPFS, Dexie, SQLite, Memory) |
| Durability of writes | Periodic snapshot via adapter | Per-write durability through the underlying engine |
| In-memory mode | Default | Optional via Memory RxStorage |
| Query API | MongoDB-style | MongoDB-style with observable RxQuery |
| Reactivity | Events, no observable queries | RxJS observables on every query and document |
| Multi-tab support | Fragile, snapshot collisions | Coordinated writes across tabs |
| Replication / sync | Not built in | Built-in sync engine, HTTP, GraphQL, CouchDB, WebRTC, Firestore |
| Conflict resolution | None | Custom conflict handlers with revisions |
| Schema and migrations | Optional, ad-hoc | JSON schema with versioned migrations |
| Project activity | Low since around 2020 | Actively maintained |
If LokiJS got you most of the way and stopped being enough once persistence, multi-tab safety, or sync entered the picture, RxDB is the natural next step. It keeps the document-store ergonomics, adds reactive queries, lets you choose between durable and in-memory storages per use case, and gives you a real replication protocol for the day your app stops being a single-device toy.
For more on the broader direction RxDB is built around, see the local-first future.