17 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) - 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.VISIBILITY_SERVICE- App visibility and connection management
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
- 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
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>
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
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:
// Always extend BaseService for services
export class MyModuleService extends BaseService {
protected readonly metadata = {
name: 'MyModuleService',
version: '1.0.0',
dependencies: [] // List service dependencies
}
protected async onInitialize(): Promise<void> {
// Service-specific initialization
}
}
// API services for LNbits integration
export class MyModuleAPI extends BaseService {
private baseUrl: string
constructor() {
super()
const config = appConfig.modules.myModule.config
this.baseUrl = config.apiConfig.baseUrl
}
// API methods here
}
Module Plugin Pattern:
export const myModule: ModulePlugin = {
name: 'my-module',
version: '1.0.0',
dependencies: ['base'], // Always depend on base
async install(app: App, options?: { config?: MyModuleConfig }) {
// 1. Create service instances
const myService = new MyModuleService()
const myAPI = new MyModuleAPI()
// 2. Register in DI container
container.provide(SERVICE_TOKENS.MY_SERVICE, myService)
container.provide(SERVICE_TOKENS.MY_API, myAPI)
// 3. Initialize services
await myService.initialize({
waitForDependencies: true,
maxRetries: 3
})
// 4. Register components if needed
app.component('MyComponent', MyComponent)
}
}
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