# 1.1 Development Environment Setup - Detailed Implementation Plan ## Overview | Attribute | Value | |-----------|-------| | **Priority** | CRITICAL | | **Duration** | 2-3 days | | **Dependencies** | None (first step) | | **Team Size** | 1 developer | ## Objectives 1. Establish a reproducible development environment for all team members 2. Configure Go multi-module monorepo structure 3. Set up frontend workspace with proper tooling 4. Create containerized local infrastructure (PostgreSQL, Redis, Zitadel) 5. Implement consistent linting and formatting standards 6. Document onboarding process --- ## Task Breakdown ### Task 1: Initialize Go Workspace (go.work) **Duration:** 2-3 hours #### 1.1.1 Create Root go.work File Create the Go workspace file at repository root to manage multiple modules. **File:** `/go.work` ```go go 1.25 use ( ./backend/services/game-session-service ./backend/services/question-bank-service ./backend/services/user-service ./backend/services/leaderboard-service ./backend/services/admin-service ./backend/services/gateway-service ./backend/shared ) ``` #### 1.1.2 Initialize Each Service Module For each service, create the go.mod file: **Directory Structure to Create:** ``` backend/ ├── go.work ├── services/ │ ├── game-session-service/ │ │ ├── go.mod │ │ ├── go.sum │ │ └── cmd/ │ │ └── main.go │ ├── question-bank-service/ │ │ ├── go.mod │ │ ├── go.sum │ │ └── cmd/ │ │ └── main.go │ ├── user-service/ │ │ ├── go.mod │ │ ├── go.sum │ │ └── cmd/ │ │ └── main.go │ ├── leaderboard-service/ │ │ ├── go.mod │ │ ├── go.sum │ │ └── cmd/ │ │ └── main.go │ ├── admin-service/ │ │ ├── go.mod │ │ ├── go.sum │ │ └── cmd/ │ │ └── main.go │ └── gateway-service/ │ ├── go.mod │ ├── go.sum │ └── cmd/ │ └── main.go └── shared/ ├── go.mod ├── go.sum └── .gitkeep ``` **Example go.mod for game-session-service:** ```go module knowfoolery/backend/services/game-session-service go 1.25 require ( github.com/gofiber/fiber/v3 v3.0.0 knowfoolery/backend/shared v0.0.0 ) replace knowfoolery/backend/shared => ../../shared ``` **Example go.mod for shared package:** ```go module knowfoolery/backend/shared go 1.25 require ( github.com/rs/zerolog v1.31.0 github.com/go-playground/validator/v10 v10.16.0 github.com/prometheus/client_golang v1.17.0 go.opentelemetry.io/otel v1.21.0 ) ``` #### 1.1.3 Create Stub Main Files **File:** `backend/services/game-session-service/cmd/main.go` ```go package main import ( "log" "github.com/gofiber/fiber/v3" ) func main() { app := fiber.New(fiber.Config{ AppName: "Know Foolery - Game Session Service", }) app.Get("/health", func(c fiber.Ctx) error { return c.JSON(fiber.Map{"status": "healthy", "service": "game-session"}) }) log.Fatal(app.Listen(":8080")) } ``` **Repeat for each service with appropriate port:** | Service | Port | |---------|------| | game-session-service | 8080 | | question-bank-service | 8081 | | user-service | 8082 | | leaderboard-service | 8083 | | admin-service | 8085 | | gateway-service | 8086 | #### Verification Steps: ```bash cd backend go work sync go build ./... ``` --- ### Task 2: Set Up Shared Packages Structure **Duration:** 2-3 hours #### 1.2.1 Create Shared Package Directory Structure ``` backend/shared/ ├── go.mod ├── go.sum ├── domain/ │ ├── errors/ │ │ ├── errors.go # Base error types │ │ └── codes.go # Error code constants │ ├── events/ │ │ ├── event.go # Event interface │ │ └── contracts.go # Event type definitions │ ├── types/ │ │ ├── id.go # ID type wrappers │ │ ├── pagination.go # Pagination types │ │ └── enums.go # Common enums │ └── valueobjects/ │ ├── player_name.go # PlayerName value object │ └── score.go # Score value object └── infra/ ├── auth/ │ ├── zitadel/ │ │ ├── client.go # Zitadel client │ │ └── middleware.go # JWT middleware │ └── rbac/ │ └── roles.go # Role definitions ├── database/ │ ├── postgres/ │ │ └── client.go # PostgreSQL connection factory │ └── redis/ │ └── client.go # Redis connection factory ├── observability/ │ ├── logging/ │ │ └── logger.go # Zerolog setup │ ├── tracing/ │ │ └── tracer.go # OpenTelemetry setup │ └── metrics/ │ └── prometheus.go # Prometheus registration ├── security/ │ └── sanitize.go # Input sanitization └── utils/ ├── httputil/ │ ├── errors.go # HTTP error responses │ ├── response.go # Standard response format │ └── pagination.go # Pagination helpers └── validation/ └── validator.go # Validation helpers ``` #### 1.2.2 Create Placeholder Files Create minimal placeholder implementations for each file to establish the structure. **File:** `backend/shared/domain/errors/errors.go` ```go package errors import "fmt" // DomainError represents a domain-level error type DomainError struct { Code string Message string Err error } func (e *DomainError) Error() string { if e.Err != nil { return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err) } return fmt.Sprintf("[%s] %s", e.Code, e.Message) } func (e *DomainError) Unwrap() error { return e.Err } // Common domain errors var ( ErrNotFound = &DomainError{Code: "NOT_FOUND", Message: "Resource not found"} ErrValidation = &DomainError{Code: "VALIDATION", Message: "Validation failed"} ErrUnauthorized = &DomainError{Code: "UNAUTHORIZED", Message: "Unauthorized access"} ErrConflict = &DomainError{Code: "CONFLICT", Message: "Resource conflict"} ErrInternalServer = &DomainError{Code: "INTERNAL", Message: "Internal server error"} ) ``` **File:** `backend/shared/infra/observability/logging/logger.go` ```go package logging import ( "os" "time" "github.com/rs/zerolog" ) // NewLogger creates a new zerolog logger func NewLogger(serviceName, environment string) zerolog.Logger { zerolog.TimeFieldFormat = time.RFC3339Nano var logger zerolog.Logger if environment == "development" { logger = zerolog.New(zerolog.ConsoleWriter{ Out: os.Stdout, TimeFormat: "15:04:05", }).With().Timestamp().Logger() } else { logger = zerolog.New(os.Stdout).With().Timestamp().Logger() } return logger.With(). Str("service", serviceName). Str("environment", environment). Logger() } ``` #### Verification Steps: ```bash cd backend/shared go mod tidy go build ./... ``` --- ### Task 3: Configure Linting (golangci-lint, ESLint) **Duration:** 2-3 hours #### 1.3.1 Backend Linting (golangci-lint) **File:** `backend/.golangci.yml` ```yaml run: timeout: 5m tests: true go: "1.25" linters: enable: - gofmt - goimports - govet - errcheck - staticcheck - unused - gosimple - ineffassign - typecheck - gosec - misspell - lll - unconvert - dupl - goconst - gocyclo - prealloc - bodyclose - noctx - exhaustive linters-settings: lll: line-length: 120 gocyclo: min-complexity: 15 dupl: threshold: 150 goconst: min-len: 3 min-occurrences: 3 misspell: locale: US gosec: excludes: - G104 # Unhandled errors (we use errcheck) issues: exclude-rules: - path: _test\.go linters: - gocyclo - errcheck - dupl - gosec - path: cmd/main\.go linters: - gocyclo max-issues-per-linter: 50 max-same-issues: 10 ``` #### 1.3.2 Frontend Linting (ESLint) **File:** `frontend/.eslintrc.json` ```json { "root": true, "env": { "browser": true, "es2022": true, "node": true }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:solid/recommended", "prettier" ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "plugins": [ "@typescript-eslint", "solid" ], "rules": { "@typescript-eslint/explicit-function-return-type": "warn", "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "@typescript-eslint/no-explicit-any": "warn", "prefer-const": "error", "no-var": "error", "object-shorthand": "error", "prefer-template": "error", "no-console": ["warn", { "allow": ["warn", "error"] }] }, "ignorePatterns": [ "dist", "node_modules", "*.config.js", "*.config.ts" ] } ``` **File:** `frontend/.prettierrc` ```json { "semi": false, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "printWidth": 100, "bracketSpacing": true } ``` #### 1.3.3 Editor Configuration **File:** `.editorconfig` (repository root) ```ini root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.go] indent_style = tab indent_size = 4 [*.md] trim_trailing_whitespace = false [Makefile] indent_style = tab ``` #### Verification Steps: ```bash # Backend cd backend golangci-lint run ./... # Frontend cd frontend npx eslint . --ext .ts,.tsx npx prettier --check . ``` --- ### Task 4: Create Docker Compose for Local Development **Duration:** 3-4 hours #### 1.4.1 Development Docker Compose **File:** `infrastructure/dev/docker-compose.yml` ```yaml version: "3.9" services: # PostgreSQL Database postgres: image: postgres:15-alpine container_name: knowfoolery-postgres environment: POSTGRES_USER: knowfoolery POSTGRES_PASSWORD: devpassword POSTGRES_DB: knowfoolery ports: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data - ./init-scripts:/docker-entrypoint-initdb.d healthcheck: test: ["CMD-SHELL", "pg_isready -U knowfoolery -d knowfoolery"] interval: 10s timeout: 5s retries: 5 networks: - knowfoolery-network # Redis Cache redis: image: redis:7-alpine container_name: knowfoolery-redis ports: - "6379:6379" volumes: - redis_data:/data command: redis-server --appendonly yes healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 networks: - knowfoolery-network # Zitadel Authentication (optional for initial setup) zitadel: image: ghcr.io/zitadel/zitadel:latest container_name: knowfoolery-zitadel command: start-from-init --masterkeyFromEnv --tlsMode disabled environment: ZITADEL_MASTERKEY: "MasterkeyNeedsToHave32Characters" ZITADEL_DATABASE_COCKROACH_HOST: crdb ZITADEL_EXTERNALSECURE: false ZITADEL_FIRSTINSTANCE_ORG_NAME: "KnowFoolery" ZITADEL_FIRSTINSTANCE_ORG_HUMAN_USERNAME: "admin" ZITADEL_FIRSTINSTANCE_ORG_HUMAN_PASSWORD: "AdminPassword123!" ports: - "8080:8080" depends_on: crdb: condition: service_healthy networks: - knowfoolery-network profiles: - auth # CockroachDB for Zitadel crdb: image: cockroachdb/cockroach:latest container_name: knowfoolery-crdb command: start-single-node --insecure --http-addr :9090 ports: - "26257:26257" - "9090:9090" volumes: - crdb_data:/cockroach/cockroach-data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9090/health?ready=1"] interval: 10s timeout: 5s retries: 10 networks: - knowfoolery-network profiles: - auth # Prometheus Metrics prometheus: image: prom/prometheus:latest container_name: knowfoolery-prometheus ports: - "9091:9090" volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml - prometheus_data:/prometheus command: - '--config.file=/etc/prometheus/prometheus.yml' - '--storage.tsdb.path=/prometheus' - '--web.enable-lifecycle' networks: - knowfoolery-network profiles: - observability # Grafana Dashboards grafana: image: grafana/grafana:latest container_name: knowfoolery-grafana ports: - "3001:3000" environment: GF_SECURITY_ADMIN_USER: admin GF_SECURITY_ADMIN_PASSWORD: admin GF_USERS_ALLOW_SIGN_UP: false volumes: - grafana_data:/var/lib/grafana - ./grafana/provisioning:/etc/grafana/provisioning depends_on: - prometheus networks: - knowfoolery-network profiles: - observability # Jaeger Tracing jaeger: image: jaegertracing/all-in-one:latest container_name: knowfoolery-jaeger ports: - "16686:16686" # UI - "14268:14268" # Collector HTTP - "6831:6831/udp" # Agent environment: COLLECTOR_ZIPKIN_HOST_PORT: ":9411" networks: - knowfoolery-network profiles: - observability volumes: postgres_data: redis_data: crdb_data: prometheus_data: grafana_data: networks: knowfoolery-network: driver: bridge ``` #### 1.4.2 Database Initialization Script **File:** `infrastructure/dev/init-scripts/01-create-databases.sql` ```sql -- Create separate databases for each service CREATE DATABASE IF NOT EXISTS game_sessions; CREATE DATABASE IF NOT EXISTS questions; CREATE DATABASE IF NOT EXISTS users; CREATE DATABASE IF NOT EXISTS leaderboards; CREATE DATABASE IF NOT EXISTS admin; -- Grant privileges GRANT ALL PRIVILEGES ON DATABASE game_sessions TO knowfoolery; GRANT ALL PRIVILEGES ON DATABASE questions TO knowfoolery; GRANT ALL PRIVILEGES ON DATABASE users TO knowfoolery; GRANT ALL PRIVILEGES ON DATABASE leaderboards TO knowfoolery; GRANT ALL PRIVILEGES ON DATABASE admin TO knowfoolery; ``` #### 1.4.3 Prometheus Configuration **File:** `infrastructure/dev/prometheus/prometheus.yml` ```yaml global: scrape_interval: 15s evaluation_interval: 15s scrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - job_name: 'game-session-service' static_configs: - targets: ['host.docker.internal:8080'] metrics_path: /metrics - job_name: 'question-bank-service' static_configs: - targets: ['host.docker.internal:8081'] metrics_path: /metrics - job_name: 'user-service' static_configs: - targets: ['host.docker.internal:8082'] metrics_path: /metrics - job_name: 'leaderboard-service' static_configs: - targets: ['host.docker.internal:8083'] metrics_path: /metrics - job_name: 'gateway-service' static_configs: - targets: ['host.docker.internal:8086'] metrics_path: /metrics ``` #### 1.4.4 Environment Variables **File:** `infrastructure/dev/.env` ```bash # Database POSTGRES_USER=knowfoolery POSTGRES_PASSWORD=devpassword POSTGRES_HOST=localhost POSTGRES_PORT=5432 POSTGRES_DB=knowfoolery # Redis REDIS_HOST=localhost REDIS_PORT=6379 # Zitadel (when auth profile is enabled) ZITADEL_URL=http://localhost:8080 ZITADEL_PROJECT_ID=your-project-id ZITADEL_CLIENT_ID=your-client-id # Environment ENVIRONMENT=development LOG_LEVEL=debug # Service Ports GAME_SESSION_PORT=8080 QUESTION_BANK_PORT=8081 USER_SERVICE_PORT=8082 LEADERBOARD_PORT=8083 ADMIN_SERVICE_PORT=8085 GATEWAY_PORT=8086 ``` #### Verification Steps: ```bash cd infrastructure/dev docker-compose up -d postgres redis docker-compose ps docker-compose logs postgres ``` --- ### Task 5: Initialize Frontend Workspace (Yarn Workspaces) **Duration:** 2-3 hours #### 1.5.1 Root Package Configuration **File:** `frontend/package.json` ```json { "name": "knowfoolery-frontend", "private": true, "workspaces": [ "shared/*", "apps/*" ], "scripts": { "dev": "yarn workspace @knowfoolery/web dev", "build": "yarn workspaces foreach -A run build", "lint": "eslint . --ext .ts,.tsx", "lint:fix": "eslint . --ext .ts,.tsx --fix", "format": "prettier --write .", "format:check": "prettier --check .", "test": "yarn workspaces foreach -A run test", "clean": "yarn workspaces foreach -A run clean" }, "devDependencies": { "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "eslint": "^8.0.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-solid": "^0.13.0", "prettier": "^3.0.0", "typescript": "^5.0.0" }, "packageManager": "yarn@4.0.0" } ``` #### 1.5.2 Web Application Package **File:** `frontend/apps/web/package.json` ```json { "name": "@knowfoolery/web", "private": true, "version": "0.0.1", "type": "module", "scripts": { "dev": "vite", "build": "tsc && vite build", "preview": "vite preview", "test": "vitest", "clean": "rm -rf dist node_modules/.vite" }, "dependencies": { "solid-js": "^1.9.0", "@solidjs/router": "^0.10.0", "@suid/material": "^0.16.0", "@suid/icons-material": "^0.7.0" }, "devDependencies": { "vite": "^5.0.0", "vite-plugin-solid": "^2.8.0", "vitest": "^1.0.0", "@solidjs/testing-library": "^0.8.0" } } ``` **File:** `frontend/apps/web/vite.config.ts` ```typescript import { defineConfig } from 'vite' import solid from 'vite-plugin-solid' export default defineConfig({ plugins: [solid()], server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8086', changeOrigin: true, }, }, }, build: { target: 'esnext', }, }) ``` **File:** `frontend/apps/web/tsconfig.json` ```json { "compilerOptions": { "target": "ESNext", "module": "ESNext", "moduleResolution": "bundler", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "jsx": "preserve", "jsxImportSource": "solid-js", "types": ["vite/client"], "noEmit": true, "isolatedModules": true, "strict": true, "skipLibCheck": true, "resolveJsonModule": true, "baseUrl": ".", "paths": { "@/*": ["src/*"], "@shared/*": ["../../shared/*"] } }, "include": ["src"] } ``` **File:** `frontend/apps/web/index.html` ```html
Quiz game coming soon...