web-app/docs/06-deployment/index.md
padreug cdf099e45f Create comprehensive Obsidian-style documentation structure
- Reorganize all markdown documentation into structured docs/ folder
- Create 7 main documentation categories (00-overview through 06-deployment)
- Add comprehensive index files for each category with cross-linking
- Implement Obsidian-compatible [[link]] syntax throughout
- Move legacy/deprecated documentation to archive folder
- Establish documentation standards and maintenance guidelines
- Provide complete coverage of modular architecture, services, and deployment
- Enable better navigation and discoverability for developers and contributors

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 14:31:27 +02:00

16 KiB

🚀 Deployment Guide

Production deployment and operations for Ario's web application, desktop app, and Progressive Web App across multiple platforms and environments.

Table of Contents

Deployment Options

Web Application (SPA)

  • Static Hosting - Deploy to CDN or static hosting service
  • Traditional Web Server - Nginx, Apache, or similar
  • Cloud Platforms - Vercel, Netlify, Cloudflare Pages
  • Container Deployment - Docker with web server

Progressive Web App (PWA)

  • Service Worker - Offline capabilities and caching
  • App Manifest - Installation and app-like experience
  • Push Notifications - Real-time updates (future feature)
  • Background Sync - Offline data synchronization

Desktop Application

  • Electron - Cross-platform desktop packaging
  • Platform-Specific - Windows MSI, macOS DMG, Linux AppImage/Snap
  • Auto-Update - Seamless application updates
  • Code Signing - Security and trust verification

Self-Hosted

  • Full Control - Complete control over infrastructure
  • Privacy - No third-party hosting dependencies
  • Custom Configuration - Tailored environment variables and settings
  • Local Development - Internal network deployment

Production Build Process

Build Commands

# Standard web build
npm run build

# Build with analysis
npm run analyze

# Electron desktop build
npm run electron:build

# Platform-specific builds
npm run build:win     # Windows
npm run build:mac     # macOS
npm run build:linux   # Linux

Build Configuration

Vite Production Config

// vite.config.ts - Production optimizations
export default defineConfig({
  build: {
    target: 'es2020',
    minify: 'terser',
    sourcemap: false,        // Disable in production
    chunkSizeWarningLimit: 600,
    
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['@headlessui/vue', '@heroicons/vue'],
          'nostr-vendor': ['nostr-tools'],
          'crypto-vendor': ['crypto-js']
        }
      }
    }
  }
})

Build Optimization

  • Tree Shaking - Remove unused code
  • Code Splitting - Lazy load modules and routes
  • Asset Optimization - Compress images and fonts
  • Bundle Analysis - Monitor bundle size and dependencies

Build Output Structure

dist/
├── index.html              # Entry point
├── assets/                 # Static assets with hashed names
│   ├── index-[hash].js    # Main application bundle
│   ├── vendor-[hash].js   # Vendor dependencies
│   └── [module]-[hash].js # Module-specific bundles
├── icons/                 # PWA icons
├── manifest.json          # PWA manifest
└── sw.js                  # Service worker

Environment Configuration

Environment Variables

Production Environment (.env.production)

# Application Configuration
VITE_APP_NAME="Ario"
VITE_APP_VERSION="1.0.0"
VITE_BASE_URL="/"

# Nostr Configuration
VITE_NOSTR_RELAYS='["wss://relay.damus.io","wss://nos.lol","wss://relay.snort.social"]'
VITE_ADMIN_PUBKEYS='["admin_pubkey_1","admin_pubkey_2"]'

# Lightning Configuration (if using LNbits)
VITE_LNBITS_URL="https://your-lnbits-instance.com"
VITE_LNBITS_ADMIN_KEY=""  # Only for invoice creation

# Security & Performance
VITE_DEBUG=false
VITE_ENABLE_PWA=true
VITE_ENABLE_ANALYTICS=true

# Optional Features
VITE_ENABLE_NOTIFICATIONS=true
VITE_MAX_RELAY_CONNECTIONS=10
VITE_EVENT_CACHE_SIZE=1000

Configuration Validation

// src/config/production.ts
export const productionConfig = {
  app: {
    name: import.meta.env.VITE_APP_NAME || 'Ario',
    version: import.meta.env.VITE_APP_VERSION || '1.0.0',
    baseUrl: import.meta.env.VITE_BASE_URL || '/',
  },
  
  nostr: {
    relays: JSON.parse(import.meta.env.VITE_NOSTR_RELAYS || '[]'),
    adminPubkeys: JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS || '[]'),
    maxConnections: Number(import.meta.env.VITE_MAX_RELAY_CONNECTIONS) || 10,
  },
  
  features: {
    debug: import.meta.env.VITE_DEBUG === 'true',
    pwa: import.meta.env.VITE_ENABLE_PWA === 'true',
    notifications: import.meta.env.VITE_ENABLE_NOTIFICATIONS === 'true',
  }
}

// Validate required configuration
if (!productionConfig.nostr.relays.length) {
  throw new Error('VITE_NOSTR_RELAYS must be configured')
}

Security Configuration

Content Security Policy (CSP)

