Nostr transport: - Switch from kind 1 to NIP-78 kind 30078 (application-specific data) - Relays store events persistently — no inventory protocol needed - AES-256-GCM encryption via Web Crypto API (room key = shared secret) - PBKDF2 key derivation (100k iterations) from room key - Chunking for large events (images >48KB) with per-chunk encryption - Auto re-publish missing local events on room join - Manual "Re-sync" button for recovery of failed publishes Performance (all variants): - Emit 'change' immediately after local store write (IDB/NeDB/SQLite) - Folder and Nostr writes run fire-and-forget in background - Fast event hashing: fingerprint metadata only, skip full payload SHA-256 - Saving spinner in paste UI while write completes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
IndexSyncFile — IndexedDB Variant
Local-first key-value and document store using IndexedDB as the local cache, syncing with a user-selected folder via the File System Access API.
Why IndexedDB
- Zero dependencies — uses the browser's built-in IndexedDB API directly
- Persistent by default — IndexedDB data survives page reloads and browser restarts without any extra work
- No WASM loading — instant startup, no async module initialization
- Battle-tested — IndexedDB is the most widely supported browser storage API
Architecture
Write: app ---> IndexedDB (immediate) ---> /events/timestamp_hash.json
Sync: /events/*.json ---> sort by timestamp ---> skip applied ---> apply to IndexedDB
IndexedDB serves as both the fast query layer and the local persistence layer. The folder's event log is the sync mechanism that keeps multiple browsers converged.
IDB schema (5 object stores)
| Store | Key | Purpose |
|---|---|---|
kv |
key |
Key-value records |
docs |
[store, id] |
Document collection records |
idx |
[store, indexName, id] |
Secondary index entries |
applied |
filename |
Tracks which event files have been processed |
meta |
key |
Client ID, directory handle persistence |
The FileSystemDirectoryHandle is stored directly in the meta IDB store via structured clone, so the user doesn't need to re-select the folder on every page load.
Install and build
npm install
npm run build
Usage
import { FolderSyncDB } from './dist/index.js';
const db = await FolderSyncDB.open({
dbName: 'MyApp',
autoSyncIntervalMs: 5000,
});
await db.selectFolder();
// KV
await db.kv.set('user', { name: 'Alice' });
// Collections
const notes = db.collection({
name: 'notes',
indexes: [{ name: 'byTag', fields: ['tag'] }],
});
await notes.put({ id: 'n1', title: 'Hello', tag: 'work' });
// Exact match
const workNotes = await notes.findByIndex('byTag', 'work');
// Range query (IDB compound key ranges)
const recent = await notes.queryByIndex('byDate', { gte: '2024-01-01' });
Index implementation
Secondary indexes are maintained in a dedicated idx IDB object store using compound keys [store, indexName, id]. An IDB index on [store, indexName, value] enables efficient exact-match and range queries via IDBKeyRange.
Index entries are updated atomically with document writes inside a single IDB transaction. On first use of a collection, indexes are rebuilt from existing documents.
Variant-specific notes
- Directory handle is persisted in IDB (no separate storage needed)
- IDB transactions are used for atomic document + index updates
- No external dependencies