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:
- Each page must be self-contained and independently accessible
- All pages will be created in
www/directory (using existingupload_www.sh) - Preserve all aesthetics from
client/- same layouts, CSS, page formatting - 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:
- Self-Contained Pages: Each HTML page includes authentication check
- Shared Worker: All pages connect to same
ndk-worker.js - Shared Cache: All pages use same NDK Dexie database (
ndk-client) - Shared Authentication: nostr-login-lite state shared across tabs
- Independent Distribution: Each page can be distributed separately
- Preserve Aesthetics: Copy CSS, layouts, and styling from
client/ - 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.shscript
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.css→www/css/client.cssclient/css/*.ttf→www/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
eventstable instead of per-kind tables - NDK has
eventTagsfor efficient tag queries - NDK tracks relay status and event relay associations
- NDK has unpublished events queue
Testing Strategy
Phase 1: Basic Functionality
- Authentication works with nostr-login-lite
- NDK worker initializes successfully
- Profile loads from cache and relays
- Event 10002 loads and updates relay pool
- Event 3 (contacts) loads successfully
Phase 2: Subscriptions
- User metadata subscription works
- Follows metadata subscription works
- Feed subscription works
- DM subscription works
Phase 3: Publishing
- Can publish notes (kind 1)
- Can publish profile updates (kind 0)
- Can publish relay list (kind 10002)
- Can publish DMs (kind 4)
Phase 4: Multi-tab & Independence
- Multiple tabs share same NDK worker
- Events appear in all tabs
- Logout works across all tabs
- Can access any page directly without launcher
- Authentication works from any entry point
- Shared cache works across different pages
Phase 5: Performance
- Cache-first loading is fast
- Relay connections are stable
- No memory leaks
- Subscription deduplication works
Phase 6: Distribution
- Individual page distribution works
- All dependencies included
- Pages work standalone
- Pages work together when multiple open
Implementation Checklist
Preparation
- Copy
www/ndk-worker.jstoclient/ndk_worker.mjsas template - Copy
www/ndk-core.bundle.jstoclient/js/ - Create
client/js/init-ndk.mjswith 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
- Set up development environment with NDK bundle
- Start with NDK worker implementation (
client/ndk_worker.mjs) - Create init-ndk.mjs with path-aware worker loading
- Create standalone page template
- Test standalone page can be accessed directly
- Create launcher page (
client/index.html) - Migrate individual pages one by one
- Test distribution scenarios
Questions to Resolve
- Database Name: Use
ndk-clientfor shared database - Worker Path Resolution: Ensure worker path works from all page locations
- Dependency Bundling: How to package dependencies for individual page distribution?
- Feature Parity: Are there custom features that NDK doesn't support?
- 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:
- Get the NDK worker working first
- Create a robust init-ndk.mjs that handles authentication from any page
- Implement path-aware worker loading
- Create one standalone page template
- Replicate template for all other pages
- Test each page can be accessed independently
- Test pages work together when multiple are open
- Test distribution scenarios