# ARCHIVED: NDK Comprehensive Guide for POWR App **ARCHIVED:** This document has been superseded by [docs/technical/ndk/comprehensive_guide.md](../technical/ndk/comprehensive_guide.md) as part of the documentation reorganization. **UPDATED FOR MVP: This guide has been consolidated and aligned with our simplified social approach for the MVP release.** This guide combines key information from our various NDK analysis documents and the ndk-mobile package to provide a comprehensive reference for implementing Nostr features in POWR app. It's organized to support our MVP strategy with a focus on the core NDK capabilities we need. ## Table of Contents 1. [NDK Core Concepts](#ndk-core-concepts) 2. [Proper State Management](#proper-state-management) 3. [Subscription Lifecycle](#subscription-lifecycle) 4. [NIP-19 and Encoding/Decoding](#nip-19-and-encodingdecoding) 5. [Context Provider Pattern](#context-provider-pattern) 6. [Mobile-Specific Considerations](#mobile-specific-considerations) 7. [Best Practices for POWR MVP](#best-practices-for-powr-mvp) ## NDK Core Concepts NDK (Nostr Development Kit) provides a set of tools for working with the Nostr protocol. Key components include: - **NDK Instance**: Central object for interacting with the Nostr network - **NDKEvent**: Represents a Nostr event - **NDKUser**: Represents a user (pubkey) - **NDKFilter**: Defines criteria for querying events - **NDKSubscription**: Manages subscriptions to event streams - **NDKRelay**: Represents a connection to a relay ### Basic Setup ```typescript import NDK from '@nostr-dev-kit/ndk'; // Initialize NDK with relays const ndk = new NDK({ explicitRelayUrls: [ 'wss://relay.damus.io', 'wss://relay.nostr.band' ] }); // Connect to relays await ndk.connect(); ``` ## Proper State Management The ndk-mobile library emphasizes proper state management using Zustand. This approach is critical for avoiding the issues we've been seeing in our application. ### Zustand Store Pattern ```typescript // Create a singleton store using Zustand import { create } from 'zustand'; // Private store instance - not exported let store: ReturnType | undefined; // Store creator function const createNDKStore = () => create((set) => ({ ndk: undefined, initialized: false, connecting: false, connectionError: null, initialize: (config) => set((state) => { if (state.initialized) return state; const ndk = new NDK(config); return { ndk, initialized: true }; }), connect: async () => { set({ connecting: true, connectionError: null }); try { const ndk = store?.getState().ndk; if (!ndk) throw new Error("NDK not initialized"); await ndk.connect(); set({ connecting: false }); } catch (error) { set({ connecting: false, connectionError: error }); } } })); // Getter for the singleton store export const getNDKStore = () => { if (!store) { store = createNDKStore(); } return store; }; // Hook for components to use export const useNDKStore = (selector: (state: NDKState) => T) => { // Ensure the store exists getNDKStore(); // Return the selected state return useStore(store)(selector); }; ``` ### Critical State Management Points 1. **Single Store Instance**: Always use a singleton pattern for NDK stores 2. **Lazy Initialization**: Only create the store when first accessed 3. **Proper Selectors**: Select only what you need from the store 4. **Clear State Transitions**: Define explicit state transitions (connecting, connected, error) ## Subscription Lifecycle Proper subscription management is crucial for app stability and performance. The subscribe.ts file in ndk-mobile provides advanced subscription handling. ### Basic Subscription Pattern ```typescript import { useEffect } from 'react'; import { useNDK } from './hooks/useNDK'; function EventComponent({ filter }) { const { ndk } = useNDK(); const [events, setEvents] = useState([]); useEffect(() => { if (!ndk) return; // Create subscription const sub = ndk.subscribe(filter, { closeOnEose: false, // Keep connection open }); // Handle incoming events sub.on('event', (event) => { setEvents(prev => [...prev, event]); }); // Start subscription sub.start(); // Critical: Clean up subscription when component unmounts return () => { sub.stop(); }; }, [ndk, JSON.stringify(filter)]); return (/* render events */); } ``` ### Enhanced Subscription Hook The ndk-mobile package includes an enhanced useSubscribe hook with additional features: ```typescript // Example based on ndk-mobile implementation function useEnhancedSubscribe(filter, options = {}) { const { ndk } = useNDK(); const [events, setEvents] = useState([]); const [eose, setEose] = useState(false); const subRef = useRef(null); useEffect(() => { if (!ndk) return; // Create subscription const sub = ndk.subscribe(filter, { closeOnEose: options.closeOnEose || false, wrap: options.wrap || false }); subRef.current = sub; // Handle incoming events sub.on('event', (event) => { // Process events (filtering, wrapping, etc.) // Add to state setEvents(prev => { // Check for duplicates using event.id if (prev.some(e => e.id === event.id)) return prev; return [...prev, event]; }); }); // Handle end of stored events sub.on('eose', () => { setEose(true); }); // Start subscription sub.start(); // Clean up return () => { if (subRef.current) { subRef.current.stop(); } }; }, [ndk, JSON.stringify(filter), options]); return { events, eose }; } ``` ## NIP-19 and Encoding/Decoding NIP-19 functions are essential for handling Nostr identifiers like npub, note, and naddr. ### Decoding NIP-19 Entities ```typescript import { nip19 } from '@nostr-dev-kit/ndk'; // Decode any NIP-19 entity (naddr, npub, nsec, note, etc.) function decodeNIP19(encoded: string) { try { const decoded = nip19.decode(encoded); // decoded.type will be 'npub', 'note', 'naddr', etc. // decoded.data will contain the data specific to that type return decoded; } catch (error) { console.error('Invalid NIP-19 format:', error); return null; } } // Convert npub to hex pubkey function npubToHex(npub: string) { try { const decoded = nip19.decode(npub); if (decoded.type === 'npub') { return decoded.data as string; // This is the hex pubkey } return null; } catch (error) { console.error('Invalid npub format:', error); return null; } } ``` ### Encoding to NIP-19 Formats ```typescript // Create an npub from a hex public key function hexToNpub(hexPubkey: string) { return nip19.npubEncode(hexPubkey); } // Create a note (event reference) from event ID function eventIdToNote(eventId: string) { return nip19.noteEncode(eventId); } // Create an naddr for addressable events function createNaddr(pubkey: string, kind: number, identifier: string) { return nip19.naddrEncode({ pubkey, // Hex pubkey kind, // Event kind (number) identifier // The 'd' tag value }); } ``` ## Context Provider Pattern The ndk-mobile package emphasizes the Context Provider pattern for proper NDK integration: ```typescript import { NDKProvider } from '@nostr-dev-kit/ndk-mobile'; import App from './App'; // Root component export default function Root() { return ( ); } ``` This pattern ensures: 1. **Single NDK Instance**: The entire app shares one NDK instance 2. **Consistent State**: Auth state and relay connections are managed in one place 3. **Hooks Availability**: All NDK hooks (useNDK, useSubscribe, etc.) work correctly 4. **Proper Cleanup**: Connections and subscriptions are managed appropriately ## Mobile-Specific Considerations The ndk-mobile package includes several mobile-specific optimizations: ### Caching Strategy Mobile devices need efficient caching to reduce network usage and improve performance: ```typescript import { SQLiteAdapter } from '@nostr-dev-kit/ndk-mobile/cache-adapter/sqlite'; // Initialize NDK with SQLite caching const ndk = new NDK({ explicitRelayUrls: [...], cacheAdapter: new SQLiteAdapter({ dbName: 'nostr-cache.db' }) }); ``` ### Mobile Signers For secure key management on mobile devices: ```typescript import { SecureStorageSigner } from '@nostr-dev-kit/ndk-mobile/signers/securestorage'; // Use secure storage for private keys const signer = new SecureStorageSigner({ storageKey: 'nostr-private-key' }); // Add to NDK ndk.signer = signer; ``` ## Best Practices for POWR MVP Based on our analysis and the ndk-mobile implementation, here are key best practices for our MVP: ### 1. State Management - **Use singleton stores** for NDK and session state - **Implement proper state machine** for auth transitions with Zustand - **Add event listeners/callbacks** for components to respond to auth changes ```typescript // Authentication state using Zustand (aligned with ndk-mobile patterns) export const useAuthStore = create((set) => ({ state: 'unauthenticated', // 'unauthenticated', 'authenticating', 'authenticated', 'deauthenticating' user: null, error: null, startAuthentication: async (npub) => { set({ state: 'authenticating', error: null }); try { // Authentication logic here const user = await authenticateUser(npub); set({ state: 'authenticated', user }); } catch (error) { set({ state: 'unauthenticated', error }); } }, logout: async () => { set({ state: 'deauthenticating' }); // Cleanup logic set({ state: 'unauthenticated', user: null }); }, })); ``` ### 2. Subscription Management - **Always clean up subscriptions** when components unmount - **Keep subscription references** in useRef to ensure proper cleanup - **Use appropriate cache strategies** to reduce relay load - **Implement proper error handling** for subscription failures ```typescript // Example component with proper subscription management function WorkoutShareComponent({ workoutId }) { const { ndk } = useNDK(); const [relatedPosts, setRelatedPosts] = useState([]); const subRef = useRef(null); useEffect(() => { if (!ndk) return; // Create subscription for posts mentioning this workout const sub = ndk.subscribe({ kinds: [1], '#e': [workoutId] }, { closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST }); subRef.current = sub; // Handle events sub.on('event', (event) => { setRelatedPosts(prev => [...prev, event]); }); // Start subscription sub.start(); // Clean up on unmount return () => { if (subRef.current) { subRef.current.stop(); } }; }, [ndk, workoutId]); // Component JSX } ``` ### 3. Simplified Social Features For the MVP, we're focusing on: - **Workout sharing**: Publishing kind 1301 workout records and kind 1 notes quoting them - **Official feed**: Display only POWR official account posts in the social tab - **Static content**: Prefer static content over complex real-time feeds ```typescript // Example function to share a workout async function shareWorkout(ndk, workout, caption) { // First, publish the workout record (kind 1301) const workoutEvent = new NDKEvent(ndk); workoutEvent.kind = 1301; workoutEvent.content = JSON.stringify(workout); // Add appropriate tags workoutEvent.tags = [ ['d', workout.id], ['title', workout.title], ['duration', workout.duration.toString()] ]; // Publish workout record const workoutPub = await workoutEvent.publish(); // Then create a kind 1 note quoting the workout const noteEvent = new NDKEvent(ndk); noteEvent.kind = 1; noteEvent.content = caption || `Just completed ${workout.title}!`; // Add e tag to reference the workout event noteEvent.tags = [ ['e', workoutEvent.id, '', 'quote'] ]; // Publish social note await noteEvent.publish(); return { workoutEvent, noteEvent }; } ``` ### 4. Error Handling & Offline Support - **Implement graceful fallbacks** for network errors - **Store pending publish operations** for later retry - **Use SQLite caching** for offline access to previously loaded data ```typescript // Publication queue service example (aligned with mobile patterns) class PublicationQueue { async queueForPublication(event) { try { // Try immediate publication await event.publish(); return true; } catch (error) { // Store for later retry await this.storeEventForLater(event); return false; } } async publishPendingEvents() { const pendingEvents = await this.getPendingEvents(); for (const event of pendingEvents) { try { await event.publish(); await this.markAsPublished(event); } catch (error) { // Keep in queue for next retry console.error("Failed to publish:", error); } } } } ``` ### 5. Provider Pattern - **Use NDKProvider** to wrap the application - **Configure NDK once** at the app root level - **Access NDK via hooks** rather than creating instances ```typescript // App.tsx export default function App() { return ( ); } // Using NDK in components function SomeComponent() { const { ndk } = useNDK(); // Use ndk here return (/* component JSX */); } ``` ### 6. MVP Implementation Focus For the MVP, focus on implementing: 1. **Core Authentication**: Proper login/logout with state management 2. **Workout Sharing**: Publication of workouts to Nostr 3. **Limited Social Features**: Static feed of official POWR account 4. **User Profile**: Basic user information display Defer these for post-MVP: 1. Full social feed implementation 2. Real-time following/interaction 3. Complex subscription patterns By following these best practices, we can create a stable foundation for the POWR app's MVP release that addresses the current architectural issues while providing a simplified but functional social experience.