<!-- In index.html for production -->
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' 'unsafe-inline';
  style-src 'self' 'unsafe-inline';
  connect-src 'self' wss: ws: https:;
  img-src 'self' data: https:;
  font-src 'self' data:;
  manifest-src 'self';
">

Security Headers (nginx example)

# nginx.conf security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Referrer-Policy "strict-origin-when-cross-origin";
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()";

Platform-Specific Deployment

Web Hosting - Vercel

vercel.json Configuration

{
  "buildCommand": "npm run build",
  "outputDirectory": "dist",
  "framework": "vite",
  "rewrites": [
    {
      "source": "/((?!api/).*)",
      "destination": "/index.html"
    }
  ],
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "X-Frame-Options",
          "value": "DENY"
        },
        {
          "key": "X-Content-Type-Options",
          "value": "nosniff"
        }
      ]
    }
  ]
}

Deployment Steps

# 1. Install Vercel CLI
npm i -g vercel

# 2. Deploy to Vercel
vercel --prod

# 3. Set environment variables in Vercel dashboard
# - VITE_NOSTR_RELAYS
# - VITE_ADMIN_PUBKEYS
# - Other configuration variables

Web Hosting - Netlify

netlify.toml Configuration

[build]
  command = "npm run build"
  publish = "dist"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

[build.environment]
  NODE_VERSION = "18"

[[headers]]
  for = "/*"
  [headers.values]
    X-Frame-Options = "DENY"
    X-XSS-Protection = "1; mode=block"
    X-Content-Type-Options = "nosniff"

Self-Hosted - Docker

Dockerfile

# Multi-stage build for production
FROM node:18-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

# Production stage
FROM nginx:alpine

# Copy built application
COPY --from=builder /app/dist /usr/share/nginx/html

# Copy nginx configuration
COPY docker/nginx.conf /etc/nginx/nginx.conf

# Expose port
EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

Docker Compose

# docker-compose.yml
version: '3.8'

services:
  ario-web:
    build: .
    ports:
      - "80:80"
    environment:
      - VITE_NOSTR_RELAYS=["wss://relay.damus.io"]
      - VITE_ADMIN_PUBKEYS=["your_admin_pubkey"]
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
    restart: unless-stopped

Desktop Application

Electron Forge Configuration

// forge.config.js
module.exports = {
  packagerConfig: {
    name: 'Ario',
    executableName: 'ario',
    icon: './src/assets/icon',
    asar: true,
    
    // Code signing (production)
    osxSign: {
      identity: process.env.APPLE_IDENTITY,
      'hardened-runtime': true,
      entitlements: './entitlements.plist',
      'entitlements-inherit': './entitlements.plist'
    },
    
    osxNotarize: {
      tool: 'notarytool',
      appleId: process.env.APPLE_ID,
      appleIdPassword: process.env.APPLE_PASSWORD,
      teamId: process.env.APPLE_TEAM_ID
    }
  },
  
  makers: [
    {
      name: '@electron-forge/maker-squirrel',
      config: {
        name: 'ario',
        setupIcon: './src/assets/icon.ico'
      }
    },
    {
      name: '@electron-forge/maker-dmg',
      config: {
        format: 'ULFO',
        icon: './src/assets/icon.icns'
      }
    },
    {
      name: '@electron-forge/maker-deb',
      config: {
        options: {
          maintainer: 'Ario Team',
          homepage: 'https://ario.app'
        }
      }
    }
  ],
  
  publishers: [
    {
      name: '@electron-forge/publisher-github',
      config: {
        repository: {
          owner: 'your-org',
          name: 'ario'
        },
        prerelease: false
      }
    }
  ]
}

Auto-Update Configuration

// electron/main.ts - Auto-updater setup
import { autoUpdater } from 'electron-updater'

if (!app.isPackaged) {
  autoUpdater.updateConfigPath = path.join(__dirname, 'dev-app-update.yml')
}

autoUpdater.checkForUpdatesAndNotify()

autoUpdater.on('update-available', () => {
  dialog.showMessageBox(mainWindow, {
    type: 'info',
    title: 'Update Available',
    message: 'A new version is available. It will be downloaded in the background.',
    buttons: ['OK']
  })
})

Monitoring & Maintenance

Performance Monitoring

Web Vitals Tracking

// src/utils/analytics.ts
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

export function initPerformanceMonitoring() {
  if (import.meta.env.VITE_ENABLE_ANALYTICS) {
    getCLS(sendToAnalytics)
    getFID(sendToAnalytics)
    getFCP(sendToAnalytics)
    getLCP(sendToAnalytics)
    getTTFB(sendToAnalytics)
  }
}

function sendToAnalytics(metric: any) {
  // Send to your analytics service
  console.log('Performance metric:', metric)
}

Application Health Monitoring

// src/services/HealthMonitor.ts
export class HealthMonitor extends BaseService {
  private healthStatus = ref({
    relayConnections: 0,
    lastEventReceived: null as Date | null,
    memoryUsage: 0,
    errorCount: 0
  })
  
  async initialize(): Promise<void> {
    setInterval(() => this.checkHealth(), 30000) // Every 30 seconds
  }
  
