Files
client/plans/ndk-migration-plan.md
2026-04-17 16:52:51 -04:00

29 KiB

NDK Migration Plan: Distributed Web Apps with Shared NDK Worker

Executive Summary

This plan outlines the migration of the existing custom Nostr client implementation (client/) to use NDK (Nostr Development Kit) with Dexie cache. The migration will modernize the codebase, reduce maintenance burden, and leverage NDK's built-in features for relay management, subscriptions, and caching.

Primary Goal: Create independently distributable web pages in www/ that share a common NDK SharedWorker

Key Architectural Requirements:

  1. Each page must be self-contained and independently accessible
  2. All pages will be created in www/ directory (using existing upload_www.sh)
  3. Preserve all aesthetics from client/ - same layouts, CSS, page formatting
  4. Users should not notice backend changes - only improved performance/reliability

Technical Requirements:

  • Users can visit any page directly (e.g., /www/profile.html)
  • Each page checks authentication and launches nostr-login-lite if needed
  • All pages share the same NDK SharedWorker for relay management
  • Pages can be distributed individually or as a complete suite
  • No duplication of relay connections when multiple pages are open

Architecture Comparison

Current Custom Implementation

graph TB
    A[client/index.html] --> B[nostr-login-lite]
    A --> C[client/js/init.mjs]
    C --> D[client/shared_worker.mjs]
    D --> E[Custom WebSocket Management]
    D --> F[client/js/db.js - Custom Dexie Schema]
    A --> G[liveQuery Observables]
    G --> F
    E --> H[Manual Relay Connections]
    E --> I[Manual Subscription Management]
    D --> J[Message-based Signing via window.nostr]

Key Components:

  • Authentication: nostr-login-lite → window.nostr facade
  • SharedWorker: Custom implementation with manual relay/subscription management
  • Database: Custom Dexie schema (DB_NOSTR) with specific event tables
  • Relay Management: Manual WebSocket connections, reconnection logic, exponential backoff
  • Subscriptions: Custom subscription queue system with authentication-aware processing
  • Data Flow: SharedWorker updates DB → liveQuery observables → UI updates

Target NDK Implementation

graph TB
    A[Any Page: index.html, profile.html, feed.html, etc.] --> B[nostr-login-lite]
    A --> C[js/init-ndk.mjs - Shared Init Module]
    C --> D[ndk_worker.mjs - Shared NDK Worker]
    D --> E[NDK Instance Singleton]
    E --> F[NDKPool - Relay Management]
    E --> G[NDKCacheBrowser with Dexie Adapter]
    A --> H[NDK Event Subscriptions]
    H --> I[Event Handlers]
    F --> J[Automatic Relay Connections]
    F --> K[Automatic Subscription Deduplication]
    D --> L[Message-based Communication]

Key Components:

  • Authentication: nostr-login-lite → window.nostr facade (unchanged)
  • SharedWorker: NDK-based worker shared by ALL pages
  • Database: NDK's Dexie cache adapter (ndk-client) shared by ALL pages
  • Relay Management: NDKPool handles connections, reconnection, health monitoring
  • Subscriptions: NDK handles deduplication, caching, and relay selection
  • Data Flow: NDK subscriptions → event handlers → UI updates
  • Independent Pages: Each page can be accessed directly, authenticates if needed

Key Differences Analysis

Aspect Custom Implementation NDK Implementation Migration Impact
Relay Connections Manual WebSocket management, custom reconnection logic NDKPool with built-in connection management Remove ~500 lines of WebSocket code
Subscriptions Custom queue system, manual placeholder replacement NDK subscriptions with automatic deduplication Simplify subscription logic significantly
Caching Custom Dexie schema per event kind NDK standardized cache schema Start with fresh NDK cache
Event Storage Separate tables per kind (Event_0, Event_3, etc.) Unified events table with indexes Major schema change
Authentication Custom message-based signing Can use NDK signer or keep message-based Keep existing pattern for compatibility
Relay Selection Manual relay list from Event 10002 NDK outbox model with automatic relay selection Can leverage NDK's smarter relay selection
Error Handling Custom error handling per operation NDK built-in error handling and retry logic Simplified error handling
Page Independence Requires launcher page Each page independently accessible Major architectural improvement

