Event Database
The Event Database is the core persistence layer of AppleSauce that enables automatic storage and retrieval of Nostr events. The EventStore
can use an event database to persist events between application sessions, while maintaining the same API regardless of the underlying storage mechanism.
Overview
The Event Database system provides a clean abstraction layer that allows the EventStore
to work with any storage backend while maintaining consistent behavior. When no database is provided, the EventStore
automatically falls back to using only the in-memory EventMemory
for storage.
Architecture
The EventStore acts as a smart wrapper around the event database, providing high-level features while the database handles the core storage operations. When no database is provided, the EventStore uses only EventMemory for storage.
Database Interface
The event database system is built around two core interfaces:
IEventDatabase
(Synchronous)
interface IEventDatabase {
// Read operations
hasEvent(id: string): boolean;
getEvent(id: string): NostrEvent | undefined;
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[] | undefined;
getByFilters(filters: Filter | Filter[]): NostrEvent[];
getTimeline(filters: Filter | Filter[]): NostrEvent[];
// Write operations
add(event: NostrEvent): NostrEvent;
remove(event: string | NostrEvent): boolean;
update?(event: NostrEvent): void;
}
IAsyncEventDatabase
(Asynchronous)
interface IAsyncEventDatabase {
// Read operations (all return Promises)
hasEvent(id: string): Promise<boolean>;
getEvent(id: string): Promise<NostrEvent | undefined>;
hasReplaceable(kind: number, pubkey: string, identifier?: string): Promise<boolean>;
getReplaceable(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent | undefined>;
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent[] | undefined>;
getByFilters(filters: Filter | Filter[]): Promise<NostrEvent[]>;
getTimeline(filters: Filter | Filter[]): Promise<NostrEvent[]>;
// Write operations (all return Promises)
add(event: NostrEvent): Promise<NostrEvent>;
remove(event: string | NostrEvent): Promise<boolean>;
update?(event: NostrEvent): void;
}
EventStore Integration
The EventStore
automatically integrates with any database that implements these interfaces:
With Database (Persistent Storage)
import { EventStore } from "applesauce-core";
import { BetterSqlite3EventDatabase } from "applesauce-sqlite/better-sqlite3";
// Create a persistent database
const database = new BetterSqlite3EventDatabase("./events.db");
// EventStore automatically uses the database for persistence
const eventStore = new EventStore(database);
// Events are automatically persisted
eventStore.add(someEvent);
Without Database (Memory Only)
import { EventStore } from "applesauce-core";
// No database provided - uses EventMemory only
const eventStore = new EventStore();
// Events are stored in memory only (lost on restart)
eventStore.add(someEvent);
With Async Database
import { AsyncEventStore } from "applesauce-core";
import { LibsqlEventDatabase } from "applesauce-sqlite/libsql";
// Create an async database
const database = new LibsqlEventDatabase("file:./events.db");
await database.initialize();
// Use AsyncEventStore for async databases
const eventStore = new AsyncEventStore(database);
// All operations are async
await eventStore.add(someEvent);
Hybrid Architecture
The EventStore uses a hybrid approach that combines persistent storage with in-memory caching:
Memory Layer (EventMemory)
- Purpose: Ensures single event instances and provides fast access
- Features: LRU cache, event deduplication, symbol-based caching
- Always Present: Even with a database, EventMemory is used for caching
Database Layer (Optional)
- Purpose: Provides persistent storage between application sessions
- Features: Full event persistence, efficient querying, search capabilities
- Optional: If not provided, only EventMemory is used
Event Flow
When an event is added to the EventStore:
EventStore receives the event and performs high-level processing:
- Handles kind 5 delete events by removing referenced events
- Manages replaceable events by removing old versions
- Validates events if a verification function is provided
- Tracks expiration timestamps for automatic cleanup
EventMemory ensures single event instances and provides fast access:
- Deduplicates events by ID
- Maintains LRU cache for performance
- Enables symbol-based caching
Event Database (if provided) handles persistent storage:
- Stores events for retrieval between application sessions
- Provides efficient querying capabilities
- Maintains indexes for fast filtering
Key Features
Event Storage
The primary role of an event database is to store and retrieve Nostr events:
const eventStore = new EventStore(database);
// Event is stored in the database
const event = eventStore.add(nostrEvent);
// Event can be retrieved after application restart
const retrieved = eventStore.getEvent(event.id);
EventStore Features
The EventStore provides high-level features on top of the database:
Delete Event Handling (Kind 5)
// When a kind 5 delete event is added, EventStore automatically:
// 1. Removes all events referenced in the delete event
// 2. Tracks deleted event IDs to prevent re-adding
const deleteEvent = eventStore.add(kind5DeleteEvent);
// Referenced events are automatically removed from the database
Replaceable Event Management
// EventStore automatically removes old versions of replaceable events
const profile1 = eventStore.add(profileEventV1);
const profile2 = eventStore.add(profileEventV2); // Newer version
// Old version is automatically removed from database
// Only the latest version remains accessible
const latestProfile = eventStore.getReplaceable(0, pubkey);
Event Deduplication
// EventMemory ensures single event instances
const event1 = eventStore.add(eventFromRelay1);
const event2 = eventStore.add(eventFromRelay2); // Same event, different relay
console.log(event1 === event2); // true - same instance
Efficient Querying
Databases provide optimized querying capabilities:
// Query events by filters
const notes = eventStore.getByFilters({
kinds: [1],
authors: ["pubkey1", "pubkey2"],
});
// Get timeline (chronologically sorted)
const timeline = eventStore.getTimeline({
kinds: [1],
limit: 100,
});
Database Implementations
The applesauce-sqlite
package provides several database implementations:
SQLite Implementations
- Better SQLite3: High-performance Node.js implementation
- Native SQLite: Uses Node.js built-in SQLite module
- Bun SQLite: Optimized for Bun runtime
- LibSQL: Supports local and remote databases
Custom Implementations
You can create custom database implementations:
class CustomEventDatabase implements IEventDatabase {
hasEvent(id: string): boolean {
// Your implementation
}
getEvent(id: string): NostrEvent | undefined {
// Your implementation
}
add(event: NostrEvent): NostrEvent {
// Your implementation
}
remove(event: string | NostrEvent): boolean {
// Your implementation
}
// ... implement all required methods
}
// Use with EventStore
const customDb = new CustomEventDatabase();
const eventStore = new EventStore(customDb);
Best Practices
Choose the Right Database
- Development: Use memory-only for fast iteration
- Production: Use persistent database for data retention
- High Performance: Use hybrid approach with optimized database
Handle Async Databases
// Always use AsyncEventStore with async databases
const asyncDb = new LibsqlEventDatabase("file:./events.db");
await asyncDb.initialize();
const eventStore = new AsyncEventStore(asyncDb);
// All operations are async
await eventStore.add(event);
const retrieved = await eventStore.getEvent(id);
Memory Management
// For long-running applications, periodically prune memory
setInterval(() => {
const pruned = eventStore.prune(1000); // Remove 1000 unclaimed events
console.log(`Pruned ${pruned} events`);
}, 60000); // Every minute