You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
27 KiB
27 KiB
Know Foolery - Technical Architecture Documentation
Architecture Overview
Know Foolery follows a microservices architecture with clear separation between frontend presentation, backend services, and data persistence layers. The system is designed for cross-platform compatibility, scalability, and maintainability.
System Architecture Diagram
┌────────────────────────────────────────────────────────────────────────────┐
│ Client Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Web App │ │ Mobile iOS │ │ Mobile Androi│ │ Desktop ctr│ │
│ │ (React) │ │(React Native)│ │(React Native)│ │ (Wails) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
│
HTTPS/WSS
│
┌─────────────────────────────────────────────────────────────────────────────┐
│ API Gateway Layer │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ NGINX + API Gateway Service (Go) │ │
│ │ • Authentication & Authorization │ │
│ │ • Rate Limiting & CORS │ │
│ │ • Request Routing & Load Balancing │ │
│ │ • Security Headers & Input Validation │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
gRPC/HTTP
│
┌─────────────────────────────────────────────────────────────────────────────┐
│ Microservices Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Game │ │ Question │ │ User │ │ Leaderboard │ │
│ │ Service │ │ Service │ │ Service │ │ Service │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Session │ │ Admin │ │
│ │ Service │ │ Service │ │
│ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
Database Connections
│
┌─────────────────────────────────────────────────────────────────────────────┐
│ Data & External Layer │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ PostgreSQL │ │ Redis │ │ Zitadel │ │ Observabi- │ │
│ │ Primary │ │ Cache & │ │ OAuth │ │ lity │ │
│ │ Database │ │ Sessions │ │ Provider │ │ Stack │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Technology Stack
Frontend Technologies
Web Application:
Framework: React 18.2+
Language: TypeScript 5.0+
Build Tool: Vite 4.0+
UI Library: Gluestack UI
Styling: NativeWind/Tailwind CSS 3.0+
State Management: React Context + useReducer
Testing: Jest + React Testing Library + Playwright
Mobile Applications:
Framework: React Native 0.81+
Language: TypeScript 5.9+
UI Library: Gluestack UI (Native)
Navigation: React Navigation 6+
State Management: React Context + useReducer
Testing: Jest + React Native Testing Library
Desktop Application:
Framework: Wails 2.10+
Template: wails-vite-react-ts-tailwind-template
Backend Technologies
Microservices:
Language: Go 1.25+
HTTP Framework: Fiber 3.0+
gRPC: Protocol Buffers + gRPC-Go
Database ORM: Ent (Facebook)
Authentication: JWT + Zitadel Integration
Testing: Go testing + testcontainers
API Gateway:
Reverse Proxy: NGINX
Gateway Service: Go + Fiber
Load Balancing: Round-robin + Health checks
Rate Limiting: Redis-based sliding window
Data Technologies
Primary Database:
Engine: PostgreSQL 15+
Connection Pooling: pgxpool
Migrations: Ent migrations
Backup: pg_dump + Object Storage
Cache Layer:
Engine: Redis 7+
Use Cases: Sessions, Rate limiting, Cache
Clustering: Redis Cluster (Production)
Persistence: RDB + AOF
Authentication:
Provider: Zitadel (Self-hosted)
Protocol: OAuth 2.0 + OpenID Connect
Tokens: JWT with RS256 signing
MFA: TOTP + WebAuthn
Infrastructure Technologies
Containerization:
Runtime: Docker 24+
Orchestration: Kubernetes 1.28+
Registry: Private Docker Registry
Observability:
Metrics: Prometheus + Node Exporter
Visualization: Grafana + Custom Dashboards
Tracing: Jaeger + OpenTelemetry
Logging: Structured logging + Loki
CI/CD:
Platform: GitHub Actions
Testing: Automated test suites
Security: SAST/DAST scanning
Deployment: GitOps with ArgoCD
Microservices Design
Service Boundaries
1. Game Service
// Responsibilities
type GameService struct {
// Core game logic and session management
SessionManager *SessionManager
ScoreCalculator *ScoreCalculator
AttemptTracker *AttemptTracker
TimerManager *TimerManager
}
// API Endpoints
POST /api/v1/game/start // Start new game session
GET /api/v1/game/session/{id} // Get session details
POST /api/v1/game/answer // Submit answer
POST /api/v1/game/hint // Request hint
POST /api/v1/game/end // End session
GET /api/v1/game/status/{id} // Get session status
2. Question Service
// Responsibilities
type QuestionService struct {
QuestionRepo *QuestionRepository
ThemeManager *ThemeManager
Randomizer *QuestionRandomizer
Validator *AnswerValidator
}
// API Endpoints
GET /api/v1/questions/random // Get random question
GET /api/v1/questions/{id} // Get specific question
GET /api/v1/questions/themes // List available themes
POST /api/v1/questions // Create question (admin)
PUT /api/v1/questions/{id} // Update question (admin)
DELETE /api/v1/questions/{id} // Delete question (admin)
3. User Service
// Responsibilities
type UserService struct {
UserRepo *UserRepository
ProfileManager *ProfileManager
SessionTracker *SessionTracker
}
// API Endpoints
POST /api/v1/users/register // Register new player
GET /api/v1/users/profile // Get user profile
PUT /api/v1/users/profile // Update profile
GET /api/v1/users/sessions // Get session history
DELETE /api/v1/users/profile // Delete account (GDPR)
4. Leaderboard Service
// Responsibilities
type LeaderboardService struct {
ScoreAggregator *ScoreAggregator
RankingEngine *RankingEngine
StatsCalculator *StatsCalculator
}
// API Endpoints
GET /api/v1/leaderboard/top10 // Get top 10 scores
GET /api/v1/leaderboard/stats // Get game statistics
GET /api/v1/leaderboard/player/{id} // Get player ranking
POST /api/v1/leaderboard/update // Update scores (internal)
5. Session Service
// Responsibilities
type SessionService struct {
SessionStore *SessionStore
TimerManager *TimerManager
StateManager *StateManager
}
// API Endpoints
POST /api/v1/sessions // Create session
GET /api/v1/sessions/{id} // Get session
PUT /api/v1/sessions/{id} // Update session
DELETE /api/v1/sessions/{id} // End session
GET /api/v1/sessions/{id}/timer // Get timer status
6. Admin Service
// Responsibilities
type AdminService struct {
AuthManager *AuthManager
QuestionMgmt *QuestionManagement
UserMgmt *UserManagement
AuditLogger *AuditLogger
}
// API Endpoints
POST /api/v1/admin/auth // Admin authentication
GET /api/v1/admin/dashboard // Dashboard data
GET /api/v1/admin/questions // List all questions
POST /api/v1/admin/questions/bulk // Bulk question import
GET /api/v1/admin/users // User management
GET /api/v1/admin/audit // Audit logs
Inter-Service Communication
Service Mesh Architecture
Communication Patterns:
Synchronous: gRPC for real-time operations
Asynchronous: Event-driven via message queues (future)
Service Discovery:
Registry: Kubernetes DNS
Health Checks: HTTP /health endpoints
Load Balancing: Round-robin with health awareness
Circuit Breaker:
Pattern: Hystrix-style circuit breakers
Fallback: Graceful degradation
Timeout: Context-based timeouts
gRPC Service Definitions
// game_service.proto
service GameService {
rpc StartGame(StartGameRequest) returns (StartGameResponse);
rpc SubmitAnswer(SubmitAnswerRequest) returns (SubmitAnswerResponse);
rpc GetSession(GetSessionRequest) returns (GetSessionResponse);
rpc EndGame(EndGameRequest) returns (EndGameResponse);
}
// question_service.proto
service QuestionService {
rpc GetRandomQuestion(RandomQuestionRequest) returns (Question);
rpc ValidateAnswer(ValidateAnswerRequest) returns (ValidationResponse);
rpc GetQuestionHint(HintRequest) returns (HintResponse);
}
// leaderboard_service.proto
service LeaderboardService {
rpc UpdateScore(UpdateScoreRequest) returns (UpdateScoreResponse);
rpc GetTopScores(TopScoresRequest) returns (TopScoresResponse);
rpc GetPlayerRank(PlayerRankRequest) returns (PlayerRankResponse);
}
Database Architecture
Data Model Design
Core Entities (Ent Schemas)
// Question Entity
type Question struct {
ent.Schema
}
func (Question) Fields() []ent.Field {
return []ent.Field{
field.String("theme").NotEmpty(),
field.Text("text").NotEmpty(),
field.String("answer").NotEmpty(),
field.Text("hint").Optional(),
field.Enum("difficulty").Values("easy", "medium", "hard").Default("medium"),
field.Bool("is_active").Default(true),
field.Time("created_at").Default(time.Now),
field.Time("updated_at").Default(time.Now).UpdateDefault(time.Now),
}
}
func (Question) Indexes() []ent.Index {
return []ent.Index{
index.Fields("theme"),
index.Fields("difficulty"),
index.Fields("is_active"),
index.Fields("created_at"),
}
}
// GameSession Entity
type GameSession struct {
ent.Schema
}
func (GameSession) Fields() []ent.Field {
return []ent.Field{
field.String("player_name").NotEmpty(),
field.Int("total_score").Default(0),
field.Int("questions_asked").Default(0),
field.Int("questions_correct").Default(0),
field.Int("hints_used").Default(0),
field.Time("start_time").Default(time.Now),
field.Time("end_time").Optional().Nillable(),
field.Enum("status").Values("active", "completed", "timeout", "abandoned").Default("active"),
field.String("current_question_id").Optional(),
field.Int("current_attempts").Default(0),
}
}
func (GameSession) Edges() []ent.Edge {
return []ent.Edge{
edge.To("attempts", QuestionAttempt.Type),
edge.To("current_question", Question.Type).Unique().Field("current_question_id"),
}
}
// QuestionAttempt Entity
type QuestionAttempt struct {
ent.Schema
}
func (QuestionAttempt) Fields() []ent.Field {
return []ent.Field{
field.String("session_id"),
field.String("question_id"),
field.Int("attempt_number"),
field.String("submitted_answer"),
field.Bool("is_correct"),
field.Bool("used_hint"),
field.Int("points_awarded"),
field.Time("submitted_at").Default(time.Now),
field.Duration("time_taken"),
}
}
func (QuestionAttempt) Edges() []ent.Edge {
return []ent.Edge{
edge.From("session", GameSession.Type).Ref("attempts").Unique().Field("session_id"),
edge.To("question", Question.Type).Unique().Field("question_id"),
}
}
Database Performance Optimization
-- Indexes for optimal query performance
CREATE INDEX idx_questions_theme_active ON questions(theme, is_active);
CREATE INDEX idx_sessions_status_start_time ON game_sessions(status, start_time);
CREATE INDEX idx_attempts_session_question ON question_attempts(session_id, question_id);
CREATE INDEX idx_leaderboard_score_time ON game_sessions(total_score DESC, end_time ASC) WHERE status = 'completed';
-- Partitioning for large datasets (future scaling)
CREATE TABLE game_sessions_y2024m01 PARTITION OF game_sessions
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
Database Connection Management
// Database configuration
type DatabaseConfig struct {
Primary DatabaseInstance
Replicas []DatabaseInstance
Pool PoolConfig
}
type DatabaseInstance struct {
Host string
Port int
Database string
Username string
Password string
SSLMode string
}
type PoolConfig struct {
MaxOpenConns int // 25
MaxIdleConns int // 5
ConnMaxLifetime time.Duration // 30 minutes
ConnMaxIdleTime time.Duration // 15 minutes
}
// Connection pooling with read/write splitting
func NewDatabaseClient(config DatabaseConfig) (*ent.Client, error) {
primaryDSN := formatDSN(config.Primary)
// Primary database for writes
primaryDB, err := sql.Open("postgres", primaryDSN)
if err != nil {
return nil, err
}
configurePools(primaryDB, config.Pool)
// Create Ent client with instrumented driver
drv := entsql.OpenDB("postgres", primaryDB)
return ent.NewClient(ent.Driver(drv)), nil
}
Security Architecture
Authentication Flow
sequenceDiagram
participant C as Client
participant G as API Gateway
participant Z as Zitadel
participant S as Service
C->>G: Request with credentials
G->>Z: Validate credentials
Z->>G: Return JWT token
G->>C: Return token + user info
C->>G: API request with JWT
G->>G: Validate JWT signature
G->>S: Forward request with user context
S->>G: Return response
G->>C: Return response
Authorization Strategy
// Role-based access control
type Role string
const (
RolePlayer Role = "player"
RoleAdmin Role = "admin"
)
type Permissions struct {
CanPlayGame bool
CanViewScores bool
CanManageQuestions bool
CanViewAuditLogs bool
CanManageUsers bool
}
var RolePermissions = map[Role]Permissions{
RolePlayer: {
CanPlayGame: true,
CanViewScores: true,
},
RoleAdmin: {
CanPlayGame: true,
CanViewScores: true,
CanManageQuestions: true,
CanViewAuditLogs: true,
CanManageUsers: true,
},
}
// JWT middleware with role checking
func AuthMiddleware() fiber.Handler {
return func(c *fiber.Ctx) error {
token := extractToken(c.Get("Authorization"))
claims, err := validateJWT(token)
if err != nil {
return c.Status(401).JSON(fiber.Map{"error": "Invalid token"})
}
c.Locals("user_id", claims.Subject)
c.Locals("user_role", claims.Role)
c.Locals("permissions", RolePermissions[Role(claims.Role)])
return c.Next()
}
}
func RequirePermission(permission func(Permissions) bool) fiber.Handler {
return func(c *fiber.Ctx) error {
perms := c.Locals("permissions").(Permissions)
if !permission(perms) {
return c.Status(403).JSON(fiber.Map{"error": "Insufficient permissions"})
}
return c.Next()
}
}
Input Validation & Security
// Comprehensive input validation
type InputValidator struct {
maxAnswerLength int
maxNameLength int
allowedChars *regexp.Regexp
}
func NewInputValidator() *InputValidator {
return &InputValidator{
maxAnswerLength: 500,
maxNameLength: 50,
allowedChars: regexp.MustCompile(`^[a-zA-Z0-9\s\-_.]+$`),
}
}
func (v *InputValidator) ValidateAnswer(answer string) error {
if len(answer) == 0 {
return errors.New("answer cannot be empty")
}
if len(answer) > v.maxAnswerLength {
return errors.New("answer too long")
}
// Sanitize HTML and potential XSS
clean := html.EscapeString(strings.TrimSpace(answer))
if clean != answer {
return errors.New("invalid characters in answer")
}
return nil
}
func (v *InputValidator) ValidatePlayerName(name string) error {
if len(name) < 2 || len(name) > v.maxNameLength {
return errors.New("name must be 2-50 characters")
}
if !v.allowedChars.MatchString(name) {
return errors.New("name contains invalid characters")
}
return nil
}
// Rate limiting implementation
type RateLimiter struct {
redis *redis.Client
limits map[string]RateLimit
}
type RateLimit struct {
Requests int // Number of requests
Window time.Duration // Time window
}
func (rl *RateLimiter) Allow(key string, limit RateLimit) bool {
current, err := rl.redis.Incr(key).Result()
if err != nil {
return false // Fail closed
}
if current == 1 {
rl.redis.Expire(key, limit.Window)
}
return current <= int64(limit.Requests)
}
// Usage in middleware
func RateLimitMiddleware() fiber.Handler {
limiter := NewRateLimiter()
return func(c *fiber.Ctx) error {
userID := c.Locals("user_id").(string)
clientIP := c.IP()
// Per-user rate limiting
userKey := fmt.Sprintf("rate_limit:user:%s", userID)
if !limiter.Allow(userKey, RateLimit{Requests: 60, Window: time.Minute}) {
return c.Status(429).JSON(fiber.Map{"error": "Rate limit exceeded"})
}
// Per-IP rate limiting
ipKey := fmt.Sprintf("rate_limit:ip:%s", clientIP)
if !limiter.Allow(ipKey, RateLimit{Requests: 100, Window: time.Minute}) {
return c.Status(429).JSON(fiber.Map{"error": "Rate limit exceeded"})
}
return c.Next()
}
}
Cross-Platform Frontend Architecture
Gluestack UI Integration
// Shared component library structure
// packages/ui-components/src/index.ts
export { GameCard } from './GameCard'
export { Leaderboard } from './Leaderboard'
export { Timer } from './Timer'
export { ScoreDisplay } from './ScoreDisplay'
export { AdminPanel } from './AdminPanel'
// Component example with Gluestack UI
// packages/ui-components/src/GameCard/GameCard.tsx
import {
Card,
VStack,
HStack,
Text,
Input,
Button,
Badge,
Progress,
Box
} from '@gluestack-ui/themed'
export interface GameCardProps {
question: string
theme: string
timeRemaining: number
attemptsLeft: number
currentScore: number
onSubmitAnswer: (answer: string) => void
onRequestHint: () => void
isLoading?: boolean
}
export const GameCard: React.FC<GameCardProps> = ({
question,
theme,
timeRemaining,
attemptsLeft,
currentScore,
onSubmitAnswer,
onRequestHint,
isLoading = false
}) => {
const [answer, setAnswer] = useState('')
const handleSubmit = () => {
if (answer.trim()) {
onSubmitAnswer(answer.trim())
setAnswer('')
}
}
return (
<Card size="lg" variant="elevated" m="$4">
<VStack space="md" p="$4">
{/* Header with theme and timer */}
<HStack justifyContent="space-between" alignItems="center">
<Badge size="sm" variant="solid" action="info">
<Text color="$white" fontSize="$sm" fontWeight="$semibold">
{theme}
</Text>
</Badge>
<HStack space="sm" alignItems="center">
<Text fontSize="$sm" color="$textLight600">
⏱️ {Math.floor(timeRemaining / 60)}:{(timeRemaining % 60).toString().padStart(2, '0')}
</Text>
</HStack>
</HStack>
{/* Progress indicator */}
<Progress value={((30 * 60 - timeRemaining) / (30 * 60)) * 100} size="sm">
<ProgressFilledTrack />
</Progress>
{/* Question */}
<Box>
<Text fontSize="$lg" fontWeight="$semibold" lineHeight="$xl">
{question}
</Text>
</Box>
{/* Answer input */}
<Input size="lg" isDisabled={isLoading}>
<InputField
placeholder="Enter your answer..."
value={answer}
onChangeText={setAnswer}
autoCapitalize="none"
autoCorrect={false}
/>
</Input>
{/* Game stats */}
<HStack justifyContent="space-between" alignItems="center">
<Text fontSize="$sm" color="$textLight600">
Attempts left: {attemptsLeft}/3
</Text>
<Text fontSize="$sm" color="$textLight600">
Score: {currentScore} points
</Text>
</HStack>
{/* Action buttons */}
<HStack space="sm">
<Button
size="lg"
variant="solid"
action="primary"
flex={1}
onPress={handleSubmit}
isDisabled={!answer.trim() || isLoading}
>
<ButtonText>Submit Answer</ButtonText>
</Button>
<Button
size="lg"
variant="outline"
action="secondary"
onPress={onRequestHint}
isDisabled={isLoading}
>
<ButtonText>💡 Hint</ButtonText>
</Button>
</HStack>
</VStack>
</Card>
)
}
Platform-Specific Adaptations
// Platform detection and adaptation
// packages/shared-logic/src/platform.ts
export interface PlatformCapabilities {
hasTouch: boolean
hasKeyboard: boolean
hasCamera: boolean
canVibrate: boolean
supportsNotifications: boolean
isOfflineCapable: boolean
}
export const getPlatformCapabilities = (): PlatformCapabilities => {
// Web platform
if (typeof window !== 'undefined') {
return {
hasTouch: 'ontouchstart' in window,
hasKeyboard: true,
hasCamera: !!navigator.mediaDevices?.getUserMedia,
canVibrate: !!navigator.vibrate,
supportsNotifications: 'Notification' in window,
isOfflineCapable: 'serviceWorker' in navigator
}
}
// React Native platform
if (typeof require !== 'undefined') {
try {
const { Platform } = require('react-native')
return {
hasTouch: true,
hasKeyboard: Platform.OS === 'ios' || Platform.OS === 'android',
hasCamera: true,
canVibrate: true,
supportsNotifications: true,
isOfflineCapable: true
}
} catch {
// Fallback for other environments
}
}
// Default capabilities
return {
hasTouch: false,
hasKeyboard: true,
hasCamera: false,
canVibrate: false,
supportsNotifications: false,
isOfflineCapable: false
}
}
// Responsive design utilities
export const useResponsiveValue = <T>(values: {
mobile: T
tablet: T
desktop: T
}) => {
const [screenSize, setScreenSize] = useState<'mobile' | 'tablet' | 'desktop'>('desktop')
useEffect(() => {
const updateScreenSize = () => {
const width = window.innerWidth
if (width < 768) {
setScreenSize('mobile')
} else if (width < 1024) {
setScreenSize('tablet')
} else {
setScreenSize('desktop')
}
}
updateScreenSize()
window.addEventListener('resize', updateScreenSize)
return () => window.removeEventListener('resize', updateScreenSize)
}, [])
return values[screenSize]
}
This technical architecture provides a solid foundation for building a scalable, secure, and maintainable quiz game platform with cross-platform capabilities.