Implementation Strategy

Distributed Architecture with Shared Worker

Each page is self-contained and independently accessible, but all pages share the same NDK SharedWorker.

Directory Structure:

www/
├── index.html (launcher page with app grid - NDK version)
├── ndk-worker.js (shared NDK worker - already exists, enhance it)
├── ndk-core.bundle.js (NDK bundle - already exists)
├── profile.html (standalone profile page)
├── feed.html (standalone feed page)
├── msg.html (standalone messaging page)
├── post.html (standalone post composer)
├── relays.html (standalone relay manager)
├── db.html (standalone database viewer)
├── ... (other standalone pages)
├── css/
│   └── client.css (copied from client/css/client.css)
├── js/
│   └── (any additional shared JS modules)
└── (nostr-login-lite already available)

client/ (reference for aesthetics)
├── index.html (reference for layout/styling)
├── css/
│   └── client.css (source of truth for styling)
└── html/
    └── (reference pages for layout/functionality)

Key Principles:

  1. Self-Contained Pages: Each HTML page includes authentication check
  2. Shared Worker: All pages connect to same ndk-worker.js
  3. Shared Cache: All pages use same NDK Dexie database (ndk-client)
  4. Shared Authentication: nostr-login-lite state shared across tabs
  5. Independent Distribution: Each page can be distributed separately
  6. Preserve Aesthetics: Copy CSS, layouts, and styling from client/
  7. Backend Only Changes: Users see same UI, just better performance

Benefits:

  • Users can bookmark/share specific tools (e.g., just the profile editor)
  • No need to go through launcher to access functionality
  • Shared worker prevents duplicate relay connections
  • Shared cache means instant data availability across pages
  • Modular architecture - easy to add/remove pages
  • Flexible distribution options
  • Familiar UI: Users see same interface they're used to
  • Easy deployment: Use existing upload_www.sh script

Detailed Implementation Plan

Step 1: Enhance Existing NDK SharedWorker

File: www/ndk-worker.js (already exists, enhance it)

Key Features:

  • Initialize NDK with Dexie cache adapter
  • Handle authentication via message-based communication
  • Manage NDK subscriptions
  • Broadcast events to connected ports
  • Handle relay list from Event 10002

Architecture:

// Core structure
let ndk = null;
let cachedProfile = null;
let activePorts = new Set();
let activeSubscriptions = new Map();

// Initialize NDK with Dexie cache
async function initNDK(pubkey) {
  const cacheAdapter = new NDKCacheBrowser({
    dbName: 'ndk-client',
    forceAdapter: 'dexie'
  });
  
  ndk = new NDK({
    explicitRelayUrls: BOOTSTRAP_RELAYS,
    cacheAdapter: cacheAdapter
  });
  
  await cacheAdapter.initializeAsync(ndk);
  await ndk.connect();
}

// Handle subscriptions with NDK
async function createSubscription(filters, opts) {
  const sub = ndk.subscribe(filters, opts);
  
  sub.on('event', (event) => {
    // Broadcast to all ports
    broadcastToAllPorts({ type: 'event', data: event.rawEvent() });
  });
  
  sub.on('eose', () => {
    broadcastToAllPorts({ type: 'eose' });
  });
  
  return sub;
}

Step 2: Copy CSS and Create Shared Utilities

