VoiceAssist Client Applications - Technical Architecture
Version: 1.0 Date: 2025-11-21 Status: Draft - Awaiting Team Review
π Table of Contents
- Architecture Overview
- Monorepo Structure
- Shared Packages
- State Management
- API Communication
- Real-time Communication
- Authentication & Authorization
- Routing & Navigation
- Performance Optimization
- Security Architecture
- Testing Architecture
- Build & Deployment
1. Architecture Overview
High-Level Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β User Devices β
β (Desktop, Tablet, Mobile - Chrome, Firefox, Safari, Edge) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CDN / Edge Cache β
β (CloudFlare / AWS CloudFront) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββΌββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ
β Web App β β Admin β β Docs β
β (SPA) β β Panel β β Site β
ββββββββββββ ββββββββββββ ββββββββββββ
β β β
βββββββββββββββΌββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββ
β Load Balancer / β
β API Gateway β
βββββββββββββββββββββββββββ
β
βββββββββββββββΌββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ
β HTTP β βWebSocket β β Auth β
β REST API β β Server β β Service β
ββββββββββββ ββββββββββββ ββββββββββββ
β
βββββββββββββββΌββββββββββββββ
β β β
βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ
βPostgreSQLβ β Redis β β Qdrant β
β (pgvector)β β Cache β β Vector β
ββββββββββββ ββββββββββββ ββββββββββββ
Design Principles
- Monorepo First - Single repository for all client applications
- Shared Core - Maximum code reuse through shared packages
- Type Safety - End-to-end TypeScript with strict mode
- Performance - Code splitting, lazy loading, optimized bundles
- Accessibility - WCAG 2.1 AA compliance across all apps
- Security - Defense in depth, secure by default
- Testability - High test coverage with automated testing
- Maintainability - Clean code, clear patterns, good documentation
2. Monorepo Structure
Directory Layout
VoiceAssist/
βββ apps/
β βββ web-app/ # Main user-facing application
β β βββ public/ # Static assets
β β βββ src/
β β β βββ components/ # React components
β β β βββ pages/ # Page components
β β β βββ hooks/ # Custom hooks
β β β βββ stores/ # Zustand stores
β β β βββ utils/ # App-specific utilities
β β β βββ types/ # App-specific types
β β β βββ App.tsx # Root component
β β β βββ main.tsx # Entry point
β β βββ index.html
β β βββ package.json
β β βββ tsconfig.json
β β βββ vite.config.ts
β β βββ tailwind.config.js
β β
β βββ admin-panel/ # Admin/management application
β β βββ public/
β β βββ src/
β β β βββ components/
β β β βββ pages/
β β β βββ hooks/
β β β βββ stores/
β β β βββ utils/
β β β βββ types/
β β β βββ App.tsx
β β β βββ main.tsx
β β βββ index.html
β β βββ package.json
β β βββ tsconfig.json
β β βββ vite.config.ts
β β βββ tailwind.config.js
β β
β βββ docs-site/ # Documentation site
β βββ app/ # Next.js app directory
β βββ content/ # MDX content
β βββ components/
β βββ public/
β βββ package.json
β βββ tsconfig.json
β βββ next.config.js
β βββ tailwind.config.js
β
βββ packages/
β βββ ui/ # Shared UI component library
β β βββ src/
β β β βββ components/
β β β β βββ Button/
β β β β β βββ Button.tsx
β β β β β βββ Button.test.tsx
β β β β β βββ Button.stories.tsx
β β β β β βββ index.ts
β β β β βββ Input/
β β β β βββ Card/
β β β β βββ ...
β β β βββ hooks/
β β β β βββ useMediaQuery.ts
β β β β βββ useDebounce.ts
β β β β βββ ...
β β β βββ index.ts
β β βββ package.json
β β βββ tsconfig.json
β β
β βββ types/ # Shared TypeScript types
β β βββ src/
β β β βββ api/
β β β β βββ auth.ts
β β β β βββ chat.ts
β β β β βββ admin.ts
β β β β βββ ...
β β β βββ models/
β β β β βββ user.ts
β β β β βββ message.ts
β β β β βββ conversation.ts
β β β β βββ ...
β β β βββ events/
β β β β βββ websocket.ts
β β β β βββ ...
β β β βββ index.ts
β β βββ package.json
β β βββ tsconfig.json
β β
β βββ api-client/ # Shared API client
β β βββ src/
β β β βββ client.ts # Axios instance
β β β βββ auth.ts # Auth endpoints
β β β βββ chat.ts # Chat endpoints
β β β βββ admin.ts # Admin endpoints
β β β βββ websocket.ts # WebSocket manager
β β β βββ index.ts
β β βββ package.json
β β βββ tsconfig.json
β β
β βββ utils/ # Shared utilities
β β βββ src/
β β β βββ formatting/
β β β β βββ date.ts
β β β β βββ currency.ts
β β β β βββ number.ts
β β β β βββ ...
β β β βββ validation/
β β β β βββ schemas.ts # Zod schemas
β β β β βββ ...
β β β βββ constants/
β β β β βββ specialties.ts
β β β β βββ countries.ts
β β β β βββ ...
β β β βββ index.ts
β β βββ package.json
β β βββ tsconfig.json
β β
β βββ config/ # Shared configurations
β βββ eslint/
β β βββ index.js
β βββ typescript/
β β βββ base.json
β β βββ react.json
β β βββ nextjs.json
β βββ tailwind/
β βββ base.js
β
βββ server/ # Backend (existing)
β
βββ docs/ # Project documentation
β βββ client-implementation/
β
βββ .github/
β βββ workflows/
β βββ ci.yml
β βββ deploy-web-app.yml
β βββ deploy-admin.yml
β βββ deploy-docs.yml
β
βββ pnpm-workspace.yaml
βββ turbo.json
βββ package.json
βββ README.md
Package Manager: pnpm with Workspaces
Why pnpm?
- Faster than npm/yarn (symlinked node_modules)
- Disk space efficient (global store)
- Strict dependency resolution
- Built-in monorepo support
Configuration:
# pnpm-workspace.yaml packages: - "apps/*" - "packages/*"
// package.json (root) { "name": "voiceassist-monorepo", "private": true, "scripts": { "dev": "turbo run dev", "build": "turbo run build", "test": "turbo run test", "lint": "turbo run lint", "type-check": "turbo run type-check", "clean": "turbo run clean && rm -rf node_modules" }, "devDependencies": { "turbo": "^1.11.0", "typescript": "^5.3.0" }, "engines": { "node": ">=18.0.0", "pnpm": ">=8.0.0" } }
Build Orchestration: Turborepo
Why Turborepo?
- Intelligent task caching
- Parallel execution
- Remote caching support
- Dependency-aware scheduling
Configuration:
// turbo.json { "$schema": "https://turbo.build/schema.json", "globalDependencies": [".env", "tsconfig.json"], "pipeline": { "build": { "dependsOn": ["^build"], "outputs": ["dist/**", ".next/**", "out/**"] }, "dev": { "cache": false, "persistent": true }, "test": { "dependsOn": ["build"], "outputs": ["coverage/**"] }, "lint": { "outputs": [] }, "type-check": { "dependsOn": ["^build"], "outputs": [] }, "clean": { "cache": false } } }
3. Shared Packages
3.1 UI Component Library (@voiceassist/ui)
Purpose: Shared, reusable UI components across all applications
Key Components:
// packages/ui/src/components/Button/Button.tsx import { forwardRef } from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "../../utils/cn"; const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none", { variants: { variant: { primary: "bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-600", secondary: "bg-gray-200 text-gray-900 hover:bg-gray-300 focus-visible:ring-gray-500", outline: "border border-gray-300 bg-transparent hover:bg-gray-100 focus-visible:ring-gray-500", ghost: "hover:bg-gray-100 hover:text-gray-900", danger: "bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-600", }, size: { sm: "h-9 px-3", md: "h-10 px-4", lg: "h-11 px-8", icon: "h-10 w-10", }, }, defaultVariants: { variant: "primary", size: "md", }, }, ); export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> { asChild?: boolean; } export const Button = forwardRef<HTMLButtonElement, ButtonProps>( ({ className, variant, size, asChild = false, ...props }, ref) => { return <button className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />; }, ); Button.displayName = "Button";
Component Organization:
Each component follows this structure:
Button/
βββ Button.tsx # Main component
βββ Button.test.tsx # Unit tests
βββ Button.stories.tsx # Storybook stories
βββ types.ts # TypeScript types
βββ index.ts # Exports
Shared Hooks:
// packages/ui/src/hooks/useMediaQuery.ts import { useState, useEffect } from "react"; export function useMediaQuery(query: string): boolean { const [matches, setMatches] = useState(false); useEffect(() => { const media = window.matchMedia(query); // Set initial value setMatches(media.matches); // Listen for changes const listener = (event: MediaQueryListEvent) => { setMatches(event.matches); }; media.addEventListener("change", listener); return () => media.removeEventListener("change", listener); }, [query]); return matches; } // Usage example const isMobile = useMediaQuery("(max-width: 768px)");
3.2 Types Package (@voiceassist/types)
Purpose: Shared TypeScript types for type-safe communication
// packages/types/src/models/user.ts export interface User { id: string; email: string; firstName: string; lastName: string; role: "user" | "admin"; specialty?: string; licenseNumber?: string; institution?: string; avatarUrl?: string; phone?: string; bio?: string; createdAt: string; updatedAt: string; } export interface AuthTokens { accessToken: string; refreshToken: string; tokenType: "Bearer"; expiresIn: number; } // packages/types/src/models/message.ts export interface ChatMessage { id: string; sessionId: string; role: "user" | "assistant" | "system"; content: string; citations?: Citation[]; attachments?: Attachment[]; metadata?: Record<string, any>; createdAt: string; streaming?: boolean; } export interface Citation { id: string; sourceType: "textbook" | "journal" | "guideline" | "uptodate" | "note" | "trial"; title: string; subtitle?: string; authors?: string[]; publicationYear?: number; recommendationClass?: "I" | "IIa" | "IIb" | "III"; evidenceLevel?: "A" | "B" | "C"; doi?: string; pmid?: string; url?: string; excerpt?: string; relevanceScore?: number; } // packages/types/src/events/websocket.ts export type ClientEvent = | { type: "session.start"; sessionId?: string; mode: string; clinicalContext?: any } | { type: "message.send"; sessionId: string; content: string; attachments?: string[] } | { type: "audio.chunk"; sessionId: string; data: ArrayBuffer } | { type: "generation.stop"; sessionId: string }; export type ServerEvent = | { type: "session.started"; sessionId: string } | { type: "message.delta"; sessionId: string; messageId: string; role: string; contentDelta: string } | { type: "message.complete"; sessionId: string; messageId: string } | { type: "citation.list"; sessionId: string; messageId: string; citations: Citation[] } | { type: "audio.chunk"; sessionId: string; data: ArrayBuffer } | { type: "error"; code: string; message: string };
3.3 API Client Package (@voiceassist/api-client)
Purpose: Centralized API communication with type safety
// packages/api-client/src/client.ts import axios, { AxiosInstance, AxiosError } from "axios"; import { useAuth } from "@voiceassist/stores"; // Import from shared store const API_URL = import.meta.env.VITE_API_URL || "http://localhost:8000"; export const apiClient: AxiosInstance = axios.create({ baseURL: API_URL, timeout: 30000, headers: { "Content-Type": "application/json", }, }); // Request interceptor - Add auth token apiClient.interceptors.request.use( (config) => { const { tokens } = useAuth.getState(); if (tokens?.accessToken) { config.headers.Authorization = `Bearer ${tokens.accessToken}`; } return config; }, (error) => Promise.reject(error), ); // Response interceptor - Handle token refresh apiClient.interceptors.response.use( (response) => response, async (error: AxiosError) => { const originalRequest = error.config; // If 401 and not already retried, attempt token refresh if (error.response?.status === 401 && !originalRequest?._retry) { originalRequest._retry = true; try { const { refreshToken } = useAuth.getState(); if (!refreshToken) { throw new Error("No refresh token"); } // Refresh token await useAuth.getState().refreshToken(); // Retry original request return apiClient(originalRequest); } catch (refreshError) { // Refresh failed, logout user useAuth.getState().logout(); window.location.href = "/login"; return Promise.reject(refreshError); } } return Promise.reject(error); }, ); // Error handler helper export function handleApiError(error: unknown): string { if (axios.isAxiosError(error)) { if (error.response?.data?.message) { return error.response.data.message; } if (error.response?.status === 404) { return "Resource not found"; } if (error.response?.status === 500) { return "Server error. Please try again later."; } if (error.code === "ECONNABORTED") { return "Request timeout. Please try again."; } } return "An unexpected error occurred"; }
4. State Management
Global State: Zustand
Why Zustand?
- Simple API, minimal boilerplate
- No providers needed
- TypeScript-friendly
- Small bundle size (~1kb)
- React hooks-based
- Built-in middleware (persist, devtools, immer)
Auth Store Example
// packages/stores/src/authStore.ts import { create } from "zustand"; import { persist, devtools } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; import type { User, AuthTokens } from "@voiceassist/types"; interface AuthState { user: User | null; tokens: AuthTokens | null; isAuthenticated: boolean; isLoading: boolean; } interface AuthActions { login: (email: string, password: string) => Promise<void>; logout: () => Promise<void>; refreshToken: () => Promise<void>; setUser: (user: User) => void; } type AuthStore = AuthState & AuthActions; export const useAuth = create<AuthStore>()( devtools( persist( immer((set, get) => ({ // State user: null, tokens: null, isAuthenticated: false, isLoading: false, // Actions login: async (email, password) => { set({ isLoading: true }); try { const response = await authApi.login({ email, password }); set({ user: response.user, tokens: response.tokens, isAuthenticated: true, isLoading: false, }); } catch (error) { set({ isLoading: false }); throw error; } }, logout: async () => { const { tokens } = get(); if (tokens) { await authApi.logout(tokens.refreshToken); } set({ user: null, tokens: null, isAuthenticated: false, }); }, refreshToken: async () => { const { tokens } = get(); if (!tokens?.refreshToken) { throw new Error("No refresh token"); } const response = await authApi.refresh(tokens.refreshToken); set({ tokens: response.tokens }); }, setUser: (user) => { set({ user }); }, })), { name: "voiceassist-auth", partialize: (state) => ({ user: state.user, tokens: state.tokens, isAuthenticated: state.isAuthenticated, }), }, ), { name: "AuthStore" }, ), );
Chat Store Example
// apps/web-app/src/stores/chatStore.ts import { create } from "zustand"; import { devtools } from "zustand/middleware"; import { immer } from "zustand/middleware/immer"; import type { ChatMessage, Conversation } from "@voiceassist/types"; interface ChatState { conversations: Conversation[]; currentConversationId: string | null; messages: Record<string, ChatMessage[]>; // conversationId -> messages isStreaming: boolean; isConnected: boolean; } interface ChatActions { setConversations: (conversations: Conversation[]) => void; setCurrentConversation: (id: string) => void; addMessage: (conversationId: string, message: ChatMessage) => void; updateMessage: (conversationId: string, messageId: string, updates: Partial<ChatMessage>) => void; setStreaming: (isStreaming: boolean) => void; setConnected: (isConnected: boolean) => void; clearMessages: (conversationId: string) => void; } type ChatStore = ChatState & ChatActions; export const useChat = create<ChatStore>()( devtools( immer((set) => ({ // State conversations: [], currentConversationId: null, messages: {}, isStreaming: false, isConnected: false, // Actions setConversations: (conversations) => { set({ conversations }); }, setCurrentConversation: (id) => { set({ currentConversationId: id }); }, addMessage: (conversationId, message) => { set((state) => { if (!state.messages[conversationId]) { state.messages[conversationId] = []; } state.messages[conversationId].push(message); }); }, updateMessage: (conversationId, messageId, updates) => { set((state) => { const messages = state.messages[conversationId]; if (!messages) return; const index = messages.findIndex((m) => m.id === messageId); if (index !== -1) { Object.assign(messages[index], updates); } }); }, setStreaming: (isStreaming) => { set({ isStreaming }); }, setConnected: (isConnected) => { set({ isConnected }); }, clearMessages: (conversationId) => { set((state) => { delete state.messages[conversationId]; }); }, })), { name: "ChatStore" }, ), );
5. API Communication
REST API Communication
Base URL Configuration:
// Environment variables VITE_API_URL=https://voice.asimo.io VITE_WS_URL=wss://voice.asimo.io
API Modules:
// packages/api-client/src/chat.ts import { apiClient, handleApiError } from "./client"; import type { Conversation, ChatMessage } from "@voiceassist/types"; export const chatApi = { /** * Get all conversations for current user */ getConversations: async (): Promise<Conversation[]> => { try { const response = await apiClient.get<Conversation[]>("/api/conversations"); return response.data; } catch (error) { throw new Error(handleApiError(error)); } }, /** * Get specific conversation */ getConversation: async (id: string): Promise<Conversation> => { const response = await apiClient.get<Conversation>(`/api/conversations/${id}`); return response.data; }, /** * Get messages for a conversation */ getMessages: async (conversationId: string): Promise<ChatMessage[]> => { const response = await apiClient.get<ChatMessage[]>(`/api/conversations/${conversationId}/messages`); return response.data; }, /** * Delete conversation */ deleteConversation: async (id: string): Promise<void> => { await apiClient.delete(`/api/conversations/${id}`); }, /** * Upload file */ uploadFile: async (file: File): Promise<{ id: string; url: string }> => { const formData = new FormData(); formData.append("file", file); const response = await apiClient.post<{ id: string; url: string }>("/api/files/upload", formData, { headers: { "Content-Type": "multipart/form-data", }, onUploadProgress: (progressEvent) => { const percentCompleted = Math.round((progressEvent.loaded * 100) / (progressEvent.total || 1)); console.log(`Upload progress: ${percentCompleted}%`); }, }); return response.data; }, };
6. Real-time Communication
WebSocket Manager
// packages/api-client/src/websocket.ts import type { ClientEvent, ServerEvent } from "@voiceassist/types"; export class WebSocketManager { private ws: WebSocket | null = null; private reconnectAttempts = 0; private maxReconnectAttempts = 5; private reconnectDelay = 2000; private pingInterval: NodeJS.Timeout | null = null; private eventHandlers: Map<string, Set<(event: ServerEvent) => void>> = new Map(); constructor(private url: string) {} connect(): Promise<void> { return new Promise((resolve, reject) => { try { this.ws = new WebSocket(this.url); this.ws.onopen = () => { console.log("WebSocket connected"); this.reconnectAttempts = 0; this.startPing(); resolve(); }; this.ws.onmessage = (event) => { try { const data: ServerEvent = JSON.parse(event.data); this.emit(data.type, data); } catch (error) { console.error("Failed to parse WebSocket message:", error); } }; this.ws.onerror = (error) => { console.error("WebSocket error:", error); reject(error); }; this.ws.onclose = () => { console.log("WebSocket closed"); this.stopPing(); this.attemptReconnect(); }; } catch (error) { reject(error); } }); } disconnect(): void { this.stopPing(); if (this.ws) { this.ws.close(); this.ws = null; } } send(event: ClientEvent): void { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(event)); } else { console.warn("WebSocket not connected, cannot send:", event); } } on(eventType: string, handler: (event: ServerEvent) => void): () => void { if (!this.eventHandlers.has(eventType)) { this.eventHandlers.set(eventType, new Set()); } this.eventHandlers.get(eventType)!.add(handler); // Return unsubscribe function return () => { this.eventHandlers.get(eventType)?.delete(handler); }; } private emit(eventType: string, event: ServerEvent): void { const handlers = this.eventHandlers.get(eventType); if (handlers) { handlers.forEach((handler) => { try { handler(event); } catch (error) { console.error(`Error in event handler for ${eventType}:`, error); } }); } } private startPing(): void { this.pingInterval = setInterval(() => { this.send({ type: "ping" } as any); }, 30000); // Ping every 30 seconds } private stopPing(): void { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } } private attemptReconnect(): void { if (this.reconnectAttempts >= this.maxReconnectAttempts) { console.error("Max reconnection attempts reached"); return; } this.reconnectAttempts++; const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1); console.log(`Attempting reconnect in ${delay}ms (attempt ${this.reconnectAttempts})`); setTimeout(() => { this.connect().catch((error) => { console.error("Reconnection failed:", error); }); }, delay); } get isConnected(): boolean { return this.ws?.readyState === WebSocket.OPEN; } }
Due to length, I'll create the remaining sections in focused documents. This Technical Architecture document continues with sections on Security, Testing, Build & Deployment, etc. Should I continue with the complete Technical Architecture, or move to other documents?
13. Internationalization (i18n)
i18n Foundation
Current Implementation:
- β
Locale configuration module:
packages/config/i18n.ts - β Supported locales: English (default), Arabic, Spanish, French
- β RTL support scaffolding for Arabic
- β Locale metadata: date/time formats, text direction, support status
Locale Configuration:
import { SupportedLocale, DEFAULT_LOCALE, detectBrowserLocale, getLocaleMetadata } from "@voiceassist/config/i18n"; // Detect user's locale const userLocale = detectBrowserLocale(); // Falls back to 'en' // Get locale metadata const metadata = getLocaleMetadata(SupportedLocale.Arabic); // { code: 'ar', direction: 'rtl', isRTL: true, ... }
Implementation Roadmap:
- Phase 1 (Current): Locale scaffolding and configuration
- Phase 2 (Planned): Integration with i18next or react-intl
- Phase 3 (Planned): Translation keys and message catalogs
- Phase 4 (Planned): Medical content localization (Arabic priority)
- Phase 5 (Planned): Dynamic locale switching and persistence
Extension Points:
packages/config/i18n.ts- Core configuration- Future:
packages/i18n/- Translation library integration - Future:
apps/*/locales/- Translation files per application
See WEB_APP_FEATURE_SPECS.md for detailed i18n requirements.
Current Status:
- β MASTER_IMPLEMENTATION_PLAN.md (20,000+ words)
- β³ WEB_APP_FEATURE_SPECS.md (Started - 3 features detailed)
- β³ TECHNICAL_ARCHITECTURE.md (In progress - Core sections complete)
- β i18n foundation added (2025-11-22)
Next up:
- Complete Technical Architecture
- Create Admin Panel specs
- Create Integration Guide
- Create Development Workflow
- Update existing README files