# client A nostr web application framework built on [NDK (Nostr Development Kit)](https://github.com/nostr-dev-kit/ndk). Uses a SharedWorker architecture where a single NDK instance manages all relay connections, caching, and subscriptions across multiple browser tabs. ## Architecture Overview ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ index.html │ │ post.html │ │ cashu.html │ ... (any page) │ init-ndk.mjs│ │ init-ndk.mjs│ │ init-ndk.mjs│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ postMessage │ │ └─────────────┬────┘─────────────────┘ ▼ ┌─────────────────┐ │ ndk-worker.js │ (SharedWorker — single instance) │ ┌─────────────┐ │ │ │ NDK │ │ │ │ + Dexie │ │ ← IndexedDB cache (ndk-shared) │ │ + Signer │ │ │ └──────┬──────┘ │ └─────────┼────────┘ ▼ ┌────────────────────────┐ │ Nostr Relay Pool │ │ wss://relay1 relay2 │ └────────────────────────┘ ``` ## Event Management Strategy This section defines **when and how** nostr events are fetched, cached, and subscribed to. It serves as the single source of truth for event lifecycle management. ### Cache Behavior All events flowing through NDK are automatically cached in **IndexedDB via the Dexie adapter** (database: `ndk-shared`). This means: - Any `ndk.fetchEvents()` call caches results automatically - Any subscription event received is cached via `cacheAdapter.setEvent()` - Pages can query the cache directly via `queryCache()` without hitting relays - NDK's `CACHE_FIRST` mode returns cached data immediately, then updates from relays ### Startup Events (fetched by worker on init) These events are fetched once during `handleInit()` when the first tab connects. They populate the Dexie cache and provide essential app-wide state. | Priority | Kind | NIP | Name | Filter | Subscription | Purpose | |----------|------|-----|------|--------|-------------|---------| | 1 | `0` | NIP-01 | User Metadata | `authors:[pubkey]` | No — one-shot | Profile display (name, picture, about) | | 2 | `10002` | NIP-65 | Relay List | `authors:[pubkey], limit:1` | **Yes** — persistent `closeOnEose:false` | Configure relay pool; live updates | | 3 | `30078` | NIP-78 | App Settings | `authors:[pubkey], #d:[user-settings]` | No — one-shot | Encrypted user preferences (NIP-44) | | 4 | `17375` | NIP-60 | Cashu Wallet | `authors:[pubkey]` | **Yes** — startup fetch + persistent | Wallet metadata, mint list, and live wallet changes | | 5 | `7375` | NIP-60 | Cashu Tokens | `authors:[pubkey]` | **Yes** — startup fetch + persistent | Unspent proofs and live token changes | | 6 | `5` | NIP-09 | Event Deletion | `authors:[pubkey], #k:[7375]` | **Yes** — startup fetch + persistent | Track spent/deleted tokens | **Startup sequence (current implementation):** ``` handleInit(pubkey) ├── initNDK() // Create NDK, connect relays ├── hydrateUserSettingsForPubkey() // kind 30078 ├── fetchUserProfile() // kind 0 ├── fetchUserRelays() // kind 10002 (one-shot) ├── subscribe kind 10002 // persistent relay list updates ├── fetchStartupEventDownloads() // one-shot preload: kinds 3, 17375, 7375, 5(#k=7375) └── ensureStartupWalletSubscriptions() // persistent wallet subscriptions: 17375, 7375, 5(#k=7375) ``` This means wallet information is not only prefetched at startup; it is now also continuously updated by long-lived worker subscriptions. ### On-Demand Events (fetched by individual pages) These events are fetched when a specific page loads. They benefit from the Dexie cache — if the same events were previously fetched, they're served from cache first. | Kind | NIP | Name | Pages | Pattern | |------|-----|------|-------|---------| | `0` | NIP-01 | User Metadata | profile, msg, post, npub | `fetchEventsFromAllRelays` or `subscribe` | | `1` | NIP-01 | Short Text Notes | post, index, links, strudel, template | `subscribe` with `CACHE_FIRST` | | `3` | NIP-02 | Contact List | post, profile, cashu | `subscribe` + `queryCache` + `fetchEventsFromAllRelays` | | `4` | NIP-04 | Encrypted DMs | msg | `subscribe` with `CACHE_FIRST` | | `1059` | NIP-44 | Gift Wrap (DMs) | msg | `subscribe` with `CACHE_FIRST` | | `5` | NIP-09 | Event Deletion | (via startup sub) | Persistent subscription | | `7` | NIP-25 | Reactions | post (via interactions) | `fetchEventsFromAllRelays` | | `6` | NIP-18 | Reposts | post (via interactions) | `fetchEventsFromAllRelays` | | `9735` | NIP-57 | Zap Receipts | post (via interactions) | `fetchEventsFromAllRelays` | | `10019` | NIP-60 | Mint List | cashu (mint discovery) | `fetchEventsFromAllRelays` | | `10063` | NIP-96 | Blossom Server List | blobs | `subscribe` with `CACHE_FIRST` | | `17375` | NIP-60 | Cashu Wallet | cashu (full wallet ops) | Via NDKCashuWallet | | `7375` | NIP-60 | Cashu Tokens | cashu (full wallet ops) | Via NDKCashuWallet (cache-first) | | `7376` | NIP-60 | Spending History | cashu (transaction list) | Via NDKCashuWallet | | `30023` | NIP-23 | Long-form Content | note | `subscribe` with `CACHE_FIRST` | | `30024` | NIP-23 | Draft Long-form | note | `subscribe` with `CACHE_FIRST` | | `30078` | NIP-78 | App-specific Data | todo, cal, links, post (viewed) | `subscribe` or `fetchEvents` | | `38000` | NIP-89 | Recommendations | cashu (mint discovery) | `fetchEventsFromAllRelays` | | `38421` | — | AI Service Announcements | ai | `subscribe` with `CACHE_FIRST` | ### Subscription Patterns The app uses three distinct patterns for fetching events: #### 1. One-shot Fetch ```javascript // Fetch once, cache result, done const events = await ndk.fetchEvents(filter); ``` Used for: startup profile, relay list, wallet definition, user settings #### 2. Subscribe with CACHE_FIRST ```javascript // Returns cached events immediately, then live events from relays subscribe(filter, { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }); ``` Used for: most page-level subscriptions (posts, contacts, notes, DMs) #### 3. Persistent Worker Subscription ```javascript // Long-lived subscription in the worker, survives page navigation ndk.subscribe(filter, { closeOnEose: false }); ``` Used for: relay list updates (kind 10002), wallet metadata (kind 17375), token events (kind 7375), and token deletions (kind 5 with #k=7375) ### Cache-First Advantage Because startup events are fetched early and cached in Dexie, subsequent page loads benefit: - **post.html** subscribes to kind 3 (contacts) → if profile.html already fetched it, cache hit - **cashu.html** starts NDKCashuWallet → kind 7375 tokens already in cache from startup → instant optimistic balance - **Any page** fetching kind 0 (profile) → already cached from startup ### Modifying Event Timing To change when an event kind is fetched: 1. **Move to startup:** Add the fetch to `fetchStartupWalletState()` or create a new startup function in `handleInit()`. This pre-populates the cache for all pages. 2. **Move to on-demand:** Remove from startup, let individual pages fetch as needed. First load will be slower but startup will be faster. 3. **Add a subscription:** For events that change frequently, add a persistent subscription in the worker. This keeps the cache fresh without page-level polling. **Trade-offs:** - More startup events = slower initial load, faster page navigation - Fewer startup events = faster initial load, slower first page that needs the data - Persistent subscriptions = always fresh data, more relay bandwidth ## Project Structure ``` www/ ├── ndk-worker.js # SharedWorker — NDK instance, cache, subscriptions ├── js/ │ ├── init-ndk.mjs # Client-side NDK API (shared by all pages) │ ├── relay-ui.mjs # Footer relay status + balance display │ ├── cashu-wallet.mjs # Cashu wallet controller (cashu.html only) │ ├── post-interactions.mjs # Social interactions (reactions, reposts, zaps) │ └── utilities.mjs # Shared utilities ├── css/client.css # Shared styles ├── *.html # Individual pages └── ndk-core.bundle.js # Bundled NDK library build/ ├── ndk-entry.js # Bundle entry point └── ndk-core.bundle.js # Built bundle ndk/ # NDK source (submodule/clone) ├── core/ # Core NDK library ├── wallet/ # Cashu wallet implementation ├── cache-browser/ # Browser cache adapter ├── cache-dexie/ # Dexie (IndexedDB) cache └── ... plans/ # Architecture and implementation plans ``` ## Building ```bash # Build the NDK bundle node build-ndk-bundle.js # Serve locally python serve_local.py ```