# 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
```