Files to Copy:

  • client/css/client.csswww/css/client.css
  • client/css/*.ttfwww/css/*.ttf (fonts)

Create: www/js/init-ndk.mjs (new shared initialization module)

Key Features:

  • Check if user is already authenticated
  • Launch nostr-login-lite if not authenticated
  • Initialize NDK worker connection (shared across all pages)
  • Handle authentication with nostr-login-lite
  • Set up event listeners for worker messages
  • Provide helper functions for subscriptions
  • Path-aware worker loading - works from any page location

Architecture:

let ndkWorker = null;

/**
 * Initialize NDK for any page - handles authentication automatically
 * This function can be called from any page (index.html, profile.html, etc.)
 */
export async function initNDKPage() {
  console.log('Initializing NDK page...');
  
  // Check if already authenticated
  const authenticated = await ensureAuthenticated();
  if (!authenticated) {
    console.log('Not authenticated - launching nostr-login-lite');
    throw new Error("Authentication required");
  }
  
  console.log('User authenticated, initializing NDK worker');
  
  // All pages in www/ directory, so worker path is always the same
  const workerPath = './ndk-worker.js';
  
  // Initialize NDK SharedWorker (shared across all tabs/pages)
  ndkWorker = new SharedWorker(workerPath, { type: 'module' });
  
  // Send authentication to worker
  const pubkey = await window.nostr.getPublicKey();
  ndkWorker.port.postMessage({
    type: 'init',
    pubkey: pubkey
  });
  
  // Set up message handlers
  ndkWorker.port.onmessage = handleWorkerMessage;
  
  console.log('NDK worker initialized and connected');
  
  return { authenticated: true, worker: ndkWorker };
}

/**
 * Ensure user is authenticated - works from any page
 * If not authenticated, launches nostr-login-lite modal
 */
async function ensureAuthenticated() {
  // Check if window.nostr exists (from nostr-login-lite or extension)
  if (window.nostr) {
    try {
      await window.nostr.getPublicKey();
      return true;
    } catch (error) {
      console.log('window.nostr exists but not authenticated');
    }
  }
  
  // Launch nostr-login-lite
  if (window.NOSTR_LOGIN_LITE) {
    await window.NOSTR_LOGIN_LITE.launch();
    // Wait for authentication
    return new Promise((resolve) => {
      const checkAuth = async () => {
        if (window.nostr) {
          try {
            await window.nostr.getPublicKey();
            resolve(true);
          } catch {
            setTimeout(checkAuth, 100);
          }
        } else {
          setTimeout(checkAuth, 100);
        }
      };
      checkAuth();
    });
  }
  
  return false;
}

// Helper to create subscriptions
export function subscribe(filters, opts = {}) {
  return new Promise((resolve) => {
    const subId = generateSubId();
    
    ndkWorker.port.postMessage({
      type: 'subscribe',
      subId: subId,
      filters: filters,
      opts: opts
    });
    
    // Return subscription handle
    resolve({
      subId: subId,
      close: () => {
        ndkWorker.port.postMessage({
          type: 'unsubscribe',
          subId: subId
        });
      }
    });
  });
}

// Helper to get current user's pubkey
export async function getPubkey() {
  if (!window.nostr) {
    throw new Error('Not authenticated');
  }
  return await window.nostr.getPublicKey();
}

Step 3: Create Standalone Page Template

File: client/html/profile.html (example standalone page)

Key Features:

  • Self-contained - can be accessed directly
  • Includes authentication check
  • Connects to shared NDK worker
  • Works independently or as part of suite

Template Structure:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Profile Editor</title>
  
  <!-- Shared styles -->
  <link rel="stylesheet" href="../css/client.css">
  
  <!-- nostr-login-lite -->
  <script src="../nostr_login_lite/build/nostr-lite.js"></script>
