# Ario Web App Architecture ## Overview The Ario web app uses a **singleton-based architecture** to manage shared state and resources across components. This document explains how the core singletons work and how different components (chat, market, feed) plug into the system. ## Core Singleton Architecture ### 1. Authentication Singleton (`useAuth`) **Location**: `src/composables/useAuth.ts` **Purpose**: Manages user authentication state using LNBits integration. **Singleton Pattern**: ```typescript export function useAuth() { // ... implementation } // Export singleton instance for global state export const auth = useAuth() ``` **Usage**: Imported throughout the app for authentication state: ```typescript import { auth } from '@/composables/useAuth' // All components see the same authentication state console.log(auth.isAuthenticated.value) ``` ### 2. Relay Hub Singleton (`relayHub`) **Location**: `src/lib/nostr/relayHub.ts` **Purpose**: Centralized management of Nostr relay connections, providing a single WebSocket connection pool for the entire application. **Singleton Pattern**: ```typescript export class RelayHub extends EventEmitter { // ... implementation } // Export singleton instance export const relayHub = new RelayHub() ``` **Key Features**: - Single WebSocket connection pool - Automatic reconnection and health checks - Event broadcasting for connection state changes - Mobile visibility handling for WebSocket management ### 3. Relay Hub Composable Singleton (`useRelayHub`) **Location**: `src/composables/useRelayHub.ts` **Purpose**: Vue-reactive wrapper around the core `relayHub` instance, providing reactive state and Vue-specific functionality. **Singleton Pattern**: ```typescript export function useRelayHub() { // Uses the same relayHub instance const initialize = async () => { await relayHub.initialize(relayUrls) } return { initialize, connect, disconnect, ... } } // Export singleton instance export const relayHubComposable = useRelayHub() ``` **Relationship**: This composable wraps the core `relayHub` instance, making it Vue-reactive while maintaining the singleton connection pool. ### 4. Chat Singleton (`useNostrChat`) **Location**: `src/composables/useNostrChat.ts` **Purpose**: Manages Nostr chat functionality including message encryption/decryption, peer management, and real-time messaging. **Singleton Pattern**: ```typescript export function useNostrChat() { // ... implementation } // Export singleton instance for global state export const nostrChat = useNostrChat() ``` **Relay Hub Integration**: Uses `useRelayHub()` to connect to relays and manage subscriptions. ### 5. Market Singleton (`useMarket`) **Location**: `src/composables/useMarket.ts` **Purpose**: Manages market functionality including stalls, products, and market data. **Singleton Pattern**: ```typescript export function useMarket() { // ... implementation } // Export singleton instance export const market = useMarket() ``` **Relay Hub Integration**: Uses `useRelayHub()` to fetch market data from Nostr relays. ### 6. Nostr Store Singleton (`useNostrStore`) **Location**: `src/stores/nostr.ts` **Purpose**: Pinia store for Nostr-related state including push notifications and relay connection status. **Singleton Pattern**: ```typescript export const useNostrStore = defineStore('nostr', () => { // ... implementation }) ``` **Relay Hub Integration**: Imports the `relayHub` singleton directly for connection management. ## Component Integration Architecture ### How Components Plug Into the System ``` ┌─────────────────────────────────────────────────────────────┐ │ Component Layer │ ├─────────────────────────────────────────────────────────────┤ │ ChatComponent │ NostrFeed │ Market.vue │ Navbar.vue │ │ ↓ │ ↓ │ ↓ │ ↓ │ ├─────────────────────────────────────────────────────────────┤ │ Composable Layer │ ├─────────────────────────────────────────────────────────────┤ │ useNostrChat │ useRelayHub │ useMarket │ useAuth │ │ ↓ │ ↓ │ ↓ │ ↓ │ ├─────────────────────────────────────────────────────────────┤ │ Singleton Instance Layer │ ├─────────────────────────────────────────────────────────────┤ │ nostrChat │ relayHub │ market │ auth │ │ ↓ │ ↓ │ ↓ │ ↓ │ ├─────────────────────────────────────────────────────────────┤ │ Core Implementation Layer │ ├─────────────────────────────────────────────────────────────┤ │ RelayHub │ LNBits API │ Market API │ Chat API │ └─────────────────────────────────────────────────────────────┘ ``` ### 1. Chat Component Integration **Component**: `src/components/nostr/ChatComponent.vue` **Integration Path**: ```typescript // 1. Component imports the chat singleton import { nostrChat } from '@/composables/useNostrChat' // 2. Chat singleton uses relay hub composable import { useRelayHub } from './useRelayHub' // 3. Relay hub composable uses core relay hub singleton import { relayHub } from '../lib/nostr/relayHub' ``` **Data Flow**: ``` ChatComponent → nostrChat → useRelayHub → relayHub → Nostr Relays ↓ ↓ ↓ ↓ UI Updates Chat State Vue State WebSocket ``` **Key Benefits**: - Single chat state across all components - Shared relay connections - Centralized message encryption/decryption - Unified peer management ### 2. Feed Component Integration **Component**: `src/components/nostr/NostrFeed.vue` **Integration Path**: ```typescript // 1. Component imports relay hub composable import { useRelayHub } from '@/composables/useRelayHub' // 2. Relay hub composable uses core relay hub singleton import { relayHub } from '../lib/nostr/relayHub' ``` **Data Flow**: ``` NostrFeed → useRelayHub → relayHub → Nostr Relays ↓ ↓ ↓ Feed UI Vue State WebSocket ``` **Key Benefits**: - Real-time feed updates - Shared relay connections - Centralized admin filtering - Efficient event subscription management ### 3. Market Component Integration **Component**: `src/pages/Market.vue` **Integration Path**: ```typescript // 1. Component uses market composable import { useMarket } from '@/composables/useMarket' // 2. Market composable uses relay hub composable import { useRelayHub } from './useRelayHub' // 3. Relay hub composable uses core relay hub singleton import { relayHub } from '../lib/nostr/relayHub' ``` **Data Flow**: ``` Market.vue → useMarket → useRelayHub → relayHub → Nostr Relays ↓ ↓ ↓ ↓ Market UI Market Vue State WebSocket State ``` **Key Benefits**: - Centralized market data - Shared relay connections - Efficient product/stall fetching - Real-time market updates ## Singleton Benefits in Practice ### 1. **Shared Connection State** ```typescript // Component A const { isConnected } = useRelayHub() console.log(isConnected.value) // true // Component B (different part of app) const { isConnected } = useRelayHub() console.log(isConnected.value) // true (same state!) ``` ### 2. **Centralized Resource Management** ```typescript // All components share the same WebSocket connections relayHub.connectedRelayCount // Same value everywhere relayHub.totalSubscriptionCount // Same value everywhere ``` ### 3. **Event Broadcasting** ```typescript // Component A listens to connection events relayHub.on('connected', () => console.log('Connected!')) // Component B triggers connection await relayHub.connect() // Component A gets notified! ``` ### 4. **Efficient Resource Usage** - **Before**: Each component creates its own relay connections - **After**: Single connection pool shared across all components - **Result**: Reduced memory usage, better performance, consistent state ## Configuration and Environment Variables ### Required Environment Variables ```bash # Nostr relay configuration VITE_NOSTR_RELAYS=["wss://relay1.example.com", "wss://relay2.example.com"] # Admin pubkeys for feed filtering VITE_ADMIN_PUBKEYS=["npub1admin1...", "npub1admin2..."] # LNBits API configuration VITE_LNBITS_BASE_URL=https://your-lnbits-instance.com VITE_API_KEY=your-api-key ``` ### Configuration Structure ```typescript // src/lib/config/index.ts export const config: AppConfig = { nostr: { relays: parseJsonEnv(import.meta.env.VITE_NOSTR_RELAYS, []), adminPubkeys: parseJsonEnv(import.meta.env.VITE_ADMIN_PUBKEYS, []) }, api: { baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || '', key: import.meta.env.VITE_API_KEY || '' } } ``` ## Testing and Development ### Mocking Singletons for Testing ```typescript // In test files, you can mock the singleton instances import { auth } from '@/composables/useAuth' import { nostrChat } from '@/composables/useNostrChat' // Mock the singletons vi.mock('@/composables/useAuth', () => ({ auth: { isAuthenticated: ref(false), currentUser: ref(null) } })) ``` ### Development Benefits - **Hot Reload**: Singleton state persists across component reloads - **Debugging**: Single point of inspection for shared state - **Performance**: No unnecessary re-initialization of expensive resources ## Best Practices ### 1. **Always Use the Singleton Instance** ```typescript // ✅ Good: Use the exported singleton import { auth } from '@/composables/useAuth' import { nostrChat } from '@/composables/useNostrChat' // ❌ Bad: Don't create new instances const { currentUser } = useAuth() // This creates a new instance! ``` ### 2. **Access State Through Composables** ```typescript // ✅ Good: Use the composable wrapper for reactive state const { isConnected, connectionStatus } = useRelayHub() // ❌ Bad: Don't access relayHub directly in components import { relayHub } from '@/lib/nostr/relayHub' console.log(relayHub.isConnected) // Not reactive! ``` ### 3. **Listen to Events for State Changes** ```typescript // ✅ Good: Listen to relay hub events relayHub.on('connected', () => { console.log('Relay connected!') }) // ❌ Bad: Don't poll for state changes setInterval(() => { console.log(relayHub.isConnected) // Inefficient! }, 1000) ``` ## Conclusion The singleton architecture in Ario provides: 1. **Efficient Resource Management**: Single connection pools and shared state 2. **Consistent User Experience**: All components see the same data 3. **Simplified Development**: Clear patterns for state management 4. **Better Performance**: No duplicate connections or state 5. **Easier Testing**: Centralized mocking of shared resources This architecture makes the app more maintainable, performant, and user-friendly while providing a solid foundation for future features. ## Shared Nostr Function Execution ### Event Building and Publishing Pattern All components that need to create and publish Nostr events follow a consistent pattern through the relay hub singleton: #### 1. Event Creation Utilities **Direct Event Template Creation** (used in `useNostrChat`): ```typescript import { nip04, finalizeEvent, type EventTemplate } from 'nostr-tools' // Create event template const eventTemplate: EventTemplate = { kind: 4, // Encrypted DM created_at: Math.floor(Date.now() / 1000), tags: [['p', peerPubkey]] content: encryptedContent } // Finalize and sign the event const event = finalizeEvent(eventTemplate, hexToBytes(privateKey)) ``` **Market Event Creation** (used in `useMarket`): ```typescript // Market events use specific kinds and tags const MARKET_EVENT_KINDS = { STALL: 30017, PRODUCT: 30018, MARKET: 30019 } // Extract data from event tags const stallId = event.tags.find((tag: any) => tag[0] === 'd')?.[1] const productId = event.tags.find((tag: any) => tag[0] === 'd')?.[1] ``` #### 2. Centralized Publishing Through Relay Hub **All components publish through the same singleton**: ```typescript // In useNostrChat.ts await relayHub.publishEvent(event) // In useMarket.ts (if publishing) await relayHub.publishEvent(event) // In any other component await relayHub.publishEvent(event) ``` **Relay Hub Publishing Logic**: ```typescript async publishEvent(event: Event): Promise<{ success: number; total: number }> { if (!this._isConnected) { throw new Error('Not connected to any relays') } const relayUrls = Array.from(this.connectedRelays.keys()) const results = await Promise.allSettled( relayUrls.map(relay => this.pool.publish([relay], event)) ) const successful = results.filter(result => result.status === 'fulfilled').length const total = results.length this.emit('eventPublished', { eventId: event.id, success: successful, total }) return { success: successful, total } } ``` #### 3. Event Querying and Subscription **Consistent Query Interface**: ```typescript // One-time event queries const events = await relayHub.queryEvents(filters) // Real-time subscriptions const unsubscribe = relayHub.subscribe({ id: 'subscription-id', filters: [{ kinds: [1], limit: 50 }], onEvent: (event) => { // Handle incoming event } }) ``` ### Shared Dependencies **nostr-tools Package**: - `finalizeEvent` - Event signing and finalization - `nip04` - Encrypted message handling - `SimplePool` - Relay connection management - `EventTemplate` - Event structure typing **Common Event Patterns**: - **Text Notes** (kind 1): Community posts and announcements - **Encrypted DMs** (kind 4): Private chat messages - **Market Events** (kinds 30017-30019): Stall, product, and market data - **Reactions** (kind 7): Emoji reactions to posts ## Configuration Management ### Environment Variables and Configuration **Centralized Config** (`src/lib/config/index.ts`): ```typescript interface NostrConfig { relays: string[] adminPubkeys: string[] } interface MarketConfig { defaultNaddr: string supportedRelays: string[] lightningEnabled: boolean defaultCurrency: string } // Environment variables VITE_NOSTR_RELAYS: JSON array of relay URLs VITE_ADMIN_PUBKEYS: JSON array of admin public keys VITE_MARKET_RELAYS: JSON array of market-specific relays VITE_LIGHTNING_ENABLED: Boolean for Lightning integration ``` **Default Relay Configuration**: ```typescript // Fallback relay configuration supportedRelays: [ 'ws://127.0.0.1:7777', 'wss://relay.damus.io', 'wss://relay.snort.social', 'wss://nostr-pub.wellorder.net', 'wss://nostr.zebedee.cloud', 'wss://nostr.walletofsatoshi.com' ] ``` ### Configuration Utilities **Admin Pubkey Validation**: ```typescript export const configUtils = { isAdminPubkey(pubkey: string): boolean { return config.nostr.adminPubkeys.includes(pubkey) } } ``` ## Error Handling and Resilience ### Connection Management **Automatic Reconnection**: ```typescript // Relay hub automatically handles reconnection private scheduleReconnect(): void { if (this.reconnectInterval) { clearTimeout(this.reconnectInterval) } this.reconnectInterval = setTimeout(async () => { await this.connect() }, this.reconnectDelay) } ``` **Health Monitoring**: ```typescript // Continuous health checks private startHealthCheck(): void { this.healthCheckInterval = setInterval(() => { this.performHealthCheck() }, this.healthCheckIntervalMs) } ``` ### Error Handling Patterns **Graceful Degradation**: ```typescript // Components handle connection failures gracefully async function loadNotes() { try { if (!relayHub.isConnected.value) { await relayHub.connect() } // ... load notes } catch (err) { error.value = err instanceof Error ? err : new Error('Failed to load notes') console.error('Failed to load notes:', err) } } ``` **Retry Logic**: ```typescript // Market app retry pattern async def wait_for_nostr_events(nostr_client: NostrClient): while True: try: await subscribe_to_all_merchants() # ... process events except Exception as e: logger.warning(f"Subscription failed. Will retry in one minute: {e}") await asyncio.sleep(10) ``` ## Event Processing and Filtering ### Filter Management **Dynamic Filter Creation**: ```typescript // Feed component creates filters based on type const filters: any[] = [{ kinds: [1], // TEXT_NOTE limit: 50 }] // Filter by authors for announcements if (props.feedType === 'announcements' && hasAdminPubkeys.value) { filters[0].authors = adminPubkeys } ``` **Market Event Filtering**: ```typescript // Market-specific filters const _filters_for_stall_events = (public_keys: List[str], since: int) -> List: stall_filter = {"kinds": [30017], "authors": public_keys} if since and since != 0: stall_filter["since"] = since return [stall_filter] ``` ### Event Processing Pipeline **Event Processing Flow**: ```typescript // 1. Receive event from relay onEvent: (event) => { // 2. Process and transform const newNote = { id: event.id, pubkey: event.pubkey, content: event.content, created_at: event.created_at, tags: event.tags || [], mentions: event.tags?.filter(tag => tag[0] === 'p').map(tag => tag[1]) || [], isReply: event.tags?.some(tag => tag[0] === 'e' && tag[3] === 'reply'), replyTo: event.tags?.find(tag => tag[0] === 'e' && tag[3] === 'reply')?.[1] } // 3. Apply business logic filters let shouldInclude = true if (props.feedType === 'announcements' && !isAdminPost(event.pubkey)) { shouldInclude = false } // 4. Add to state if approved if (shouldInclude) { notes.value.unshift(newNote) // 5. Limit array size for memory management if (notes.value.length > 100) { notes.value = notes.value.slice(0, 100) } } } ``` ## Performance Optimizations ### Memory Management **Array Size Limits**: ```typescript // Prevent memory issues with large feeds if (notes.value.length > 100) { notes.value = notes.value.slice(0, 100) } ``` **Subscription Cleanup**: ```typescript // Proper cleanup prevents memory leaks onUnmounted(() => { if (unsubscribe) { unsubscribe() unsubscribe = null } }) ``` ### Connection Pooling **Shared WebSocket Connections**: ```typescript // Single pool shared across all components private pool: SimplePool // Components don't create individual connections // They all use the centralized relay hub ``` **Efficient Relay Usage**: ```typescript // Query multiple relays simultaneously const events = await this.pool.querySync(availableRelays, filter) // Publish to all relays in parallel const results = await Promise.allSettled( relayUrls.map(relay => this.pool.publish([relay], event)) ) ``` ## Security Considerations ### Encryption and Privacy **End-to-End Encryption**: ```typescript // All chat messages are encrypted using nip04 const encryptedContent = await nip04.encrypt( privateKey, publicKey, content ) ``` **Private Key Management**: ```typescript // Keys never leave the browser // Validation of key format and length if (privateKey.length !== 64) { throw new Error(`Invalid private key length: ${privateKey.length} (expected 64)`) } // Hex format validation const hexRegex = /^[0-9a-fA-F]+$/ if (!hexRegex.test(privateKey)) { throw new Error(`Invalid private key format: contains non-hex characters`) } ``` ### Access Control **Admin Pubkey Validation**: ```typescript // Only admin pubkeys can post announcements function isAdminPost(pubkey: string): boolean { return configUtils.isAdminPubkey(pubkey) } // Filter content based on user permissions if (props.feedType === 'announcements' && !isAdminPost(event.pubkey)) { shouldInclude = false } ``` ## Benefits of This Architecture ### 1. **Consistent Event Handling** - All components use the same event creation and publishing patterns - Centralized relay management ensures consistent behavior - Shared error handling and retry logic ### 2. **Efficient Resource Usage** - Single WebSocket connection pool shared across all components - No duplicate relay connections - Centralized connection health monitoring ### 3. **Simplified Component Logic** - Components focus on business logic, not relay management - Consistent API for all Nostr operations - Easy to add new event types and functionality ### 4. **Maintainable Codebase** - Single source of truth for relay connections - Centralized event publishing logic - Easy to debug and monitor Nostr operations ### 5. **Robust Error Handling** - Automatic reconnection and health monitoring - Graceful degradation when relays are unavailable - Comprehensive error logging and user feedback ### 6. **Performance Optimized** - Memory management with array size limits - Efficient connection pooling - Parallel relay operations ## Future Extensibility This architecture makes it easy to add new Nostr functionality: 1. **New Event Types**: Add new event creation utilities following the existing pattern 2. **Additional Relays**: Configure new relays in the centralized relay hub 3. **New Components**: Import the existing singletons and use the established patterns 4. **Enhanced Features**: Extend the relay hub with new capabilities (e.g., event caching, filtering) 5. **Advanced Filtering**: Implement more sophisticated event filtering and processing 6. **Caching Layer**: Add event caching for improved performance 7. **Rate Limiting**: Implement rate limiting for relay operations 8. **Metrics and Monitoring**: Add comprehensive metrics for relay performance This architecture makes the app more maintainable, performant, and user-friendly while providing a solid foundation for future features.