docs-src/docs/articles/alternatives/localforage-alternative.md
localForage gives JavaScript developers a clean promise based wrapper around browser storage. It is a thin key-value layer that picks the best available backend, usually IndexedDB, with fallbacks to WebSQL or localStorage. Teams reach for it when they want a simple setItem and getItem API that works across browsers without writing IndexedDB transaction code by hand.
The trouble starts when an app grows past simple caching. As soon as you need indexed queries, schema validation, change subscriptions, replication with a backend, or coordination across browser tabs, you end up rebuilding most of a database on top of localForage. That is when RxDB becomes a better fit.
<center> <a href="https://rxdb.info/"></a>
localForage was started around 2014 by developers at Mozilla as part of efforts to make offline web apps more practical. The motivation was straightforward: IndexedDB had a verbose, event based API, WebSQL was deprecated in some browsers, and localStorage was synchronous and capped at a few megabytes. localForage hid those differences behind a single API modeled on localStorage.
The library settled into a stable shape early. Recent commit activity on the main repository has been low, and the feature set has stayed close to its original scope: get, set, remove, clear, keys, length, and a few iteration helpers. It does what it set out to do, and nothing more.
That focus is the point. localForage is a storage compatibility layer, not a database. When the requirements list grows beyond key-value reads and writes, the gap between what localForage offers and what an application needs widens.
RxDB is a local-first, NoSQL database that runs inside JavaScript runtimes. It stores documents in collections, validates them against a schema, runs MongoDB style queries with indexes, and emits changes through RxJS observables. RxDB sits on top of a pluggable storage layer, so the same database code can run on IndexedDB, OPFS, Dexie, in memory, in Node.js, in React Native, or in Electron.
On top of local storage, RxDB ships a replication protocol that syncs collections with any backend that can speak HTTP, GraphQL, WebRTC, CouchDB, or Firestore. Reads stay local and fast, while writes flow to the server in the background.
The places where localForage runs out of road are predictable once you list them:
done = false and dueDate < tomorrow". You either keep your own index keys by hand or scan all entries.For a cache of avatars or a saved form draft, none of this matters. For an app that wants to feel like a product with offline support, all of it matters.
RxDB treats the browser like a real database environment.
find, findOne, where, gt, in, sort, skip, and limit.A typical localForage read by key:
import localforage from 'localforage';
const todo = await localforage.getItem<Todo>('todo-42');
if (todo) {
console.log(todo.title);
}
The same lookup in RxDB, by primary key, with a typed result and a schema validated value behind it:
import { createRxDatabase } from 'rxdb/plugins/core';
import { getRxStorageIndexedDB } from 'rxdb/plugins/storage-indexeddb';
const db = await createRxDatabase({
name: 'mydb',
storage: getRxStorageIndexedDB()
});
await db.addCollections({
todos: {
schema: {
title: 'todo schema',
version: 0,
primaryKey: 'id',
type: 'object',
properties: {
id: { type: 'string', maxLength: 100 },
title: { type: 'string' },
done: { type: 'boolean' },
dueDate: { type: 'string', format: 'date-time' }
},
required: ['id', 'title', 'done'],
indexes: ['done', 'dueDate']
}
}
});
const todo = await db.todos.findOne('todo-42').exec();
console.log(todo?.title);
The RxDB version is longer at setup time, but the database now knows the shape of a todo, can index done and dueDate, and can stream changes to the rest of the app.
In localForage, finding open todos due before tomorrow means iterating every entry:
const openTodos: Todo[] = [];
await localforage.iterate<Todo, void>((value) => {
if (!value.done && value.dueDate < tomorrow) {
openTodos.push(value);
}
});
The same query in RxDB uses the index and returns an observable that re-fires on every change:
const query = db.todos.find({
selector: {
done: false,
dueDate: { $lt: tomorrow }
},
sort: [{ dueDate: 'asc' }]
});
const subscription = query.$.subscribe((openTodos) => {
render(openTodos);
});
Insert a new todo, mark one as done, or sync a remote change in another tab, and the subscriber receives the new result set without writing extra code.
localForage and RxDB both end up writing to similar browser primitives, but the way they use them differs.
Because storages are swappable, the same RxDB schemas and queries run unchanged across these backends, including in Node.js or React Native where IndexedDB is not the right fit.
For a flat cache of API responses keyed by URL, localForage is fine and has a smaller footprint. Reach for RxDB when the cache needs queries, indexes, expiry rules expressed as fields, change subscriptions for the UI, or replication back to a server. See the reactivity guide for how observable queries replace manual cache invalidation.
</details> <details> <summary>Can RxDB replace localStorage?</summary>Yes. RxDB ships a localStorage based storage adapter for small datasets, and IndexedDB or OPFS adapters for larger ones. Unlike raw localStorage, RxDB gives you schemas, queries, and async APIs that do not block the main thread.
</details> <details> <summary>Does RxDB handle multi-tab?</summary>Yes. With multiInstance: true, RxDB coordinates across tabs of the same origin. Writes in one tab are visible to queries in other tabs, leader election picks one tab to run replication, and change events propagate over a BroadcastChannel.
RxDB has been used with hundreds of thousands of documents per collection in IndexedDB. For larger datasets or write heavy workloads, the OPFS storage sidesteps many of the slow IndexedDB bottlenecks and keeps query latency low.
</details>| Feature | localForage | RxDB |
|---|---|---|
| Data model | Key-value blobs | Documents in collections |
| Schema validation | None | JSON Schema per collection |
| Queries | None, manual iteration | MongoDB style with indexes |
| Indexes | Not supported | Declared in schema |
| Reactivity | None | Observable queries and documents |
| Multi-tab sync | Not handled | Built in via BroadcastChannel |
| Replication | Not included | Pull/push protocol with many plugins |
| Conflict handling | Not applicable | Per document revisions and custom handlers |
| Storage backends | IndexedDB, WebSQL, localStorage | IndexedDB, OPFS, Dexie, memory, Node.js, React Native, more |
| Encryption | Not built in | Plugin available |
| Migrations | Manual | Schema versioning with migration strategies |
| Offline first | Storage only | Full offline first stack |
| Active development | Low | Active |
Choose localForage when the job is "store a few values in the browser without thinking about IndexedDB". It is small, well understood, and stays out of the way.
Choose RxDB when the app needs a real client side database: typed collections, indexed queries, reactive results, multi-tab coordination, and replication with a backend. RxDB takes more setup at first, then pays it back as features are added on top of the same data layer.
More resources: