Overview
Cross-browser storage adapter with automatic driver fallback using localForage.
@localmode/localforage
Cross-browser storage adapter using localForage. Automatically falls back from IndexedDB to WebSQL to localStorage, ensuring storage works in every browser environment.
Features
- 🌐 Auto-Fallback — IndexedDB -> WebSQL -> localStorage, automatically
- 🔄 Cross-Browser — Works everywhere, including older browsers
- 📦 ~10KB — Mature, battle-tested library
- 🔒 Safari Private Browsing — Falls back gracefully when IndexedDB is blocked
Installation
bash pnpm install @localmode/localforage @localmode/core bash npm install @localmode/localforage @localmode/core bash yarn add @localmode/localforage @localmode/core bash bun add @localmode/localforage @localmode/core Quick Start
import { LocalForageStorage } from '@localmode/localforage';
import { createVectorDB, embed, ingest } from '@localmode/core';
import { transformers } from '@localmode/transformers';
// Create storage — automatically picks best available driver
const storage = new LocalForageStorage({ name: 'my-app' });
// Use with VectorDB
const db = await createVectorDB({
name: 'documents',
dimensions: 384,
storage,
});
// Ingest documents
const model = transformers.embedding('Xenova/bge-small-en-v1.5');
await ingest({
db,
model,
documents: [
{ text: 'Local-first AI is the future', metadata: { source: 'blog' } },
{ text: 'Privacy-preserving machine learning', metadata: { source: 'paper' } },
],
});The ingest() function is part of the RAG pipeline. It chunks, embeds, and stores documents in a single call.
Configuration
Prop
Type
Driver Selection
By default, localForage selects the best available driver automatically. You can override the priority:
import localforage from 'localforage';
import { LocalForageStorage } from '@localmode/localforage';
// Force localStorage only (e.g., for testing)
const storage = new LocalForageStorage({
name: 'my-app',
driver: [localforage.LOCALSTORAGE],
});
// Prefer IndexedDB, fall back to localStorage (skip WebSQL)
const storage2 = new LocalForageStorage({
name: 'my-app',
driver: [localforage.INDEXEDDB, localforage.LOCALSTORAGE],
});Auto-Fallback Behavior
localForage handles driver selection transparently:
| Driver | Environment | Used When |
|---|---|---|
| IndexedDB | Modern browsers | Default, best performance |
| WebSQL | Older Safari, some mobile | IndexedDB unavailable |
| localStorage | All browsers | Last resort fallback |
Safari Private Browsing
Safari's private browsing mode blocks IndexedDB. localForage automatically falls back to localStorage, so your app keeps working. Note that localStorage has a ~5MB limit, which may affect large vector databases.
Vector Serialization
LocalForageStorage stores vectors as number[] arrays (not typed arrays) internally. This is
required because typed arrays do not survive JSON round-tripping used by the localStorage and
WebSQL drivers. Vectors are automatically converted back to their original typed array on read
(Float32Array for float vectors, Uint8Array for SQ8/PQ-compressed payloads) and to
number[] on write. This means storage size is ~2-3x larger than binary storage, and the
localStorage driver's ~5MB quota limit may be reached with large vector databases.
Persistence Guarantees
LocalForageStorage round-trips the full Collection object, so everything core stores on it survives a page reload or database reopen: SQ8/PQ vector quantization calibration (calibration, pqCodebook), storage-compression calibration (compressionCalibration, deltaCalibration, compression), and the embedding-drift modelFingerprint. Quantized and compressed databases decode identically across sessions, and drift detection works the same as with core's built-in IndexedDBStorage.
Used quantization or compression before this fix?
Earlier versions of this adapter persisted only four Collection fields and silently dropped
the calibration data — quantized/compressed vectors round-tripped fine in-session but decoded
as raw bytes after a reopen. The calibration was never stored, so affected databases cannot be
repaired: clear and re-ingest (databases that never used quantization or compression are
unaffected).
// Recover a database that used quantization/compression before the fix
await db.clear(); // or clear the underlying stores by deleting the localForage database
// Then re-ingest from your source documents
await ingest({ db, model, documents });localStorage fallback driver and calibration data
localForage's localStorage fallback driver JSON-serializes stored values, and the typed arrays
nested inside calibration data (e.g. PQ codebook centroids) do not survive that round-trip — so
quantization/compression calibration degrades under the localStorage driver only. The default
IndexedDB driver preserves it fully. If your users may land on the localStorage fallback (e.g.
Safari Private Browsing), avoid vector quantization/compression, or pin driver to
[localforage.INDEXEDDB]. This is a pre-existing characteristic of the driver, not part of the
persistence fix above.
Writing your own adapter? Verify the same guarantees with the StorageAdapter conformance suite — the official adapters run it in their own test suites.
Storage Fallback
Combine with a try/catch pattern for maximum resilience:
import { MemoryStorage } from '@localmode/core';
import { LocalForageStorage } from '@localmode/localforage';
let storage: LocalForageStorage | MemoryStorage;
try {
storage = new LocalForageStorage({ name: 'my-app' });
await storage.open();
} catch (error) {
console.warn('LocalForageStorage unavailable, falling back to memory:', error);
storage = new MemoryStorage();
}Comparison
| Adapter | Package | Bundle Size | Transactions | Auto-Fallback | Best For |
|---|---|---|---|---|---|
IndexedDBStorage | @localmode/core | 0KB (built-in) | No | No | Simple apps, zero extra deps |
DexieStorage | @localmode/dexie | ~15KB | Yes | No | Production apps needing schema versioning |
IDBStorage | @localmode/idb | ~3KB | No | No | Minimal bundle size |
LocalForageStorage | @localmode/localforage | ~10KB | No | Yes | Max browser compatibility |