feat: Add NOSTR architecture documentation

- Introduce a comprehensive documentation file detailing the singleton-based architecture of the Ario web app.
- Outline the core singletons for authentication, relay management, chat, market, and Nostr store, along with their integration into components.
- Provide insights into the benefits of the architecture, including resource management, event handling, and performance optimizations.
- Include guidelines for configuration, error handling, and future extensibility to support ongoing development.
This commit is contained in:
padreug 2025-08-13 15:18:54 +02:00
parent 91d742eb7b
commit a4584ed9bd

782
NOSTR_ARCHITECTURE.md Normal file
View file

@ -0,0 +1,782 @@
# 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.