2026-04-28 12:33:29 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00
2026-04-17 16:52:51 -04:00

client

A nostr web application framework built on NDK (Nostr Development Kit). 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

// 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

// 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

// 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

# Build the NDK bundle
node build-ndk-bundle.js

# Serve locally
python serve_local.py
Description
A nostr web application framework built on NDK (Nostr Development Kit). Uses a SharedWorker architecture where a single NDK instance manages all relay connections, caching, and subscriptions across multiple browser tabs.
Readme 21 MiB
Languages
JavaScript 78.9%
HTML 19.6%
CSS 1.3%
Shell 0.1%