Jason Tudisco fb3147f0c9 Inventory-based set reconciliation for Nostr sync
Replace timestamp-based catch-up with proper set reconciliation.
When a peer joins a room it publishes its full filename inventory.
Other peers diff against their own set and re-publish only the
events the newcomer is missing, then send their own inventory so
the newcomer can do the same. Two rounds max, no infinite loops.

Also switched from custom #channel tag (rejected by relays as
unindexed) to standard #t topic tag, and kind 4078 to kind 1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:09:39 -06:00
..

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

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.