Architecture
This document provides a detailed overview of License Server Detail’s architecture, including the component structure, data flow, and integration points.
High-Level Architecture
Section titled “High-Level Architecture”License Server Detail follows a layered architecture pattern with clear separation of concerns:
┌─────────────────────────────────────────────────────────────────────┐│ Presentation Layer ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ ││ │ Dashboard │ │ Server │ │ Monitoring │ │ Settings │ ││ │ Pages │ │ Pages │ │ Pages │ │ Pages │ ││ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └─────┬─────┘ │└──────────┼─────────────────┼─────────────────┼──────────────┼────────┘ │ │ │ │ ▼ ▼ ▼ ▼┌─────────────────────────────────────────────────────────────────────┐│ Component Layer ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ ││ │ Charts │ │ Tables │ │ Forms │ │ Cards │ ││ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └─────┬─────┘ │└──────────┼─────────────────┼─────────────────┼──────────────┼────────┘ │ │ │ │ ▼ ▼ ▼ ▼┌─────────────────────────────────────────────────────────────────────┐│ Hooks Layer ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ ││ │ useServers │ │ useMetrics │ │ useHealth │ │ useAlerts │ ││ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └─────┬─────┘ │└──────────┼─────────────────┼─────────────────┼──────────────┼────────┘ │ │ │ │ ▼ ▼ ▼ ▼┌─────────────────────────────────────────────────────────────────────┐│ Services Layer ││ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ ││ │ HTTP Client │ │ WebSocket │ │ SSE │ │ Alerting │ ││ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └─────┬─────┘ │└──────────┼─────────────────┼─────────────────┼──────────────┼────────┘ │ │ │ │ └─────────────────┴─────────────────┴──────────────┘ │ ▼┌─────────────────────────────────────────────────────────────────────┐│ Data Layer ││ ┌──────────────────────────┐ ┌────────────────────────────────┐ ││ │ Convex Backend │ │ License Monitor API │ ││ └──────────────────────────┘ └────────────────────────────────┘ │└─────────────────────────────────────────────────────────────────────┘Component Architecture
Section titled “Component Architecture”Next.js App Router Structure
Section titled “Next.js App Router Structure”The application uses Next.js 15 with the App Router pattern:
app/├── layout.tsx # Root layout with providers├── page.tsx # Landing page├── api/│ ├── auth/│ │ └── [...nextauth]/│ │ └── route.ts # NextAuth handler│ ├── health/│ │ └── route.ts # Health check endpoint│ └── trpc/│ └── [trpc]/│ └── route.ts # tRPC handler├── auth/│ ├── signin/│ │ └── page.tsx # Sign in page│ └── error/│ └── page.tsx # Auth error page└── dashboard/ ├── layout.tsx # Dashboard layout (protected) ├── page.tsx # Dashboard home ├── servers/ │ ├── page.tsx # Server list │ └── [id]/ │ └── page.tsx # Server details ├── monitoring/ │ └── page.tsx # System monitoring ├── security/ │ └── page.tsx # Security settings └── settings/ └── page.tsx # App settingsProvider Hierarchy
Section titled “Provider Hierarchy”// Root layout provider structure<AuthProvider> <ConvexClientProvider> <TRPCProvider> <QueryClientProvider> <ThemeProvider> {children} </ThemeProvider> </QueryClientProvider> </TRPCProvider> </ConvexClientProvider></AuthProvider>HTTP Client Architecture
Section titled “HTTP Client Architecture”The HTTP client (lib/http/client.ts) provides a robust foundation for API communication:
┌─────────────────────────────────────────────────────────────────┐│ HTTP Client │├─────────────────────────────────────────────────────────────────┤│ Request Interceptors ││ ┌─────────────────────────────────────────────────────────────┐││ │ 1. Add Authorization: Bearer <token> │││ │ 2. Add X-Request-ID correlation header │││ │ 3. Add Content-Type headers │││ └─────────────────────────────────────────────────────────────┘│├─────────────────────────────────────────────────────────────────┤│ Response Interceptors ││ ┌─────────────────────────────────────────────────────────────┐││ │ 1. 401/403 → Refresh session → Retry once │││ │ 2. 429/5xx → Exponential backoff → Retry (max 3) │││ │ 3. Circuit breaker → Open after 5 failures │││ │ 4. Normalize errors → ApiError with requestId │││ └─────────────────────────────────────────────────────────────┘│├─────────────────────────────────────────────────────────────────┤│ Circuit Breaker ││ ┌─────────────────────────────────────────────────────────────┐││ │ State: CLOSED → OPEN (after failures) → HALF-OPEN → CLOSED │││ │ Cooldown: 10 seconds (configurable) │││ │ Failure threshold: 5 consecutive failures │││ └─────────────────────────────────────────────────────────────┘│└─────────────────────────────────────────────────────────────────┘Request Flow
Section titled “Request Flow”// Simplified request flowasync function makeRequest(config: RequestConfig) { // 1. Check circuit breaker state if (circuitBreaker.isOpen()) { throw new Error('Circuit breaker is open'); }
// 2. Add authentication header const token = await getAccessToken(); config.headers['Authorization'] = `Bearer ${token}`; config.headers['X-Request-ID'] = generateCorrelationId();
try { // 3. Make request const response = await axios(config); circuitBreaker.recordSuccess(); return response; } catch (error) { // 4. Handle 401/403 - refresh and retry if (error.status === 401 || error.status === 403) { await refreshSession(); return axios(config); // Retry once }
// 5. Handle transient errors with retry if (isTransientError(error)) { return retryWithBackoff(config, error); }
// 6. Record failure for circuit breaker circuitBreaker.recordFailure(); throw normalizeError(error); }}Authentication Flow
Section titled “Authentication Flow”┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│ User │ │ Next.js │ │ Okta │ │ License ││ Browser │ │ Server │ │ │ │ Monitor │└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ │ Click Login │ │ │ │───────────────>│ │ │ │ │ │ │ │ │ Redirect to │ │ │ │ Okta OAuth │ │ │<───────────────────────────────>│ │ │ │ │ │ │ User Authenticates │ │ │<───────────────────────────────>│ │ │ │ │ │ │ Callback with │ │ │ │ Auth Code │ │ │ │───────────────>│ │ │ │ │ │ │ │ │ Exchange for │ │ │ │ Tokens │ │ │ │───────────────>│ │ │ │<───────────────│ │ │ │ │ │ │ │ Create JWT │ │ │ │ Session │ │ │<───────────────│ │ │ │ │ │ │ │ API Request │ │ │ │ with Token │ │ │ │───────────────>│ │ │ │ │ │ │ │ │ Forward with │ │ │ │ API Key │ │ │ │───────────────────────────────>│ │ │<───────────────────────────────│ │<───────────────│ │ │ │ │ │ │State Management
Section titled “State Management”React Query Configuration
Section titled “React Query Configuration”// Query client configurationconst queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 30 * 1000, // 30 seconds gcTime: 5 * 60 * 1000, // 5 minutes retry: 3, retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30000), refetchOnWindowFocus: true, refetchOnReconnect: true, }, mutations: { retry: 1, }, },});Query Key Structure
Section titled “Query Key Structure”// Standardized query key patternsconst queryKeys = { servers: { all: ['servers'] as const, list: (filters?: ServerFilters) => ['servers', 'list', filters] as const, detail: (id: string) => ['servers', 'detail', id] as const, health: (id: string) => ['servers', 'health', id] as const, metrics: (id: string) => ['servers', 'metrics', id] as const, }, licenses: { all: ['licenses'] as const, byServer: (serverId: string) => ['licenses', 'server', serverId] as const, sessions: (serverId: string) => ['licenses', 'sessions', serverId] as const, }, alerts: { all: ['alerts'] as const, unresolved: () => ['alerts', 'unresolved'] as const, byServer: (serverId: string) => ['alerts', 'server', serverId] as const, },};Error Handling
Section titled “Error Handling”Error Normalization
Section titled “Error Normalization”interface ApiError extends Error { status: number; code: string; message: string; requestId: string; method: string; url: string; timestamp: Date;}
// Error creationfunction normalizeError(error: unknown): ApiError { if (isAxiosError(error)) { return { name: 'ApiError', status: error.response?.status ?? 0, code: error.response?.data?.code ?? 'UNKNOWN_ERROR', message: error.response?.data?.message ?? error.message, requestId: error.config?.headers?.['X-Request-ID'] ?? '', method: error.config?.method?.toUpperCase() ?? 'UNKNOWN', url: error.config?.url ?? '', timestamp: new Date(), }; } // Handle other error types...}Error Boundary Strategy
Section titled “Error Boundary Strategy”// Page-level error boundaryexport default function DashboardErrorBoundary({ error, reset,}: { error: Error & { digest?: string }; reset: () => void;}) { useEffect(() => { // Log to error tracking service logger.error('Dashboard error', { error, digest: error.digest }); }, [error]);
return ( <div className="error-container"> <h2>Something went wrong</h2> <p>{error.message}</p> <button onClick={reset}>Try again</button> </div> );}Logging Architecture
Section titled “Logging Architecture”Logger Configuration
Section titled “Logger Configuration”// Log levels and configurationtype LogLevel = 'silent' | 'error' | 'warn' | 'info' | 'debug';
const logger = createLogger({ level: process.env.NEXT_PUBLIC_LOG_LEVEL ?? (isDev ? 'debug' : 'info'), redactFields: ['token', 'secret', 'password', 'apiKey', 'authorization'], format: isDev ? 'pretty' : 'json',});Structured Logging
Section titled “Structured Logging”// Log entry structureinterface LogEntry { timestamp: string; level: LogLevel; message: string; context?: string; requestId?: string; metadata?: Record<string, unknown>;}
// Usagelogger.info('Server health check completed', { context: 'HealthService', requestId: 'req-123', metadata: { serverId: 'server-1', status: 'online', responseTime: 45, },});Performance Optimizations
Section titled “Performance Optimizations”Connection Pooling
Section titled “Connection Pooling”WebSocket connections are pooled and reused:
const pool = new ConnectionPool({ maxConnections: 5, loadBalancingStrategy: 'least-connections', healthCheckInterval: 30000,});Query Optimization
Section titled “Query Optimization”// Prefetching related queriesasync function prefetchServerData(serverId: string) { await Promise.all([ queryClient.prefetchQuery({ queryKey: queryKeys.servers.detail(serverId), queryFn: () => fetchServer(serverId), }), queryClient.prefetchQuery({ queryKey: queryKeys.servers.health(serverId), queryFn: () => fetchServerHealth(serverId), }), queryClient.prefetchQuery({ queryKey: queryKeys.licenses.byServer(serverId), queryFn: () => fetchServerLicenses(serverId), }), ]);}Next Steps
Section titled “Next Steps”- Authentication - Okta setup details
- Real-Time Streaming - WebSocket and SSE
- Data Model - Database schema