Root README: updated variant table, API docs with joinRoom/leaveRoom, Nostr events, build instructions. Paste README: added paste-nostr description and cross-device sync info. Nostr README: full documentation for dual-sync architecture. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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:
- Update IndexedDB (fast)
- Write event file to folder (if folder connected)
- Publish event to Nostr relay (if room joined)
On sync:
- Scan folder for new events from other local browsers
- Check Nostr relay cache for events from remote devices
- Merge both, deduplicate by filename
- Apply unseen events to IndexedDB
- 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
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.