</head>
<body>
  <div id="app">
    <!-- Page content here -->
  </div>
  
  <script type="module">
    import { initNDKPage, subscribe, getPubkey } from '../js/init-ndk.mjs';
    
    // Initialize page - handles authentication automatically
    async function init() {
      try {
        // This will authenticate user if needed
        await initNDKPage();
        
        // Now we're authenticated and connected to NDK worker
        const pubkey = await getPubkey();
        
        // Load profile data
        await loadProfile(pubkey);
        
      } catch (error) {
        console.error('Initialization failed:', error);
      }
    }
    
    async function loadProfile(pubkey) {
      // Subscribe to profile (kind 0)
      const sub = await subscribe(
        { kinds: [0], authors: [pubkey], limit: 1 },
        { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
      );
      
      // Handle profile updates
      window.addEventListener('ndkEvent', (event) => {
        if (event.detail.kind === 0 && event.detail.pubkey === pubkey) {
          updateProfileUI(event.detail);
        }
      });
    }
    
    function updateProfileUI(profile) {
      // Update UI with profile data
      const content = JSON.parse(profile.content);
      document.getElementById('name').value = content.name || '';
      document.getElementById('about').value = content.about || '';
      // etc.
    }
    
    // Start initialization
    init();
  </script>
</body>
</html>

Key Points:

  • Each page calls initNDKPage() which handles authentication
  • If user not logged in, nostr-login-lite modal appears
  • After authentication, page connects to shared NDK worker
  • All pages share same worker = no duplicate relay connections
  • All pages share same cache = instant data availability

Step 4: Create Launcher Page (index.html)

File: client/index.html

Purpose: App launcher with grid of available tools

Key Features:

  • Also self-contained (can be accessed directly)
  • Shows grid of available apps/tools
  • Each button links to standalone page
  • Also uses shared NDK worker for any data it needs

Structure:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Nostr Tools</title>
  <link rel="stylesheet" href="./css/client.css">
  <script src="./nostr_login_lite/build/nostr-lite.js"></script>
</head>
<body>
  <div id="app-launcher">
    <h1>Nostr Tools</h1>
    
    <div class="app-grid">
      <a href="./html/profile.html" class="app-button">
        <div class="app-icon">👤</div>
        <div class="app-name">Profile</div>
      </a>
      
      <a href="./html/feed.html" class="app-button">
        <div class="app-icon">📰</div>
        <div class="app-name">Feed</div>
      </a>
      
      <a href="./html/msg.html" class="app-button">
        <div class="app-icon">💬</div>
        <div class="app-name">Messages</div>
      </a>
      
      <a href="./html/post.html" class="app-button">
        <div class="app-icon">✍️</div>
        <div class="app-name">Post</div>
      </a>
      
      <!-- More app buttons... -->
    </div>
  </div>
  
  <script type="module">
    import { initNDKPage, getPubkey } from './js/init-ndk.mjs';
    
    async function init() {
      try {
        // Authenticate and connect to NDK worker
        await initNDKPage();
        
        // Show user info in header
        const pubkey = await getPubkey();
        displayUserInfo(pubkey);
        
      } catch (error) {
        console.error('Initialization failed:', error);
      }
    }
    
    init();
  </script>
</body>
</html>

Step 5: Implement Core Features (Shared Across Pages)

A. Profile Loading

// Subscribe to user profile (kind 0)
async function loadUserProfile(pubkey) {
  const sub = await subscribe(
    { kinds: [0], authors: [pubkey], limit: 1 },
    { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
  );
  
  // NDK will emit cached profile immediately, then update from relays
}

B. Relay List Management (Event 10002)

// Subscribe to relay list
async function loadRelayList(pubkey) {
  const sub = await subscribe(
    { kinds: [10002], authors: [pubkey], limit: 1 },
    { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
  );
  
  // Worker will automatically update NDK relay pool when Event 10002 received
}

C. Contacts/Follows (Event 3)

// Subscribe to contacts
async function loadContacts(pubkey) {
  const sub = await subscribe(
    { kinds: [3], authors: [pubkey], limit: 1 },
    { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
  );
}

D. Publishing Events

// Publish event via worker
async function publishEvent(unsignedEvent) {
  return new Promise((resolve, reject) => {
    const requestId = generateRequestId();
    
    // Set up response handler
    const handler = (event) => {
      if (event.detail.requestId === requestId) {
        window.removeEventListener('ndkPublishResponse', handler);
        if (event.detail.error) {
          reject(new Error(event.detail.error));
        } else {
          resolve(event.detail.result);
        }
      }
    };
    
    window.addEventListener('ndkPublishResponse', handler);
    
    // Send publish request to worker
    ndkWorker.port.postMessage({
      type: 'publish',
      requestId: requestId,
      event: unsignedEvent
    });
  });
}

Step 6: Distribution Strategy

How Pages Can Be Distributed:

Option 1: Complete Suite

Distribute entire client/ directory:

client/
├── index.html (launcher)
├── ndk_worker.mjs
├── js/ (all shared code)
├── html/ (all pages)
├── css/ (all styles)
└── nostr_login_lite/

User gets full suite with launcher.

Option 2: Individual Pages

Distribute single page with dependencies:

profile-tool/
├── profile.html (renamed to index.html)
├── ndk_worker.mjs
├── js/
│   ├── init-ndk.mjs
│   └── ndk-core.bundle.js
├── css/
│   └── client.css
└── nostr_login_lite/
    └── build/
        └── nostr-lite.js

User gets just the profile editor as standalone tool.

Option 3: Hybrid Distribution

Distribute subset of pages:

messaging-suite/
├── index.html (custom launcher for just messaging tools)
├── ndk_worker.mjs
├── js/ (shared code)
├── html/
│   ├── msg.html
│   ├── post.html
│   └── feed.html
├── css/
└── nostr_login_lite/

User gets curated set of related tools.

Key Benefits:

  • Each page is independently functional
  • Shared worker prevents resource duplication when multiple pages open
  • Shared cache means data persists across pages
  • Users can bookmark specific tools
  • Developers can distribute just what users need

NDK Cache Schema vs Custom Schema

Custom DB_NOSTR Schema

Event_0: "&pubkey"           // Profile metadata
Event_1: "&id, pubkey, created_at"  // Notes
Event_3: "&pubkey"           // Contacts
Event_10002: "&pubkey"       // Relay list
Event_10063: "&pubkey"       // User server list
Event_4: "&id, pubkey, created_at, p"  // DMs
Event_30023: "[pubkey+d], pubkey, id"  // Long-form
Event_30024: "[pubkey+d], pubkey, id"  // Drafts
Event_30078: "&d, pubkey, id"  // App-specific
Relays: "&relay, read, write, order, visible, connected"

NDK Dexie Cache Schema

events: "id, kind, pubkey, created_at, [kind+pubkey], [kind+created_at]"
eventTags: "[eventId+tag+value], eventId, tag, value"
profiles: "pubkey"
nip05: "nip05, fetchedAt"
relayStatus: "url"
unpublishedEvents: "++id, eventId"
eventRelays: "[eventId+relayUrl], eventId, relayUrl"

Key Differences:

  • NDK uses unified events table instead of per-kind tables
  • NDK has eventTags for efficient tag queries
  • NDK tracks relay status and event relay associations
  • NDK has unpublished events queue

Testing Strategy

Phase 1: Basic Functionality

  1. Authentication works with nostr-login-lite
  2. NDK worker initializes successfully
  3. Profile loads from cache and relays
  4. Event 10002 loads and updates relay pool
  5. Event 3 (contacts) loads successfully

Phase 2: Subscriptions

  1. User metadata subscription works
  2. Follows metadata subscription works
  3. Feed subscription works
  4. DM subscription works

Phase 3: Publishing

  1. Can publish notes (kind 1)
  2. Can publish profile updates (kind 0)
  3. Can publish relay list (kind 10002)
  4. Can publish DMs (kind 4)

Phase 4: Multi-tab & Independence

  1. Multiple tabs share same NDK worker
  2. Events appear in all tabs
  3. Logout works across all tabs
  4. Can access any page directly without launcher
  5. Authentication works from any entry point
  6. Shared cache works across different pages

Phase 5: Performance

  1. Cache-first loading is fast
  2. Relay connections are stable
  3. No memory leaks
  4. Subscription deduplication works

Phase 6: Distribution

  1. Individual page distribution works
  2. All dependencies included
  3. Pages work standalone
  4. Pages work together when multiple open

Implementation Checklist

Preparation

  • Copy www/ndk-worker.js to client/ndk_worker.mjs as template
  • Copy www/ndk-core.bundle.js to client/js/
  • Create client/js/init-ndk.mjs with path-aware worker loading

Core Implementation

  • Implement NDK worker with Dexie cache
  • Implement authentication flow in init-ndk.mjs
  • Add automatic nostr-login-lite launch if not authenticated
  • Add path-aware worker loading (works from /client/ and /client/html/)
  • Implement profile subscription (kind 0)
  • Implement relay list subscription (kind 10002)
  • Implement contacts subscription (kind 3)
  • Implement event publishing

Page Templates

  • Create standalone page template
  • Create launcher page (index.html)
  • Test standalone page can be accessed directly
  • Test authentication works from any page
  • Test shared worker connection from different pages

Individual Pages

  • Migrate profile.html as standalone page
  • Migrate feed.html as standalone page
  • Migrate msg.html as standalone page
  • Migrate post.html as standalone page
  • Migrate relays.html as standalone page
  • Migrate db.html as standalone page
  • Migrate remaining pages in client/html/

Testing

  • Test accessing each page directly (not through launcher)
  • Test authentication from different entry points
  • Test shared worker across multiple pages
  • Test shared cache across multiple pages
  • Test multi-tab functionality
  • Test logout across tabs
  • Test distributing individual pages

Distribution Testing

  • Test complete suite distribution
  • Test individual page distribution
  • Test subset distribution
  • Verify all dependencies included in distributions
  • Test pages work standalone
  • Test pages work together when multiple open

Risk Assessment

High Risk

  • Breaking existing functionality: Replacing entire implementation
    • Mitigation: Thorough testing before deployment

Medium Risk

  • Performance differences: NDK might perform differently than custom code

    • Mitigation: Performance testing, can optimize NDK usage
  • Learning curve: Team needs to learn NDK API

    • Mitigation: Good documentation, incremental migration
  • Path resolution issues: Worker path must work from different page locations

    • Mitigation: Test from all page locations, use path-aware loading

Low Risk

  • Authentication compatibility: nostr-login-lite should work the same

    • Mitigation: Keep existing auth pattern
  • Multi-tab issues: NDK SharedWorker pattern already proven in www/

    • Mitigation: Use proven pattern from www/ implementation

Success Criteria

Must Have

Authentication works with nostr-login-lite
Profile loads from cache and relays
Relay list (Event 10002) loads and updates NDK pool
Can publish events
Multi-tab support works
Logout works across all tabs
Each page can be accessed directly
Authentication works from any entry point
Shared worker works across all pages

Should Have

Performance equal to or better than custom implementation
All 15+ tool pages migrated
Individual page distribution works
Pages work standalone and together

Nice to Have

Improved relay selection using NDK outbox model
Better error handling and retry logic
Reduced codebase size


Next Steps

  1. Set up development environment with NDK bundle
  2. Start with NDK worker implementation (client/ndk_worker.mjs)
  3. Create init-ndk.mjs with path-aware worker loading
  4. Create standalone page template
  5. Test standalone page can be accessed directly
  6. Create launcher page (client/index.html)
  7. Migrate individual pages one by one
  8. Test distribution scenarios

Questions to Resolve

  1. Database Name: Use ndk-client for shared database
  2. Worker Path Resolution: Ensure worker path works from all page locations
  3. Dependency Bundling: How to package dependencies for individual page distribution?
  4. Feature Parity: Are there custom features that NDK doesn't support?
  5. Distribution Format: Zip files? Git repos? NPM packages?

Appendix: Code Comparison Examples

Example 1: Connecting to Relays

Custom Implementation:

// Manual WebSocket management
const OpenRelayConnection = async (relay) => {
  OBJ_SOCKETS[relay] = new WebSocket(relay);
  
  OBJ_SOCKETS[relay].onopen = function (event) {
    wsOnOpen(event, relay);
  };
  
  OBJ_SOCKETS[relay].onmessage = function (event) {
    wsOnMessage(event, relay);
  };
  
  OBJ_SOCKETS[relay].onclose = function (event) {
    wsOnClose(event, relay);
  };
  
  OBJ_SOCKETS[relay].onerror = function (error) {
    console.log(error, relay);
  };
};

NDK Implementation:

// NDK handles everything
const ndk = new NDK({
  explicitRelayUrls: relayUrls,
  cacheAdapter: cacheAdapter
});

await ndk.connect();
// That's it! NDK manages connections, reconnections, health checks

Example 2: Subscribing to Events

Custom Implementation:

// Build subscription manually
const subscription = await arrGenerateSubscription(subId);
const socket = OBJ_SOCKETS[relay];
if (socket && socket.readyState === WebSocket.OPEN) {
  socket.send(JSON.stringify(subscription));
}

// Handle responses in onmessage
socket.onmessage = async (event) => {
  const msg = JSON.parse(event.data);
  if (msg[0] === "EVENT") {
    await DB_NOSTR.Event_0.put(msg[2]);
  }
};

NDK Implementation:

// NDK handles subscription, deduplication, caching
const sub = ndk.subscribe(
  { kinds: [0], authors: [pubkey] },
  { closeOnEose: false, cacheUsage: 'CACHE_FIRST' }
);

sub.on('event', (event) => {
  // Event is already cached by NDK
  updateUI(event);
});

Example 3: Publishing Events

Custom Implementation:

// Manual signing and publishing
const signedEvent = await requestEventSigning(event);
event.id = signedEvent.id;
event.sig = signedEvent.sig;
event.pubkey = signedEvent.pubkey;

const arrReq = ["EVENT", event];

for (let relay of await DB_NOSTR.Relays.where({ write: 1, connected: 1 }).toArray()) {
  await OBJ_SOCKETS[relay.relay].send(JSON.stringify(arrReq));
}

NDK Implementation:

// NDK handles signing, relay selection, publishing
const ndkEvent = new NDKEvent(ndk, event);
await ndkEvent.sign(signer);
await ndkEvent.publish();
// NDK automatically selects best relays and handles retries

Example 4: Accessing Pages Independently

Custom Implementation:

// Must go through launcher
// Direct access to /client/html/profile.html doesn't work
// Requires index.html to initialize worker first

NDK Implementation:

// Can access any page directly
// User visits /client/html/profile.html
// Page checks authentication
// If not authenticated, shows nostr-login-lite
// After auth, connects to shared worker
// Works immediately, no launcher needed

// But launcher still available for convenience
// User can also visit /client/index.html for app grid

Conclusion

This migration will significantly simplify the codebase while improving reliability and maintainability. The distributed architecture allows each page to be independently accessible while sharing resources through the NDK SharedWorker.

Key Architectural Benefits:

  • Independent Access: Users can go directly to any tool
  • Shared Resources: All pages share same worker and cache
  • Flexible Distribution: Distribute complete suite, individual pages, or subsets
  • No Duplication: Shared worker prevents duplicate relay connections
  • Instant Data: Shared cache means data available across all pages
  • Modular Design: Easy to add, remove, or distribute individual tools

The key to success is:

  1. Get the NDK worker working first
  2. Create a robust init-ndk.mjs that handles authentication from any page
  3. Implement path-aware worker loading
  4. Create one standalone page template
  5. Replicate template for all other pages
  6. Test each page can be accessed independently
  7. Test pages work together when multiple are open
  8. Test distribution scenarios