nostr-idb Package
The nostr-idb package provides a browser-based IndexedDB storage solution for Nostr events, allowing you to cache events locally in web applications built with AppleSauce.
Overview
nostr-idb is a lightweight IndexedDB wrapper specifically designed for storing and querying Nostr events in web browsers. The NostrIDB class provides a full relay-like API with automatic batching, in-memory index caching, and subscription management.
The package integrates seamlessly with AppleSauce's event store system, providing persistent local caching that improves application performance and enables offline functionality.
Complete API Documentation
For complete API documentation, see the nostr-idb npm package.
Key Features
- Browser-based Storage: Uses IndexedDB for persistent storage in web browsers
- In-Memory Index Caching: Caches indexes in memory for dramatically faster repeated queries
- Automatic Batching: Automatically batches writes for optimal performance
- Relay-like API: Complete nostr relay-like interface with subscriptions
- Automatic Pruning: Can automatically prune old events to manage storage
- Built for Performance: Built directly on top of IndexedDB for the lowest latency
Installation
npm install nostr-idbyarn install nostr-idbpnpm install nostr-idbBasic Usage
Setting Up with AppleSauce
The NostrIDB class integrates with AppleSauce's event loaders through a cache request function:
import { EventStore } from "applesauce-core";
import { persistEventsToCache } from "applesauce-core/helpers";
import { createEventLoaderForStore } from "applesauce-loaders/loaders";
import { RelayPool } from "applesauce-relay";
import { NostrIDB } from "nostr-idb";
// Create your event store and relay pool
const eventStore = new EventStore();
const pool = new RelayPool();
// Create a NostrIDB instance (creates its own database automatically)
// You can optionally pass configuration options:
const nostrIDB = new NostrIDB({
cacheIndexes: 1000, // Cache 1000 indexes in memory for faster queries
batchWrite: 1000, // Batch writes for better performance
writeInterval: 100, // Write interval in ms
maxEvents: 10000, // Maximum events to store
});
// Start the database (starts background processes)
await nostrIDB.start();
// Create a cache request function for event loaders
const cacheRequest = (filters) => nostrIDB.filters(filters);
// Automatically persist new events to the cache
persistEventsToCache(eventStore, async (events) => {
await Promise.allSettled(
events.map(async (event) => {
await nostrIDB.add(event);
}),
);
});
// Use the cache with event loaders
createEventLoaderForStore(eventStore, pool, {
cacheRequest,
lookupRelays: ["wss://purplepag.es/", "wss://index.hzrd149.com/"],
});How It Works
- Database Initialization: Create a
NostrIDBinstance (it will create its own IndexedDB database automatically) and callstart()to begin background processes - In-Memory Index Caching: Indexes are cached in memory (configurable via constructor options) for dramatically faster repeated queries
- Automatic Batching: Events are automatically batched for optimal write performance
- Cache Integration: The
cacheRequestfunction usesnostrIDB.filters()which returns aPromise<NostrEvent[]>- this queries the cache before event loaders query relays - Event Persistence:
persistEventsToCache()automatically saves new events to the cache as they're added to the event store
Integration Patterns
With Event Loaders
The most common use case is integrating the cache with AppleSauce's event loaders:
import { EventStore } from "applesauce-core";
import { Filter } from "applesauce-core/helpers";
import { persistEventsToCache } from "applesauce-core/helpers";
import { createEventLoaderForStore } from "applesauce-loaders/loaders";
import { RelayPool } from "applesauce-relay";
import { NostrIDB } from "nostr-idb";
const eventStore = new EventStore();
const pool = new RelayPool();
// Initialize NostrIDB (creates its own database)
const nostrIDB = new NostrIDB();
await nostrIDB.start();
// Create cache request function
const cacheRequest = (filters: Filter[]) => nostrIDB.filters(filters);
// Persist events to cache
persistEventsToCache(eventStore, async (events) => {
await Promise.allSettled(
events.map(async (event) => {
await nostrIDB.add(event);
}),
);
});
// Use cache with event loader
createEventLoaderForStore(eventStore, pool, {
cacheRequest,
lookupRelays: ["wss://relay.example.com"],
});With Timeline Loaders
You can also use the cache with timeline loaders:
import { createTimelineLoader } from "applesauce-loaders/loaders";
import { NostrIDB } from "nostr-idb";
const nostrIDB = new NostrIDB();
await nostrIDB.start();
const cacheRequest = (filters) => nostrIDB.filters(filters);
// Persist events to cache
persistEventsToCache(eventStore, async (events) => {
await Promise.allSettled(
events.map(async (event) => {
await nostrIDB.add(event);
}),
);
});
// Create timeline loader with cache
const timeline = createTimelineLoader(
pool,
["wss://relay.example.com"],
{ kinds: [1] },
{ eventStore, cache: cacheRequest },
);Performance Considerations
In-Memory Index Caching
The most important performance feature for IndexedDB is in-memory index caching. The cacheIndexes option controls how many indexes are cached in memory:
const nostrIDB = new NostrIDB({
cacheIndexes: 1000, // Default: 1000 indexes cached in memory
});Why this matters:
- IndexedDB queries can be slow when reading from disk
- Caching indexes in memory dramatically speeds up repeated queries
- Higher values use more memory but provide faster queries
- For applications with many repeated queries, increase
cacheIndexes
Other Performance Features
- Automatic Batching: Events are automatically batched (configurable via
batchWriteandwriteIntervalconstructor options) for optimal write performance - Deduplication:
add()automatically handles event deduplication - Indexed Queries: IndexedDB indexes make filter queries efficient
- Memory Usage: Events are stored in IndexedDB, not in memory, reducing memory footprint
- Automatic Pruning: The
NostrIDBclass can automatically prune old events to stay withinmaxEventslimit - Count Method: Use
nostrIDB.count(filters)to get event counts matching filters (returns a Promise that can be used with RxJS observables)
Limitations
- Browser Only: IndexedDB is only available in browser environments, not in Node.js
- Storage Limits: Subject to browser storage quotas (typically 50-100MB per origin)
- No Full-Text Search: Unlike SQLite implementations,
nostr-idbdoesn't provide full-text search capabilities - Lifecycle Management: Requires calling
start()before use andstop()when done to properly manage background processes
