# FolderSyncDB — Nostr + Folder Variant Local-first browser key-value and document store with **dual-sync**: shared folder for local/offline use, plus Nostr relays for cross-device reach over the internet. ## Why this variant? The other variants sync browsers via a shared folder on disk. This only works when both browsers can access the same folder (same machine or network drive). This variant adds **Nostr relay sync** as a second transport. Nostr relays are public WebSocket servers that relay messages between clients. Your browser connects directly to them — no server of your own, no accounts, no setup. ``` Laptop (file://) Phone (browser) ┌──────────┐ ┌──────────┐ │ IndexedDB │ │ IndexedDB │ └─────┬─────┘ └─────┬─────┘ │ │ ┌────┴────┐ ┌────┴────┐ │ Sync │ │ Sync │ │ Engine │ │ Engine │ └──┬───┬──┘ └──┬───┬──┘ │ │ │ │ Folder Nostr ──── wss://relay ──────── Nostr Folder ``` Use either or both: - **Folder only** — local multi-browser sync, works offline - **Nostr only** — cross-device sync, no folder needed - **Both** — folder for local speed, Nostr for internet reach ## How it works **On write:** 1. Update IndexedDB (fast) 2. Write event file to folder (if folder connected) 3. Publish event to Nostr relay (if room joined) **On sync:** 1. Scan folder for new events from other local browsers 2. Check Nostr relay cache for events from remote devices 3. Merge both, deduplicate by filename 4. Apply unseen events to IndexedDB 5. Bridge: folder events get published to Nostr, Nostr events get written to folder **Real-time push:** When a Nostr subscription receives a new event, `sync()` is triggered immediately — no waiting for the polling interval. ## Quick start ```ts import { FolderSyncDB } from './nostr/src/index.ts'; const db = await FolderSyncDB.open({ autoSyncIntervalMs: 5000, relays: [ // optional, defaults to popular public relays 'wss://relay.damus.io', 'wss://nos.lol', 'wss://relay.nostr.band', ], }); // Local folder sync (same as other variants) await db.selectFolder(); // Cross-device sync via Nostr await db.joinRoom('my-shared-room-key'); // Use normally await db.kv.set('theme', 'dark'); const theme = await db.kv.get('theme'); // Listen for events db.on('nostr:connected', ({ roomKey }) => console.log('joined:', roomKey)); db.on('change', (e) => console.log('changed:', e)); ``` ## API additions On top of the standard FolderSyncDB API, this variant adds: | Method | Description | |--------|-------------| | `joinRoom(roomKey)` | Join a Nostr sync room. All clients with the same key sync together. | | `leaveRoom()` | Disconnect from the current room. | | `isConnected()` | Whether a room is currently joined and relay is connected. | | `currentRoom` | The current room key, or `null`. | ### OpenOptions additions | Option | Type | Default | Description | |--------|------|---------|-------------| | `relays` | `string[]` | 3 popular public relays | Nostr relay WebSocket URLs | | `roomKey` | `string` | none | Auto-join this room on open | ### Events additions | Event | Payload | When | |-------|---------|------| | `nostr:connected` | `{ roomKey }` | Joined a Nostr room | | `nostr:disconnected` | -- | Left a Nostr room | ## Room key The room key is simply a shared string that identifies your sync group. It's used as a Nostr tag — all clients subscribed to the same tag receive each other's events. - Anyone who knows the room key can join and sync - Events are not encrypted (v1) — use random room keys for privacy through obscurity - Each client generates its own Nostr keypair (stored in IndexedDB) ## Works from `file://` Nostr relays use WebSocket (`wss://`), which works from `file://` origins in Chrome. Unlike WebRTC or `fetch()`, browsers don't block outgoing WebSocket connections from `file://` pages. ## Dependencies - `nostr-tools` — lightweight Nostr protocol library (keypair generation, event signing, relay pool management). Pure JS, no WASM. ## Local cache Uses IndexedDB (same as the `indexeddb/` variant). Zero-overhead, browser-managed persistence.