Skip to content

Architecture

This document provides a detailed overview of License Server Detail’s architecture, including the component structure, data flow, and integration points.

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 │ │
│ └──────────────────────────┘ └────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘

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 settings
// Root layout provider structure
<AuthProvider>
<ConvexClientProvider>
<TRPCProvider>
<QueryClientProvider>
<ThemeProvider>
{children}
</ThemeProvider>
</QueryClientProvider>
</TRPCProvider>
</ConvexClientProvider>
</AuthProvider>

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 ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
// Simplified request flow
async 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);
}
}
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 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 │ │
│ │───────────────────────────────>│
│ │<───────────────────────────────│
│<───────────────│ │ │
│ │ │ │
// Query client configuration
const 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,
},
},
});
// Standardized query key patterns
const 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,
},
};
interface ApiError extends Error {
status: number;
code: string;
message: string;
requestId: string;
method: string;
url: string;
timestamp: Date;
}
// Error creation
function 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...
}
// Page-level error boundary
export 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>
);
}
// Log levels and configuration
type 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',
});
// Log entry structure
interface LogEntry {
timestamp: string;
level: LogLevel;
message: string;
context?: string;
requestId?: string;
metadata?: Record<string, unknown>;
}
// Usage
logger.info('Server health check completed', {
context: 'HealthService',
requestId: 'req-123',
metadata: {
serverId: 'server-1',
status: 'online',
responseTime: 45,
},
});

WebSocket connections are pooled and reused:

const pool = new ConnectionPool({
maxConnections: 5,
loadBalancingStrategy: 'least-connections',
healthCheckInterval: 30000,
});
// Prefetching related queries
async 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),
}),
]);
}