Skip to main content

RxDB as a localForage Alternative for Real Database Features

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.

JavaScript Database

A Short History of localForage

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.

What RxDB Is

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.

Where localForage Stops

The places where localForage runs out of road are predictable once you list them:

  • Key-value only. Every value is opaque to the library. There is no notion of fields, types, or relationships.
  • No indexes. You cannot ask for "all todos where done = false and dueDate < tomorrow". You either keep your own index keys by hand or scan all entries.
  • No queries. There is no query language, no filtering, no sorting, no pagination beyond what you build yourself.
  • No schema. Data shape drifts over time and migrations become custom scripts.
  • No reactivity. When a value changes, other parts of the app do not find out unless you wire your own event bus on top.
  • No multi-tab coordination. Two tabs writing to the same key can clobber each other without warning.
  • No replication. Syncing with a server is left entirely to the application.
  • Limited debugging. There are no dev tools that understand collections, queries, or change streams because those concepts do not exist in the library.

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.

What RxDB Adds On Top

RxDB treats the browser like a real database environment.

  • Documents and collections. Data is stored in typed collections with a JSON schema that validates every write.
  • Indexed queries. Define indexes in the schema and run MongoDB style queries such as find, findOne, where, gt, in, sort, skip, and limit.
  • Reactive results. Every query returns an observable that re-emits when matching documents change, so UI components stay in sync without manual refetching.
  • Multi-tab. Open the same app in three tabs and they share one database state, with leader election and cross-tab change propagation handled by RxDB.
  • Replication primitives. A pull/push checkpoint protocol keeps local data in sync with any backend you choose, including custom REST APIs.
  • Storage choice. Swap IndexedDB, OPFS, Dexie, memory, or a server side storage without changing application code.
  • Conflict handling. Each document carries a revision, and a custom conflict handler decides how concurrent edits merge.

Code Sample: Reading a Single Record

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.

Code Sample: Indexed Range Query With a Subscription

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.

Storage Layer Notes

localForage and RxDB both end up writing to similar browser primitives, but the way they use them differs.

  • localForage selects one backend per database and stores opaque blobs at string keys.
  • RxDB writes structured documents through a pluggable storage interface. For modern browsers, the OPFS storage avoids many IndexedDB performance issues by writing to the Origin Private File System. The Dexie storage is a thin layer over IndexedDB for cases where Dexie is already in use.

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.

FAQ

Should I use localForage or RxDB for caching?

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.

Can RxDB replace localStorage?

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.

Does RxDB handle multi-tab?

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.

How big can RxDB scale in IndexedDB?

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.

Comparison Table

FeaturelocalForageRxDB
Data modelKey-value blobsDocuments in collections
Schema validationNoneJSON Schema per collection
QueriesNone, manual iterationMongoDB style with indexes
IndexesNot supportedDeclared in schema
ReactivityNoneObservable queries and documents
Multi-tab syncNot handledBuilt in via BroadcastChannel
ReplicationNot includedPull/push protocol with many plugins
Conflict handlingNot applicablePer document revisions and custom handlers
Storage backendsIndexedDB, WebSQL, localStorageIndexedDB, OPFS, Dexie, memory, Node.js, React Native, more
EncryptionNot built inPlugin available
MigrationsManualSchema versioning with migration strategies
Offline firstStorage onlyFull offline first stack
Active developmentLowActive

When to Pick Which

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: