- Introduced guidelines for handling complex object reactivity in Vue, addressing common issues such as input components not updating and computed properties not recalculating. - Provided solutions including using Object.assign for object reactivity, dynamic keys for component re-renders, and safe navigation with fallbacks. - Included practical examples to illustrate the application of these patterns in real scenarios, enhancing developer understanding of Vue's reactivity system.
25 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Development Commands
Development
npm run dev- Start development server with Vite (includes --host flag)npm run build- Build for production (includes TypeScript check with vue-tsc -b)npm run preview- Preview production build locallynpm run analyze- Build with bundle analysis (opens visualization)
Electron Development
npm run electron:dev- Run both Vite dev server and Electron concurrentlynpm run electron:build- Full build and package for Electronnpm run start- Start Electron using Forgenpm run package- Package Electron app with Forgenpm run make- Create distributables with Electron Forge
Architecture Overview
This is a modular Vue 3 + TypeScript + Vite application with Electron support, featuring a Nostr protocol client and Lightning Network integration for events/ticketing.
Modular Architecture
The application uses a plugin-based modular architecture with dependency injection for service management:
Core Modules:
- Base Module (
src/modules/base/) - Core infrastructure (Nostr, Auth, PWA) - Wallet Module (
src/modules/wallet/) - Lightning wallet management with real-time balance updates - Nostr Feed Module (
src/modules/nostr-feed/) - Social feed functionality - Chat Module (
src/modules/chat/) - Encrypted Nostr chat - Events Module (
src/modules/events/) - Event ticketing with Lightning payments - Market Module (
src/modules/market/) - Nostr marketplace functionality
Module Configuration:
- Modules are configured in
src/app.config.ts - Each module can be enabled/disabled and configured independently
- Modules have dependencies (e.g., all modules depend on 'base')
Plugin Manager:
src/core/plugin-manager.tshandles module lifecycle- Registers, installs, and manages module dependencies
- Handles route registration from modules
Dependency Injection Pattern
CRITICAL: Always use the dependency injection pattern for accessing shared services:
Service Registration (Base Module):
// src/modules/base/index.ts
import { container, SERVICE_TOKENS } from '@/core/di-container'
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth)
Service Consumption (Other Modules):
// In any module's composables or services
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
❌ NEVER do this:
// DON'T import services directly - breaks modular architecture
import { relayHubComposable } from '@/composables/useRelayHub'
✅ Always do this:
// DO use dependency injection for loose coupling
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
Available Services:
SERVICE_TOKENS.RELAY_HUB- Centralized Nostr relay managementSERVICE_TOKENS.AUTH_SERVICE- Authentication servicesSERVICE_TOKENS.PAYMENT_SERVICE- Lightning payment and wallet managementSERVICE_TOKENS.VISIBILITY_SERVICE- App visibility and connection managementSERVICE_TOKENS.WALLET_SERVICE- Wallet operations (send, receive, transactions)SERVICE_TOKENS.WALLET_WEBSOCKET_SERVICE- Real-time wallet balance updates via WebSocket
Core Stack:
- Vue 3 with Composition API (
<script setup>style) - TypeScript throughout
- Vite build system with PWA support
- Electron for desktop app packaging
- Pinia for state management
- Vue Router for navigation
- TailwindCSS v4 with Shadcn/ui components
- Vue-i18n for internationalization
Key Features:
- Nostr protocol client for decentralized social networking
- Lightning Network integration for event ticketing
- Real-time wallet balance updates via WebSocket - Automatic UI updates when payments are sent/received
- PWA capabilities with service worker
- Theme switching and language switching
- Real-time connection status monitoring
Directory Structure:
src/components/- Vue components organized by featureui/- Shadcn/ui component librarynostr/- Nostr-specific componentsevents/- Event/ticketing componentslayout/- App layout components
src/composables/- Vue composables for reusable logicsrc/stores/- Pinia stores for state managementsrc/lib/- Core business logicnostr/- Nostr client implementationapi/- API integrationstypes/- TypeScript type definitions
src/pages/- Route pageselectron/- Electron main process code
Lightning Wallet Integration: The app integrates with LNbits for Lightning Network wallet functionality with real-time balance updates:
Core Wallet Services:
src/core/services/PaymentService.ts- Centralized payment processing and wallet balance managementsrc/modules/wallet/services/WalletService.ts- Wallet operations (send, receive, transactions, pay links)src/modules/wallet/services/WalletWebSocketService.ts- Real-time balance updates via WebSocket
WebSocket Real-Time Features:
- Automatic balance updates when payments are sent or received
- Live transaction notifications with toast messages
- Connection management with automatic reconnection and exponential backoff
- Battery optimization via VisibilityService integration (pauses when app not visible)
- Unit conversion handling between sats and millisats for different LNbits WebSocket behaviors
WebSocket Configuration (in app.config.ts):
websocket: {
enabled: true, // Enable/disable WebSocket functionality
reconnectDelay: 1000, // Initial reconnection delay
maxReconnectAttempts: 5 // Maximum reconnection attempts
}
How WebSocket Balance Updates Work:
- Connects to LNbits WebSocket:
wss://your-lnbits/api/v1/ws/{walletInkey} - Handles both incoming and outgoing payment notifications
- Incoming payments: Uses balance as-is (post-payment balance from LNbits)
- Outgoing payments: Adjusts balance by subtracting payment amount (pre-payment balance from LNbits)
- Updates stored in
PaymentService.updateWalletBalance()for consistency - Triggers Vue reactivity to update UI components immediately
Nostr Integration: The app connects to Nostr relays using a custom NostrClient class built on nostr-tools. Key files:
src/lib/nostr/client.ts- Core Nostr client implementationsrc/composables/useNostr.ts- Vue composable for Nostr connection managementsrc/stores/nostr.ts- Pinia store for Nostr state
Development Guidelines
Modular Architecture Patterns
Module Structure:
src/modules/[module-name]/
├── index.ts # Module plugin definition
├── components/ # Module-specific components
├── composables/ # Module composables
├── services/ # Module services
├── stores/ # Module-specific stores
├── types/ # Module type definitions
└── views/ # Module pages/views
Module Plugin Pattern:
export const myModule: ModulePlugin = {
name: 'my-module',
version: '1.0.0',
dependencies: ['base'], // Always depend on base for core services
async install(app: App, options?: { config?: MyModuleConfig }) {
// Module installation logic
// Register components, initialize services, etc.
},
routes: [/* module routes */],
components: {/* exported components */},
composables: {/* exported composables */}
}
Service Integration:
- All modules MUST use dependency injection for shared services
- NEVER import services directly across module boundaries
- Base module provides core infrastructure services
- Modules can register their own services in the DI container
⚠️ CRITICAL - WebSocket Connection Management:
- ALWAYS integrate with VisibilityService for any module that uses WebSocket connections
- All services extending
BaseServicehave automatic access tothis.visibilityService - Register visibility callbacks during service initialization:
this.visibilityService.registerService(name, onResume, onPause) - Implement proper connection recovery in
onResume()handler (check health, reconnect if needed, restore subscriptions) - Implement battery-conscious pausing in
onPause()handler (stop heartbeats, queue operations) - Mobile browsers suspend WebSocket connections when app loses visibility - visibility management is essential for reliable real-time features
- See
docs/VisibilityService.mdanddocs/VisibilityService-Integration.mdfor comprehensive integration guides - Future modules will likely ALL depend on WebSocket connections - plan for visibility management from the start
Centralized Infrastructure
Nostr Relay Management:
- Single RelayHub manages all Nostr connections
- All modules use the same relay configuration from
VITE_NOSTR_RELAYS - No module should create separate relay connections
Authentication:
- Centralized auth service handles all authentication
- Modules access auth state through dependency injection
- Router guards use the shared auth service
Configuration:
- Environment variables prefixed with
VITE_ - Module configs in
src/app.config.ts - Centralized config parsing and validation
Form Implementation Standards
CRITICAL: Always use Shadcn/UI Form Components with vee-validate
All forms in the application MUST follow the official Shadcn Vue form implementation pattern:
Required Form Setup:
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
// 1. Define Zod schema for validation
const formSchema = toTypedSchema(z.object({
name: z.string().min(1, "Name is required").max(100, "Name too long"),
email: z.string().email("Invalid email address").optional(),
items: z.array(z.string()).min(1, "Select at least one item"),
}))
// 2. Set up form with vee-validate
const form = useForm({
validationSchema: formSchema,
initialValues: {
name: '',
email: '',
items: []
}
})
// 3. Destructure form methods
const { setFieldValue, resetForm, values, meta } = form
// 4. Create form validation computed
const isFormValid = computed(() => meta.value.valid)
// 5. Create submit handler with form.handleSubmit
const onSubmit = form.handleSubmit(async (values) => {
console.log('Form submitted:', values)
// Handle form submission logic
})
Required Form Template Structure:
<template>
<!-- form.handleSubmit automatically prevents default submission -->
<form @submit="onSubmit" class="space-y-6">
<!-- Text Input Field -->
<FormField v-slot="{ componentField }" name="name">
<FormItem>
<FormLabel>Name *</FormLabel>
<FormControl>
<Input
placeholder="Enter name"
v-bind="componentField"
/>
</FormControl>
<FormDescription>Enter your full name</FormDescription>
<FormMessage />
</FormItem>
</FormField>
<!-- Multiple Checkbox Selection -->
<FormField name="items">
<FormItem>
<div class="mb-4">
<FormLabel class="text-base">Items *</FormLabel>
<FormDescription>Select one or more items</FormDescription>
</div>
<div v-for="item in availableItems" :key="item.id">
<FormField
v-slot="{ value, handleChange }"
type="checkbox"
:value="item.id"
:unchecked-value="false"
name="items"
>
<FormItem class="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
:model-value="value.includes(item.id)"
@update:model-value="handleChange"
/>
</FormControl>
<FormLabel class="font-normal">{{ item.name }}</FormLabel>
</FormItem>
</FormField>
</div>
<FormMessage />
</FormItem>
</FormField>
<!-- Submit Button -->
<Button
type="submit"
:disabled="isLoading || !isFormValid"
>
{{ isLoading ? 'Submitting...' : 'Submit' }}
</Button>
</form>
</template>
Key Form Requirements:
- ✅ Form validation: Use
@submit="onSubmit"- form.handleSubmit automatically prevents page refresh - ✅ Button state: Disable submit button with
!isFormValiduntil all required fields are valid - ✅ Error display: Use
<FormMessage />for automatic error display - ✅ Field binding: Use
v-bind="componentField"for proper form field integration - ✅ Checkbox arrays: Use nested FormField pattern for multiple checkbox selection
- ✅ Type safety: Zod schema provides full TypeScript type safety
❌ NEVER do this:
<!-- Wrong: Manual form handling without vee-validate -->
<form @submit.prevent="handleSubmit">
<!-- Wrong: Direct v-model bypasses form validation -->
<Input v-model="myValue" />
<!-- Wrong: Manual validation instead of using meta.valid -->
<Button :disabled="!name || !email">Submit</Button>
✅ ALWAYS do this:
<!-- Correct: Uses form.handleSubmit for proper form handling -->
<form @submit="onSubmit">
<!-- Correct: Uses FormField with componentField binding -->
<FormField v-slot="{ componentField }" name="fieldName">
<FormControl>
<Input v-bind="componentField" />
</FormControl>
</FormField>
<!-- Correct: Uses form meta for validation state -->
<Button :disabled="!isFormValid">Submit</Button>
Vue Reactivity Best Practices
CRITICAL: Handling Complex Object Reactivity
When working with complex objects from API responses or services, Vue's reactivity system may not always detect changes properly. This is especially common with nested objects or objects from external sources.
Common Reactivity Issues:
- Input components not updating when object properties change
- Template not re-rendering after API responses
- Computed properties not recalculating
✅ SOLUTIONS:
1. Force Object Reactivity with Object.assign:
// ❌ DON'T: Direct assignment may not trigger reactivity
createdObject.value = apiResponse
// ✅ DO: Create new object reference to ensure reactivity
createdObject.value = Object.assign({}, apiResponse)
2. Force Component Re-render with Dynamic Keys:
<!-- ❌ DON'T: Component may not update with new data -->
<Input :value="object.property" readonly />
<!-- ✅ DO: Force re-render with dynamic key -->
<Input
:key="`field-${object?.id}`"
:model-value="object?.property || ''"
readonly
/>
3. Use Safe Navigation and Fallbacks:
<!-- ✅ Prevent errors and ensure consistent data types -->
<Input
:model-value="object?.property || ''"
:key="`field-${object?.uniqueId}`"
readonly
/>
When to Apply These Patterns:
- ✅ API responses stored in reactive refs
- ✅ Complex objects from services
- ✅ Input components showing external data
- ✅ Any scenario where template doesn't update after data changes
Example from Wallet Module:
// Service returns complex invoice object
const invoice = await walletService.createInvoice(data)
// Force reactivity for template updates
createdInvoice.value = Object.assign({}, invoice)
<!-- Template with forced reactivity -->
<Input
:key="`bolt11-${createdInvoice?.payment_hash}`"
:model-value="createdInvoice?.payment_request || ''"
readonly
/>
Module Development Best Practices
Module Structure Requirements:
- Independence: Modules must be independent of each other - no direct imports between modules
- Base Dependency: All modules should depend on 'base' module for core infrastructure
- Service Pattern: All services should extend
BaseServicefor standardized initialization - API Isolation: Module-specific API calls must be in the module's services folder
- Dependency Injection: Cross-module communication only through DI container
📋 MODULE DEVELOPMENT CHECKLIST
Before considering any module complete, verify ALL items:
Service Implementation:
- Service extends
BaseService - Has
metadatawithname,version,dependencies - Dependencies listed in metadata match actual usage
- No manual
injectService()calls inonInitialize() - Registers with VisibilityService if has real-time features
- Implements
onResume()andonPause()if using VisibilityService - Uses module config, not direct config imports
Module Plugin:
- Depends on
'base'module - Creates service instances
- Registers services in DI container BEFORE initialization
- Calls
service.initialize()withwaitForDependencies: true - Registers components AFTER service initialization
Configuration:
- Module added to
app.config.ts - Has
enabledflag - Has
configobject with necessary settings - Uses
appConfig.modules.[moduleName].configin services
Forms (if applicable):
- Uses Shadcn/UI form components
- Uses vee-validate with Zod schema
- Has proper validation messages
- Disables submit button until form is valid
- Uses
form.handleSubmit()for submission
Testing Checklist:
- Service initializes without errors
- Dependencies are properly injected
- VisibilityService callbacks work (test by switching tabs)
- Configuration is properly loaded
- Module can be disabled via config
Required Module Structure:
src/modules/[module-name]/
├── index.ts # Module plugin definition (REQUIRED)
├── components/ # Module-specific components
├── composables/ # Module composables (use DI for services)
├── services/ # Module services (extend BaseService)
│ ├── [module]Service.ts # Core module service
│ └── [module]API.ts # LNbits API integration
├── stores/ # Module-specific Pinia stores
├── types/ # Module type definitions
└── views/ # Module pages/views
Service Implementation Pattern:
⚠️ CRITICAL SERVICE REQUIREMENTS - MUST FOLLOW EXACTLY:
// ✅ CORRECT: Proper BaseService implementation
export class MyModuleService extends BaseService {
// 1. REQUIRED: Declare metadata with dependencies
protected readonly metadata = {
name: 'MyModuleService',
version: '1.0.0',
dependencies: ['PaymentService', 'AuthService'] // List ALL service dependencies by name
}
// 2. REQUIRED: DO NOT manually inject services in onInitialize
protected async onInitialize(): Promise<void> {
// ❌ WRONG: Manual injection
// this.paymentService = injectService(SERVICE_TOKENS.PAYMENT_SERVICE)
// ✅ CORRECT: BaseService auto-injects based on metadata.dependencies
// this.paymentService is already available here!
// 3. REQUIRED: Register with VisibilityService if you have ANY real-time features
if (this.hasRealTimeFeatures()) {
this.visibilityService.registerService(
this.metadata.name,
this.onResume.bind(this),
this.onPause.bind(this)
)
}
// 4. Initialize your module-specific logic
await this.loadInitialData()
}
// 5. REQUIRED: Implement visibility handlers for connection management
private async onResume(): Promise<void> {
// Restore connections, refresh data when app becomes visible
await this.checkConnectionHealth()
await this.refreshData()
}
private async onPause(): Promise<void> {
// Pause expensive operations for battery efficiency
this.pausePolling()
}
private hasRealTimeFeatures(): boolean {
// Return true if your service uses WebSockets, polling, or real-time updates
return true
}
}
// API services for LNbits integration
export class MyModuleAPI extends BaseService {
private baseUrl: string
constructor() {
super()
// ❌ WRONG: Direct config import
// import { config } from '@/lib/config'
// ✅ CORRECT: Use module configuration
const moduleConfig = appConfig.modules.myModule.config
this.baseUrl = moduleConfig.apiConfig.baseUrl
}
// API methods here
}
❌ COMMON MISTAKES TO AVOID:
- Manual service injection in onInitialize - BaseService handles this
- Direct config imports - Always use module configuration
- Missing metadata.dependencies - Breaks automatic dependency injection
- No VisibilityService integration - Causes connection issues on mobile
- Not using proper initialization options - Miss dependency waiting
Module Plugin Pattern:
⚠️ CRITICAL MODULE INSTALLATION REQUIREMENTS:
export const myModule: ModulePlugin = {
name: 'my-module',
version: '1.0.0',
dependencies: ['base'], // ALWAYS depend on 'base' for core infrastructure
async install(app: App, options?: { config?: MyModuleConfig }) {
// 1. REQUIRED: Create service instances
const myService = new MyModuleService()
const myAPI = new MyModuleAPI()
// 2. REQUIRED: Register in DI container BEFORE initialization
container.provide(SERVICE_TOKENS.MY_SERVICE, myService)
container.provide(SERVICE_TOKENS.MY_API, myAPI)
// 3. CRITICAL: Initialize services with proper options
await myService.initialize({
waitForDependencies: true, // REQUIRED: Wait for dependencies
maxRetries: 3, // RECOMMENDED: Retry on failure
timeout: 5000 // OPTIONAL: Timeout for initialization
})
// Initialize API service if it needs initialization
if (myAPI.initialize) {
await myAPI.initialize({
waitForDependencies: true,
maxRetries: 3
})
}
// 4. Register components AFTER services are initialized
app.component('MyComponent', MyComponent)
// 5. OPTIONAL: Export for testing/debugging
return {
service: myService,
api: myAPI
}
}
}
MODULE CONFIGURATION IN app.config.ts:
// REQUIRED: Add module configuration
export default {
modules: {
'my-module': {
enabled: true,
config: {
apiConfig: {
baseUrl: import.meta.env.VITE_API_BASE_URL || 'http://localhost:5000'
},
// Module-specific configuration
features: {
realTimeUpdates: true,
offlineSupport: false
}
}
}
}
}
Nostr Integration Rules:
- NEVER create separate relay connections - always use the central RelayHub
- Access RelayHub through DI:
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) - Use RelayHub methods for all Nostr operations (subscribe, publish, etc.)
- Event kinds should be module-specific and follow NIP specifications
LNbits API Integration:
- Create module-specific API service in
services/[module]API.ts - Extend BaseService for automatic dependency management
- Use authentication headers:
X-Api-Key: walletKey - Base URL from config: Use
appConfig.modules.[module].config.apiConfig.baseUrl - Error handling: Implement proper error handling with user feedback
Composables Best Practices:
export function useMyModule() {
// Always use DI for services
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
const myAPI = injectService(SERVICE_TOKENS.MY_API)
// Never import services directly
// ❌ import { relayHub } from '@/modules/base/nostr/relay-hub'
// ✅ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
}
WebSocket & Visibility Management:
- Services with WebSocket connections MUST integrate with VisibilityService
- Register visibility callbacks:
this.visibilityService.registerService(name, onResume, onPause) - Handle connection recovery in
onResume()callback - Implement battery-conscious pausing in
onPause()callback
Code Conventions:
- Use TypeScript interfaces over types for extendability
- Prefer functional and declarative patterns over classes (except for services)
- Use Vue Composition API with
<script setup>syntax - Follow naming convention: lowercase-with-dashes for directories
- Leverage VueUse functions for enhanced reactivity
- Implement lazy loading for non-critical components
- Optimize images using WebP format with lazy loading
- ALWAYS use dependency injection for cross-module service access
- ALWAYS use Shadcn Form components for all form implementations
- ALWAYS extend BaseService for module services
- NEVER create direct dependencies between modules
Build Configuration:
- Vite config includes PWA, image optimization, and bundle analysis
- Manual chunking strategy for vendor libraries (vue-vendor, ui-vendor, shadcn)
- Electron Forge configured for cross-platform packaging
- TailwindCSS v4 integration via Vite plugin
Environment:
- Nostr relay configuration via
VITE_NOSTR_RELAYSenvironment variable - PWA manifest configured for standalone app experience
- Service worker with automatic updates every hour