dexie, idb, and localforage are JavaScript libraries designed to simplify client-side data persistence in web applications. dexie is a feature-rich wrapper for IndexedDB that offers a fluent API and schema management. idb is a lightweight, promise-based wrapper that exposes native IndexedDB functionality with minimal abstraction. localforage provides a simple key-value API that works across IndexedDB, WebSQL, and localStorage, abstracting the underlying storage engine completely.
When building offline-first web apps or caching layers, choosing the right storage library is critical. dexie, idb, and localforage all target client-side persistence, but they solve different problems with different levels of abstraction. Let's compare how they handle schema, queries, and transactions.
dexie uses a fluent schema definition that feels like an ORM.
// dexie: Declarative schema
import Dexie from 'dexie';
const db = new Dexie('MyDatabase');
db.version(1).stores({
friends: '++id, name, age' // id is auto-inc, name and age are indexed
});
idb exposes the native IndexedDB upgrade process via promises.
upgrade callback manually.// idb: Manual upgrade block
import { openDB } from 'idb';
const db = await openDB('MyDatabase', 1, {
upgrade(db) {
const store = db.createObjectStore('friends', {
keyPath: 'id',
autoIncrement: true
});
store.createIndex('by_age', 'age');
}
});
localforage has no schema.
localStorage.// localforage: No schema, just config
import localforage from 'localforage';
localforage.config({
name: 'MyDatabase',
version: 1,
storeName: 'friends' // Acts like a single bucket
});
// Data is stored as key-value pairs only
dexie allows complex queries using a chainable API.
// dexie: Rich querying
const adults = await db.friends
.where('age')
.above(18)
.and(friend => friend.name.startsWith('A'))
.toArray();
idb uses native IndexedDB queries via promises.
IDBKeyRange for advanced filtering.// idb: Native querying
import { IDBKeyRange } from 'idb';
const range = IDBKeyRange.lowerBound(18);
const adults = await db.getAllFromIndex('friends', 'by_age', range);
// Further filtering must be done manually in JS
localforage does not support value-based queries.
// localforage: Key-value only
const friend = await localforage.getItem('friend_123');
// To find by value, you must iterate everything (slow)
const keys = await localforage.keys();
// Manual filtering required in application code
dexie handles transactions automatically or explicitly.
// dexie: Explicit transaction
await db.transaction('rw', db.friends, db.logs, async () => {
await db.friends.add({ name: 'Alice' });
await db.logs.add({ action: 'added_friend' });
});
idb requires manual transaction creation.
// idb: Manual transaction
const tx = db.transaction('friends', 'readwrite');
await tx.objectStore('friends').add({ name: 'Alice' });
await tx.done; // Waits for completion
localforage does not support multi-key transactions.
setItem calls succeed or fail together.// localforage: No transactions
await localforage.setItem('key1', 'value1');
await localforage.setItem('key2', 'value2');
// If the second fails, the first is already saved
dexie is actively maintained with a large community.
idb is maintained by Google engineers (Jake Archibald).
localforage is in maintenance mode with low activity.
| Feature | dexie | idb | localforage |
|---|---|---|---|
| API Style | π’ Fluent, ORM-like | π‘ Promise-based Native | π Key-Value (localStorage) |
| Schema | β Declarative & Versioned | β Manual Upgrade Block | β None |
| Querying | β Rich (Ranges, Filters) | β Native (IDBKeyRange) | β Keys Only |
| Transactions | β Multi-store Atomic | β Manual Control | β Per-Operation Only |
| Bundle Weight | π Medium | π’ Tiny | π Medium |
| Status | π’ Active | π’ Active | π‘ Maintenance |
dexie is the productivity choice π§°. It removes the pain of IndexedDB while keeping its power. Use it for complex apps like note-takers, caches, or offline sync engines where you need to query data efficiently.
idb is the purist choice π§. It gives you promises without hiding the engine. Use it if you want standards compliance, minimal dependencies, and you already understand how IndexedDB works.
localforage is the legacy choice π°οΈ. It is great for dropping into old projects that need better storage than localStorage without refactoring logic. For new projects, prefer dexie or idb to avoid hitting the key-value ceiling later.
Final Thought: IndexedDB is powerful but verbose. dexie makes it enjoyable, idb makes it modern, and localforage makes it simple β but too simple for serious data work. Choose based on how much querying you need to do.
Choose dexie if you need complex querying, schema versioning, and a developer-friendly API for IndexedDB. It is ideal for applications that treat the browser database like a real backend, requiring indexes, relationships, and transactional safety without writing boilerplate code.
Choose idb if you want full control over IndexedDB with modern promise-based syntax but minimal abstraction overhead. It is best for developers who understand IndexedDB concepts and want a tiny, standards-compliant helper that doesn't hide the underlying API mechanics.
Choose localforage only for simple key-value storage needs where you want a localStorage-like API but with better performance and capacity. Avoid it for complex queries or new projects requiring advanced IndexedDB features, as it abstracts away the power of IndexedDB and is in maintenance mode.
Dexie.js is a wrapper library for indexedDB - the standard database in the browser. https://dexie.org.
IndexedDB is the portable database for all browser engines. Dexie.js makes it fun and easy to work with.
But also:
<!DOCTYPE html>
<html>
<head>
<script type="module">
// Import Dexie
import { Dexie } from 'https://unpkg.com/dexie/dist/modern/dexie.mjs';
//
// Declare Database
//
const db = new Dexie('FriendDatabase');
db.version(1).stores({
friends: '++id, age'
});
//
// Play with it
//
try {
await db.friends.add({ name: 'Alice', age: 21 });
const youngFriends = await db.friends
.where('age')
.below(30)
.toArray();
alert(`My young friends: ${JSON.stringify(youngFriends)}`);
} catch (e) {
alert(`Oops: ${e}`);
}
</script>
</head>
</html>
Yes, it's that simple. Read the docs to get into the details.
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/dexie/dist/dexie.js"></script>
<script>
//
// Declare Database
//
const db = new Dexie('FriendDatabase');
db.version(1).stores({
friends: '++id, age'
});
//
// Play with it
//
db.friends.add({ name: 'Alice', age: 21 }).then(() => {
return db.friends
.where('age')
.below(30)
.toArray();
}).then(youngFriends => {
alert (`My young friends: ${JSON.stringify(youngFriends)}`);
}).catch (e => {
alert(`Oops: ${e}`);
});
</script>
</head>
</html>
Real-world apps are often built using components in various frameworks. Here's a version of Hello World written for React and Typescript. There are also links below this sample to more tutorials for different frameworks...
import React from 'react';
import { Dexie, type EntityTable } from 'dexie';
import { useLiveQuery } from 'dexie-react-hooks';
// Typing for your entities (hint is to move this to its own module)
export interface Friend {
id: number;
name: string;
age: number;
}
// Database declaration (move this to its own module also)
export const db = new Dexie('FriendDatabase') as Dexie & {
friends: EntityTable<Friend, 'id'>;
};
db.version(1).stores({
friends: '++id, age',
});
// Component:
export function MyDexieReactComponent() {
const youngFriends = useLiveQuery(() =>
db.friends
.where('age')
.below(30)
.toArray()
);
return (
<>
<h3>My young friends</h3>
<ul>
{youngFriends?.map((f) => (
<li key={f.id}>
Name: {f.name}, Age: {f.age}
</li>
))}
</ul>
<button
onClick={() => {
db.friends.add({ name: 'Alice', age: 21 });
}}
>
Add another friend
</button>
</>
);
}
Tutorials for React, Svelte, Vue, Angular and vanilla JS
Dexie has kick-ass performance. Its bulk methods take advantage of a lesser-known feature in IndexedDB that makes it possible to store stuff without listening to every onsuccess event. This speeds up the performance to a maximum.
above(key): Collection;
aboveOrEqual(key): Collection;
add(item, key?): Promise;
and(filter: (x) => boolean): Collection;
anyOf(keys[]): Collection;
anyOfIgnoreCase(keys: string[]): Collection;
below(key): Collection;
belowOrEqual(key): Collection;
between(lower, upper, includeLower?, includeUpper?): Collection;
bulkAdd(items: Array): Promise;
bulkDelete(keys: Array): Promise;
bulkPut(items: Array): Promise;
clear(): Promise;
count(): Promise;
delete(key): Promise;
distinct(): Collection;
each(callback: (obj) => any): Promise;
eachKey(callback: (key) => any): Promise;
eachPrimaryKey(callback: (key) => any): Promise;
eachUniqueKey(callback: (key) => any): Promise;
equals(key): Collection;
equalsIgnoreCase(key): Collection;
filter(fn: (obj) => boolean): Collection;
first(): Promise;
get(key): Promise;
inAnyRange(ranges): Collection;
keys(): Promise;
last(): Promise;
limit(n: number): Collection;
modify(changeCallback: (obj: T, ctx:{value: T}) => void): Promise;
modify(changes: { [keyPath: string]: any } ): Promise;
noneOf(keys: Array): Collection;
notEqual(key): Collection;
offset(n: number): Collection;
or(indexOrPrimayKey: string): WhereClause;
orderBy(index: string): Collection;
primaryKeys(): Promise;
put(item: T, key?: Key): Promise;
reverse(): Collection;
sortBy(keyPath: string): Promise;
startsWith(key: string): Collection;
startsWithAnyOf(prefixes: string[]): Collection;
startsWithAnyOfIgnoreCase(prefixes: string[]): Collection;
startsWithIgnoreCase(key: string): Collection;
toArray(): Promise;
toCollection(): Collection;
uniqueKeys(): Promise;
until(filter: (value) => boolean, includeStopEntry?: boolean): Collection;
update(key: Key, changes: { [keyPath: string]: any }): Promise;
This is a mix of methods from WhereClause, Table and Collection. Dive into the API reference to see the details.
Dexie Cloud is the easiest way to add sync, authentication, and real-time collaboration to your Dexie app. You keep writing frontend code with Dexie.js β Dexie Cloud handles the rest.
What you get:
Getting started is just a few lines:
npm install dexie-cloud-addon
import Dexie from 'dexie';
import dexieCloud from 'dexie-cloud-addon';
const db = new Dexie('MyDatabase', { addons: [dexieCloud] });
db.version(1).stores({ items: '@id, title' });
db.cloud.configure({ databaseUrl: 'https://<your-db>.dexie.cloud' });
That's it. Your existing Dexie app now syncs. Hosted cloud or self-hosted on your own infrastructure. π
β Quickstart guide
Sample app:
Source: Dexie Cloud To-do app
Live demo: https://dexie.github.io/Dexie.js/dexie-cloud-todo-app/
https://dexie.org/docs/Samples
https://github.com/dexie/Dexie.js/tree/master/samples
https://dexie.org/docs/Questions-and-Answers
npm install dexie
For those who don't like package managers, here's the download links:
https://unpkg.com/dexie@latest/dist/dexie.min.js
https://unpkg.com/dexie@latest/dist/dexie.min.js.map
https://unpkg.com/dexie@latest/dist/modern/dexie.min.mjs
https://unpkg.com/dexie@latest/dist/modern/dexie.min.mjs.map
https://unpkg.com/dexie@latest/dist/dexie.d.ts
See CONTRIBUTING.md
pnpm install
pnpm run build
pnpm test
pnpm run watch