Storage
Persistent storage with IndexedDB and memory fallbacks.
LocalMode provides flexible storage options for persisting vector databases and application data.
See it in action
Try Encrypted Vault for a working demo of these APIs.
Storage Options
The default storage uses IndexedDB for persistence:
import { IndexedDBStorage, createVectorDB } from '@localmode/core';
// Basic usage
const storage = new IndexedDBStorage('my-app');
// With Write-Ahead Log for crash recovery
const walStorage = new IndexedDBStorage('my-app', { enableWAL: true });
// Or use default (IndexedDB) automatically:
const db = await createVectorDB({
name: 'documents',
dimensions: 384,
// Uses IndexedDBStorage by default
});The optional enableWAL option enables a Write-Ahead Log that records operations before executing them. If the browser crashes mid-write, the WAL replays uncommitted operations on next open, preventing data corruption. Defaults to false.
Data persists across page reloads and browser restarts.
For temporary data or environments without IndexedDB:
import { MemoryStorage, createVectorDB } from '@localmode/core';
const db = await createVectorDB({
name: 'temp',
dimensions: 384,
storage: new MemoryStorage(),
});
// ⚠️ Data is lost on page reloadUseful for:
- Testing and development
- Temporary caches
- Safari private browsing fallback
Safari's private browsing mode blocks IndexedDB. Use MemoryStorage as a fallback or detect this
condition with isIndexedDBSupported().
Storage Shorthand
createVectorDB() accepts a storage parameter as either a string shorthand or a StorageAdapter instance:
import { createVectorDB, MemoryStorage } from '@localmode/core';
// String shorthand (recommended for built-in adapters)
const db1 = await createVectorDB({ name: 'docs', dimensions: 384, storage: 'memory' });
const db2 = await createVectorDB({ name: 'docs', dimensions: 384, storage: 'indexeddb' });
// Omitting storage defaults to 'indexeddb'
const db3 = await createVectorDB({ name: 'docs', dimensions: 384 });
// StorageAdapter instance (for custom or third-party adapters)
const db4 = await createVectorDB({ name: 'docs', dimensions: 384, storage: new MemoryStorage() });| Value | Behavior |
|---|---|
'indexeddb' (default) | Persists data in IndexedDB |
'memory' | In-memory storage, lost on page reload |
StorageAdapter instance | Uses the provided adapter (e.g., DexieStorage, IDBStorage) |
StorageAdapter Interface
All storage adapters implement the StorageAdapter interface:
Prop
Type
StoredDocument
Prop
Type
StoredVector
Prop
Type
Third-Party Adapters
LocalMode provides three external storage adapters, each with dedicated documentation:
| Adapter | Package | Bundle Size | Best For |
|---|---|---|---|
DexieStorage | @localmode/dexie | ~15KB | Schema versioning, transactions |
IDBStorage | @localmode/idb | ~3KB | Minimal bundle size |
LocalForageStorage | @localmode/localforage | ~10KB | Max browser compatibility, auto-fallback |
All adapters implement the same StorageAdapter interface and work with createVectorDB():
import { DexieStorage } from '@localmode/dexie';
import { IDBStorage } from '@localmode/idb';
import { LocalForageStorage } from '@localmode/localforage';
import { createVectorDB } from '@localmode/core';
// Pick any adapter — same API
const storage = new DexieStorage({ name: 'my-app' });
// const storage = new IDBStorage({ name: 'my-app' });
// const storage = new LocalForageStorage({ name: 'my-app' });
const db = await createVectorDB({
name: 'documents',
dimensions: 384,
storage,
});Choosing an adapter
DexieStorage— Best for production apps that need schema versioning and transactional writes. Learn moreIDBStorage— Best when bundle size is critical (~3KB). Learn moreLocalForageStorage— Best for maximum browser compatibility with automatic IndexedDB -> WebSQL -> localStorage fallback. Learn more
Custom Storage
Implement your own storage adapter by implementing the StorageAdapter interface. Note that StorageAdapter is the interface name for custom implementations, while Storage is a convenience type alias for the built-in adapters (IndexedDBStorage | MemoryStorage).
// StorageAdapter — the interface to implement for custom adapters
// Storage — type alias for built-in implementations (IndexedDBStorage | MemoryStorage)
import type { StorageAdapter, Storage, StoredDocument, StoredVector, Collection, SerializedHNSWIndex } from '@localmode/core';
class MyCustomStorage implements StorageAdapter {
// Lifecycle
async open() { /* initialize connection */ }
async close() { /* cleanup */ }
// Document operations
async addDocument(doc: StoredDocument) { /* store document */ }
async getDocument(id: string) { /* return doc or null */ return null; }
async deleteDocument(id: string) { /* remove document */ }
async getAllDocuments(collectionId: string) { return []; }
async countDocuments(collectionId: string) { return 0; }
// Vector operations
async addVector(vec: StoredVector) { /* store vector */ }
async getVector(id: string) { /* return Float32Array or null */ return null; }
async deleteVector(id: string) { /* remove vector */ }
async getAllVectors(collectionId: string) { return new Map(); }
// Index operations
async saveIndex(collectionId: string, index: SerializedHNSWIndex) { /* persist index */ }
async loadIndex(collectionId: string) { /* return index or null */ return null; }
async deleteIndex(collectionId: string) { /* remove index */ }
// Collection operations
async createCollection(collection: Collection) { /* store collection */ }
async getCollection(id: string) { return null; }
async getCollectionByName(name: string) { return null; }
async getAllCollections() { return []; }
async deleteCollection(id: string) { /* remove collection */ }
// Utility operations
async clear() { /* clear all data */ }
async clearCollection(collectionId: string) { /* clear collection data */ }
async estimateSize() { return 0; }
}Storage Fallback
Gracefully fallback when IndexedDB is unavailable (e.g., Safari private browsing):
import { IndexedDBStorage, MemoryStorage, createVectorDB } from '@localmode/core';
let storage: IndexedDBStorage | MemoryStorage;
try {
storage = new IndexedDBStorage('my-app');
await storage.open();
} catch (error) {
console.warn('IndexedDB unavailable, falling back to memory:', error);
storage = new MemoryStorage();
}
const db = await createVectorDB({
name: 'robust-db',
dimensions: 384,
storage,
});Quota Management
Monitor and manage storage quota:
import { getStorageQuota, requestPersistence } from '@localmode/core';
// Check available quota
const quota = await getStorageQuota();
console.log('Used:', quota.usage);
console.log('Available:', quota.quota);
console.log('Percent used:', ((quota.usage / quota.quota) * 100).toFixed(1) + '%');
// Request persistent storage (won't be auto-cleared)
const isPersisted = await requestPersistence();
if (isPersisted) {
console.log('Storage is now persistent');
}Quota Warnings
import { checkQuotaWithWarnings } from '@localmode/core';
const { ok, warning, quota } = await checkQuotaWithWarnings({
warningThreshold: 0.8, // Warn at 80% usage
});
if (warning) {
console.warn('Storage is almost full!', quota);
}Storage Compression
Reduce IndexedDB disk usage by up to 4x without affecting search quality. Storage compression uses SQ8 (scalar quantization to Uint8) to compress vectors before writing to IndexedDB, and decompresses on read. The HNSW index always uses original Float32Array vectors in memory, so search recall is completely unaffected.
Compression vs Quantization
Storage compression (compression) and vector quantization (quantization) are independent features:
- Quantization compresses vectors in the HNSW index, trading recall for storage.
- Compression only reduces disk usage in IndexedDB. No recall impact.
When both are enabled, quantization takes priority and compression is skipped (since vectors are already Uint8).
Enable Compression
import { createVectorDB } from '@localmode/core';
const db = await createVectorDB({
name: 'docs',
dimensions: 384,
compression: { type: 'sq8' },
});
// Use the database normally — compression is transparent
await db.add({ id: '1', vector: embedding, metadata: { title: 'Hello' } });
// get() returns decompressed Float32Array
const doc = await db.get('1');
console.log(doc.vector instanceof Float32Array); // trueCompression Modes
| Mode | Description | Compression | Best for |
|---|---|---|---|
'sq8' | Standard SQ8 per vector | 4x | General use |
'delta-sq8' | Delta encoding + SQ8 | 4x (potentially better quantization error) | Batch-inserted similar vectors |
'none' | No compression (default) | 1x | Maximum precision |
Compression Statistics
import { createVectorDB, getCompressionStats } from '@localmode/core';
const db = await createVectorDB({
name: 'docs',
dimensions: 384,
compression: { type: 'sq8' },
});
// ... add vectors ...
const stats = await getCompressionStats(db);
console.log(stats);
// {
// enabled: true,
// type: 'sq8',
// vectorCount: 1000,
// originalSizeBytes: 1536000, // 1000 * 384 * 4
// compressedSizeBytes: 384000, // 1000 * 384 * 1
// ratio: 4.0,
// dimensions: 384,
// }CompressionConfig Options
Prop
Type
Standalone Functions
For advanced use cases (e.g., compressing vectors before custom storage, offline processing), the low-level compression functions are available:
import { compressVectors, decompressVectors } from '@localmode/core';
const vectors = [
new Float32Array([0.1, -0.5, 0.3, 0.8]),
new Float32Array([0.4, 0.2, -0.1, 0.6]),
];
// Compress
const block = compressVectors(vectors);
// block.data[0] is Uint8Array(4), block.calibration contains per-dimension min/max
// Decompress
const restored = decompressVectors(block, block.calibration);
// restored[0] ≈ vectors[0] (within range/255 per dimension)compressVectors(vectors, calibration?, mode?) accepts an optional pre-computed calibration and an optional mode ('sq8' or 'delta-sq8'). decompressVectors(block, calibration) restores approximate Float32Array vectors from a compressed block.
When using createVectorDB({ compression }), compression and decompression happen automatically on add() and get(). The standalone functions are only needed for custom workflows.
Limitations
- Compression introduces small approximation error (~
range/255per dimension per vector). - Calibration is computed from the first batch of vectors added. If later vectors have significantly different distributions, accuracy may decrease.
delta-sq8mode benefits most when vectors are batch-inserted in a meaningful order (e.g., from the same model and topic).- Export always produces decompressed Float32 vectors for portability.
Cleanup
Remove old or unused data:
import { cleanup } from '@localmode/core';
// Clean up databases older than 30 days
await cleanup({
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days in ms
onDelete: (name) => console.log(`Deleted: ${name}`),
});
// Clean up to free space
await cleanup({
targetFreeSpace: 100 * 1024 * 1024, // 100MB
});Cross-Tab Synchronization
Keep data in sync across browser tabs:
import { createBroadcaster } from '@localmode/core';
const broadcaster = createBroadcaster('my-app-sync');
// Listen for changes from other tabs
broadcaster.subscribe((message) => {
if (message.type === 'document-added') {
console.log('New document added in another tab:', message.id);
// Refresh your UI
}
});
// Broadcast changes to other tabs
await db.add({ id: 'new-doc', vector, metadata });
broadcaster.publish({
type: 'document-added',
id: 'new-doc',
});Web Locks
Prevent concurrent writes:
import { createLockManager } from '@localmode/core';
const locks = createLockManager();
// Acquire exclusive lock before writing
await locks.withLock('db-write', async () => {
await db.addMany(documents);
});
// Other tabs wait for lock to be releasedFeature Detection
Check storage capabilities:
import { isIndexedDBSupported, isWebLocksSupported } from '@localmode/core';
if (!isIndexedDBSupported()) {
console.warn('IndexedDB not available, using memory storage');
}
if (!isWebLocksSupported()) {
console.warn('Web Locks not available, using fallback');
}Write-Ahead Log (WAL)
The WAL provides crash recovery for storage operations using a Log-Execute-Commit pattern:
import { WAL, withWAL } from '@localmode/core';withWAL Utility
The simplest way to use WAL — wraps an operation with automatic logging and commit:
import { withWAL } from '@localmode/core';
await withWAL(async (wal) => {
// Log the operation before executing
await wal.log({
type: 'add',
collection: 'documents',
data: { id: 'doc-1', text: 'Hello' },
});
// Execute the actual operation
await db.add(document);
// WAL auto-commits on success, auto-rolls-back on failure
});WAL Class
For more control, use the WAL class directly:
import { WAL } from '@localmode/core';
const wal = new WAL('my-database');
// Log operation
const entryId = await wal.log({
type: 'add',
collection: 'documents',
data: myDocument,
});
// Execute operation
await storage.add(myDocument);
// Commit (mark as completed)
await wal.commit(entryId);
// On crash recovery, replay uncommitted entries
const pending = await wal.getPending();
for (const entry of pending) {
await replayOperation(entry);
await wal.commit(entry.id);
}WAL Operation Types
| Type | Description |
|---|---|
'add' | Document insertion |
'update' | Document update |
'delete' | Document deletion |
'clear' | Collection clear |
Migration System
Manage storage schema changes across versions:
import { MigrationManager, getCurrentVersion } from '@localmode/core';
import type { Migration } from '@localmode/core';Running Migrations
import { MigrationManager } from '@localmode/core';
const manager = new MigrationManager('my-database');
// Run all pending migrations
await manager.migrate();
// Check current version
const version = await manager.getCurrentVersion();
console.log(`Database at version ${version}`);Migration Type
interface Migration {
version: number;
description: string;
migrate: (db: IDBDatabase, transaction: IDBTransaction) => void | Promise<void>;
}Built-in Migrations
The MIGRATIONS array contains all built-in schema migrations for the VectorDB storage layer. These run automatically when opening a database.
import { MIGRATIONS, getCurrentVersion } from '@localmode/core';
console.log(`Latest version: ${getCurrentVersion()}`);
console.log(`Total migrations: ${MIGRATIONS.length}`);Migration Rules
- Never modify existing migrations — always add new ones
- Migrations must be idempotent — safe to run multiple times
- Test rollback scenarios — ensure data integrity on failure
- Version numbers must be sequential — no gaps allowed
Best Practices
Storage Tips
- Always use fallbacks - Safari private browsing blocks IndexedDB
- Request persistence - Prevent auto-clearing of important data
- Monitor quota - Show warnings before storage is full
- Clean up - Remove old data periodically
- Use locks - Prevent race conditions across tabs
- Use WAL for critical writes - Protect against crashes during bulk operations
Next Steps
Security
Encrypt sensitive data before storing.
Middleware
Add caching and logging to storage operations.