  private checkHealth(): void {
    const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
    
    this.healthStatus.value = {
      relayConnections: relayHub.connectedRelays.value.length,
      lastEventReceived: this.getLastEventTime(),
      memoryUsage: this.getMemoryUsage(),
      errorCount: this.getErrorCount()
    }
    
    // Alert if critical issues detected
    if (this.healthStatus.value.relayConnections === 0) {
      console.warn('No relay connections available')
    }
  }
}

Error Tracking

Global Error Handler

// src/utils/errorHandler.ts
export function setupGlobalErrorHandler() {
  window.addEventListener('error', (event) => {
    console.error('Global error:', event.error)
    
    // Send to error tracking service
    if (import.meta.env.VITE_ERROR_TRACKING) {
      sendErrorToService(event.error)
    }
  })
  
  window.addEventListener('unhandledrejection', (event) => {
    console.error('Unhandled promise rejection:', event.reason)
    sendErrorToService(event.reason)
  })
}

Logging Strategy

Production Logging

// src/utils/logger.ts
class Logger {
  private shouldLog(level: LogLevel): boolean {
    if (import.meta.env.PROD && level === 'debug') return false
    return true
  }
  
  info(message: string, ...args: unknown[]): void {
    if (this.shouldLog('info')) {
      console.log(`[INFO] ${message}`, ...args)
    }
  }
  
  error(message: string, error?: Error, ...args: unknown[]): void {
    if (this.shouldLog('error')) {
      console.error(`[ERROR] ${message}`, error, ...args)
      
      // Send to error tracking in production
      if (import.meta.env.PROD && error) {
        this.sendToErrorTracking(message, error)
      }
    }
  }
  
  private sendToErrorTracking(message: string, error: Error): void {
    // Implementation for error tracking service
  }
}

export const logger = new Logger()

Security Considerations

Client-Side Security

Key Storage Security

// src/utils/keyStorage.ts
export class SecureKeyStorage {
  private static readonly STORAGE_KEY = 'ario_encrypted_keys'
  
  static async storeEncryptedKey(privateKey: string, passphrase: string): Promise<void> {
    const encrypted = await this.encrypt(privateKey, passphrase)
    localStorage.setItem(this.STORAGE_KEY, encrypted)
  }
  
  static async retrieveDecryptedKey(passphrase: string): Promise<string | null> {
    const encrypted = localStorage.getItem(this.STORAGE_KEY)
    if (!encrypted) return null
    
    try {
      return await this.decrypt(encrypted, passphrase)
    } catch {
      return null // Invalid passphrase
    }
  }
  
  private static async encrypt(data: string, passphrase: string): Promise<string> {
    // Use Web Crypto API for encryption
    const encoder = new TextEncoder()
    const key = await window.crypto.subtle.importKey(
      'raw',
      encoder.encode(passphrase),
      { name: 'PBKDF2' },
      false,
      ['deriveKey']
    )
    
    // Implementation details...
    return encryptedData
  }
}

Input Validation

// src/utils/validation.ts
export const validators = {
  nostrPublicKey: (pubkey: string): boolean => {
    return /^[0-9a-f]{64}$/i.test(pubkey)
  },
  
  nostrPrivateKey: (privkey: string): boolean => {
    return /^[0-9a-f]{64}$/i.test(privkey)
  },
  
  lightningInvoice: (invoice: string): boolean => {
    return /^(lnbc|lntb|lnbcrt)[0-9]+[munp]?[0-9a-z]+$/i.test(invoice)
  },
  
  sanitizeContent: (content: string): string => {
    // Sanitize user-generated content
    return content
      .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
      .replace(/javascript:/gi, '')
      .trim()
  }
}

Deployment Security

HTTPS Enforcement

# nginx.conf - Force HTTPS
server {
    listen 80;
    server_name ario.app www.ario.app;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name ario.app www.ario.app;
    
    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/private.key;
    
    # Security headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Frame-Options DENY always;
    add_header X-Content-Type-Options nosniff always;
    
    location / {
        root /usr/share/nginx/html;
        try_files $uri $uri/ /index.html;
    }
}

Privacy Protection

Data Minimization

  • No Server Storage - All user data stored client-side
  • Ephemeral Sessions - No persistent server-side sessions
  • Optional Analytics - Analytics can be disabled by users
  • Relay Privacy - Users can configure their own relays

User Privacy Controls

// src/services/PrivacyService.ts
export class PrivacyService extends BaseService {
  private settings = ref({
    shareAnalytics: false,
    shareErrorReports: false,
    allowLocationTracking: false,
    publicProfile: false
  })
  
  updatePrivacySettings(updates: Partial<PrivacySettings>): void {
    this.settings.value = { ...this.settings.value, ...updates }
    
    // Apply privacy settings immediately
    if (!this.settings.value.shareAnalytics) {
      this.disableAnalytics()
    }
    
    if (!this.settings.value.shareErrorReports) {
      this.disableErrorReporting()
    }
  }
}

See Also

Configuration Documentation

Operations Documentation


Tags: #deployment #production #security #monitoring #pwa #electron
Last Updated: 2025-09-06
Author: Development Team