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.
28 KiB
28 KiB
Know Foolery - Development Guidelines & Coding Standards
Overview
This document establishes coding standards, development practices, and guidelines for the Know Foolery project to ensure consistency, maintainability, and quality across the codebase.
Code Quality Standards
General Code Quality Orientations
- Type Safety: TypeScript for frontend, Go's type system for backend
- Code Review: Mandatory peer review for all changes
- Static Analysis: Automated code quality checks
- Documentation: Comprehensive API and component documentation
Linting and Formatting
# .golangci.yml
run:
timeout: 5m
tests: true
linters:
enable:
- gofmt
- goimports
- govet
- errcheck
- staticcheck
- unused
- gosimple
- structcheck
- varcheck
- ineffassign
- deadcode
- typecheck
- gosec
- misspell
- lll
- unconvert
- dupl
- goconst
- gocyclo
linters-settings:
lll:
line-length: 120
gocyclo:
min-complexity: 15
dupl:
threshold: 150
issues:
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- errcheck
- dupl
- gosec
// .eslintrc.json
{
"extends": [
"plugin:solid/typescript",
"@typescript-eslint/recommended",
"prettier"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "solid"],
"rules": {
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/prefer-const": "error",
"solid/reactivity": "warn",
"prefer-const": "error",
"no-var": "error",
"object-shorthand": "error",
"prefer-template": "error"
}
}
Naming Conventions
Go Code
// Files: snake_case
user_service.go
jwt_middleware.go
database_client.go
// Packages: lowercase, single word when possible
package auth
package database
package observability
// Exported types: PascalCase
type GameService struct {}
type AuthClaims struct {}
type DatabaseConfig struct {}
// Unexported types: camelCase
type userRepository struct {}
type tokenValidator struct {}
// Functions: PascalCase (exported), camelCase (unexported)
func NewGameService() *GameService {}
func (s *GameService) StartGame() error {}
func validateToken() error {}
// Constants: PascalCase or SCREAMING_SNAKE_CASE for groups
const DefaultTimeout = 30 * time.Second
const (
StatusActive = "active"
StatusInactive = "inactive"
)
// Interfaces: noun or adjective + "er" suffix
type TokenValidator interface {}
type UserRepository interface {}
type ScoreCalculator interface {}
TypeScript Code
// Files: kebab-case
game-card.tsx
user-service.ts
auth-store.ts
// Components: PascalCase
export const GameCard: Component<GameCardProps> = () => {}
export const LeaderboardList: Component = () => {}
// Composables/stores: camelCase with "create"/"use" prefix
export const createGameSessionStore = () => {}
export const useAuthentication = () => {}
// Functions: camelCase
export const calculateScore = () => {}
export const validateAnswer = () => {}
// Types/Interfaces: PascalCase
interface GameSessionProps {}
type AuthenticationState = 'loading' | 'authenticated' | 'unauthenticated'
// Constants: SCREAMING_SNAKE_CASE
export const MAX_ATTEMPTS = 3
export const SESSION_TIMEOUT = 30 * 60 * 1000 // 30 minutes
Go Development Standards
Fiber Framework Patterns
Service Structure
// Service initialization with Fiber
package main
import (
"log"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/cors"
"github.com/gofiber/fiber/v3/middleware/logger"
"github.com/gofiber/fiber/v3/middleware/recover"
)
func main() {
// Create Fiber app with configuration
app := fiber.New(fiber.Config{
AppName: "Know Foolery Game Service",
ServerHeader: "Fiber",
ErrorHandler: customErrorHandler,
ReadTimeout: time.Second * 10,
WriteTimeout: time.Second * 10,
IdleTimeout: time.Second * 120,
})
// Global middleware
app.Use(logger.New())
app.Use(recover.New())
app.Use(cors.New(cors.Config{
AllowOrigins: "http://localhost:3000, https://app.knowfoolery.com",
AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
AllowHeaders: "Origin, Content-Type, Authorization",
}))
// Health check endpoint
app.Get("/health", healthCheck)
// API routes with versioning
api := app.Group("/api/v1")
setupGameRoutes(api)
// Start server
log.Fatal(app.Listen(":8080"))
}
func customErrorHandler(c *fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
message := "Internal Server Error"
if e, ok := err.(*fiber.Error); ok {
code = e.Code
message = e.Message
}
return c.Status(code).JSON(fiber.Map{
"error": true,
"message": message,
"code": code,
})
}
Handler Patterns
// Handler structure and patterns
package handlers
import (
"github.com/gofiber/fiber/v3"
"knowfoolery/internal/services"
)
type GameHandler struct {
gameService *services.GameService
validator *validator.Validate
}
func NewGameHandler(gameService *services.GameService) *GameHandler {
return &GameHandler{
gameService: gameService,
validator: validator.New(),
}
}
// HTTP handler with proper error handling
func (h *GameHandler) StartGame(c *fiber.Ctx) error {
var req StartGameRequest
// Parse and validate request
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"message": "Invalid request body",
})
}
if err := h.validator.Struct(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"message": "Validation failed",
"details": err.Error(),
})
}
// Extract user context from JWT middleware
userID := c.Locals("user_id").(string)
userRole := c.Locals("user_role").(string)
// Business logic
gameSession, err := h.gameService.StartGame(c.Context(), userID, req.PlayerName)
if err != nil {
return handleServiceError(c, err)
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
"success": true,
"data": gameSession,
})
}
// Centralized error handling
func handleServiceError(c *fiber.Ctx, err error) error {
switch {
case errors.Is(err, services.ErrInvalidInput):
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": true,
"message": "Invalid input",
"details": err.Error(),
})
case errors.Is(err, services.ErrNotFound):
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"message": "Resource not found",
})
case errors.Is(err, services.ErrUnauthorized):
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": true,
"message": "Unauthorized access",
})
default:
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"message": "Internal server error",
})
}
}
// Request/Response structures with validation
type StartGameRequest struct {
PlayerName string `json:"player_name" validate:"required,min=2,max=50,alphanum_space"`
}
type StartGameResponse struct {
SessionID string `json:"session_id"`
PlayerName string `json:"player_name"`
StartTime time.Time `json:"start_time"`
TimeRemaining int `json:"time_remaining_seconds"`
}
Middleware Patterns
// Custom middleware for authentication
package middleware
import (
"github.com/gofiber/fiber/v3"
"knowfoolery/shared/auth"
)
func JWTMiddleware(zitadelRepo auth.ZitadelRepository) fiber.Handler {
return func(c *fiber.Ctx) error {
// Extract token from Authorization header
authHeader := c.Get("Authorization")
if authHeader == "" {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": true,
"message": "Authorization header required",
})
}
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == authHeader {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": true,
"message": "Invalid authorization header format",
})
}
// Validate token
claims, err := zitadelRepo.ValidateToken(c.Context(), token)
if err != nil {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": true,
"message": "Invalid token",
})
}
// Set user context
c.Locals("user_id", claims.Subject)
c.Locals("user_email", claims.Email)
c.Locals("user_name", claims.Name)
c.Locals("user_roles", claims.Roles)
return c.Next()
}
}
// Rate limiting middleware
func RateLimitMiddleware(redisClient *redis.Client) fiber.Handler {
return func(c *fiber.Ctx) error {
userID := c.Locals("user_id")
clientIP := c.IP()
// Per-user rate limiting
if userID != nil {
userKey := fmt.Sprintf("rate_limit:user:%s", userID)
if !allowRequest(redisClient, userKey, 60, time.Minute) {
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": true,
"message": "Rate limit exceeded for user",
})
}
}
// Per-IP rate limiting
ipKey := fmt.Sprintf("rate_limit:ip:%s", clientIP)
if !allowRequest(redisClient, ipKey, 100, time.Minute) {
return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{
"error": true,
"message": "Rate limit exceeded for IP",
})
}
return c.Next()
}
}
Database Patterns with Ent
Schema Definition Standards
// Ent schema with proper field validation and indexes
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
"time"
)
type Question struct {
ent.Schema
}
func (Question) Fields() []ent.Field {
return []ent.Field{
field.String("theme").
NotEmpty().
MaxLen(100).
Comment("Question category/theme"),
field.Text("text").
NotEmpty().
MaxLen(1000).
Comment("The actual question text"),
field.String("answer").
NotEmpty().
MaxLen(500).
Comment("Correct answer"),
field.Text("hint").
Optional().
MaxLen(500).
Comment("Optional hint text"),
field.Enum("difficulty").
Values("easy", "medium", "hard").
Default("medium").
Comment("Question difficulty level"),
field.Bool("is_active").
Default(true).
Comment("Whether question is active"),
field.Time("created_at").
Default(time.Now).
Immutable().
Comment("Creation timestamp"),
field.Time("updated_at").
Default(time.Now).
UpdateDefault(time.Now).
Comment("Last update timestamp"),
}
}
func (Question) Indexes() []ent.Index {
return []ent.Index{
index.Fields("theme", "is_active"),
index.Fields("difficulty", "is_active"),
index.Fields("created_at"),
}
}
func (Question) Edges() []ent.Edge {
return []ent.Edge{
edge.To("attempts", QuestionAttempt.Type),
}
}
Service Layer Patterns
// Service layer with proper error handling
package services
import (
"context"
"errors"
"knowfoolery/internal/models"
"knowfoolery/ent"
)
var (
ErrInvalidInput = errors.New("invalid input")
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized access")
ErrGameInProgress = errors.New("game already in progress")
ErrSessionExpired = errors.New("session expired")
)
type GameService struct {
client *ent.Client
questionSvc QuestionService
sessionSvc SessionService
validator *validator.Validate
}
func NewGameService(
client *ent.Client,
questionSvc QuestionService,
sessionSvc SessionService,
) *GameService {
return &GameService{
client: client,
questionSvc: questionSvc,
sessionSvc: sessionSvc,
validator: validator.New(),
}
}
func (s *GameService) StartGame(ctx context.Context, userID, playerName string) (*models.GameSession, error) {
// Validate input
if err := s.validatePlayerName(playerName); err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err)
}
// Check for existing active session
activeSession, err := s.client.GameSession.
Query().
Where(gamesession.And(
gamesession.PlayerNameEQ(playerName),
gamesession.StatusEQ(gamesession.StatusActive),
)).
Only(ctx)
if err != nil && !ent.IsNotFound(err) {
return nil, fmt.Errorf("failed to check existing session: %w", err)
}
if activeSession != nil {
return nil, ErrGameInProgress
}
// Start database transaction
tx, err := s.client.Tx(ctx)
if err != nil {
return nil, fmt.Errorf("failed to start transaction: %w", err)
}
defer tx.Rollback()
// Create new game session
session, err := tx.GameSession.
Create().
SetPlayerName(playerName).
SetTotalScore(0).
SetQuestionsAsked(0).
SetQuestionsCorrect(0).
SetHintsUsed(0).
SetStartTime(time.Now()).
SetStatus(gamesession.StatusActive).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to create session: %w", err)
}
// Get first question
question, err := s.questionSvc.GetRandomQuestion(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get question: %w", err)
}
// Update session with first question
session, err = session.Update().
SetCurrentQuestionID(question.ID).
SetCurrentAttempts(0).
Save(ctx)
if err != nil {
return nil, fmt.Errorf("failed to update session: %w", err)
}
// Commit transaction
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("failed to commit transaction: %w", err)
}
return &models.GameSession{
ID: session.ID,
PlayerName: session.PlayerName,
TotalScore: session.TotalScore,
StartTime: session.StartTime,
Status: string(session.Status),
CurrentQuestion: &models.Question{
ID: question.ID,
Theme: question.Theme,
Text: question.Text,
Hint: question.Hint,
},
TimeRemaining: s.calculateTimeRemaining(session.StartTime),
}, nil
}
func (s *GameService) validatePlayerName(name string) error {
type PlayerNameValidation struct {
Name string `validate:"required,min=2,max=50,alphanum_space"`
}
return s.validator.Struct(&PlayerNameValidation{Name: name})
}
func (s *GameService) calculateTimeRemaining(startTime time.Time) int {
const sessionDuration = 30 * time.Minute
elapsed := time.Since(startTime)
remaining := sessionDuration - elapsed
if remaining < 0 {
return 0
}
return int(remaining.Seconds())
}
TypeScript Development Standards
SolidJS Component Patterns
Component Structure
// Component with proper TypeScript types and SolidJS patterns
import type { Component } from 'solid-js'
import { createMemo, createSignal, Show } from 'solid-js'
import {
Box,
Button,
Input,
LinearProgress,
Typography,
Chip,
} from '@suid/material'
// Props interface with proper documentation
export interface GameCardProps {
/** The question to display */
question: string
/** Theme/category of the question */
theme: string
/** Time remaining in seconds */
timeRemaining: number
/** Number of attempts left */
attemptsLeft: number
/** Callback when answer is submitted */
onSubmitAnswer: (answer: string) => Promise<void>
/** Loading state from async operations */
loading?: boolean
}
/**
* GameCard component displays a quiz question with input and controls
*
* @param props - GameCard props
* @returns SolidJS component
*/
export const GameCard: Component<GameCardProps> = (props) => {
const [answer, setAnswer] = createSignal('')
const [isSubmitting, setIsSubmitting] = createSignal(false)
const disabled = createMemo(() => props.loading || isSubmitting())
const progressValue = createMemo(() => ((30 * 60 - props.timeRemaining) / (30 * 60)) * 100)
const handleSubmit = async () => {
const value = answer().trim()
if (!value || disabled()) return
setIsSubmitting(true)
try {
await props.onSubmitAnswer(value)
setAnswer('')
} catch (error) {
console.error('Failed to submit answer:', error)
} finally {
setIsSubmitting(false)
}
}
return (
<Box sx={{ p: 3, border: '1px solid', borderColor: 'divider', borderRadius: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 1 }}>
<Chip label={props.theme} />
<Typography variant="body2">{props.timeRemaining}s</Typography>
</Box>
<LinearProgress variant="determinate" value={progressValue()} />
<Typography variant="h6" sx={{ mt: 2 }}>{props.question}</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>Attempts left: {props.attemptsLeft}/3</Typography>
<Input
value={answer()}
onInput={(e) => setAnswer(e.currentTarget.value)}
disabled={disabled()}
placeholder="Enter your answer"
/>
<Button onClick={handleSubmit} disabled={disabled() || answer().trim().length === 0}>
Submit Answer
</Button>
<Show when={disabled()}>
<Typography variant="caption">Submitting...</Typography>
</Show>
</Box>
)
}
Solid Composable Pattern
import { createSignal } from 'solid-js'
import { GameSessionService } from '../services/game-session-service'
export const createGameSessionStore = (playerName: () => string) => {
const service = new GameSessionService()
const [isLoading, setIsLoading] = createSignal(false)
const [error, setError] = createSignal<string | null>(null)
const [sessionId, setSessionId] = createSignal<string | null>(null)
const startGame = async () => {
setIsLoading(true)
setError(null)
try {
const session = await service.startGame(playerName())
setSessionId(session.sessionId)
return session
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to start game'
setError(message)
throw err
} finally {
setIsLoading(false)
}
}
return {
isLoading,
error,
sessionId,
startGame,
}
}
Testing Standards
Go Testing Patterns
// Comprehensive testing with testify and testcontainers
package services_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go/modules/postgres"
"knowfoolery/internal/services"
"knowfoolery/ent/enttest"
)
type GameServiceTestSuite struct {
suite.Suite
ctx context.Context
gameService *services.GameService
client *ent.Client
}
func (suite *GameServiceTestSuite) SetupSuite() {
suite.ctx = context.Background()
// Start PostgreSQL container for integration tests
postgresContainer, err := postgres.RunContainer(suite.ctx,
testcontainers.WithImage("postgres:15-alpine"),
postgres.WithDatabase("testdb"),
postgres.WithUsername("testuser"),
postgres.WithPassword("testpass"),
)
require.NoError(suite.T(), err)
connStr, err := postgresContainer.ConnectionString(suite.ctx)
require.NoError(suite.T(), err)
// Create Ent client with test database
suite.client = enttest.Open(suite.T(), "postgres", connStr)
// Initialize service
suite.gameService = services.NewGameService(
suite.client,
services.NewQuestionService(suite.client),
services.NewSessionService(suite.client),
)
}
func (suite *GameServiceTestSuite) TearDownSuite() {
suite.client.Close()
}
func (suite *GameServiceTestSuite) SetupTest() {
// Clean database before each test
suite.client.GameSession.Delete().ExecX(suite.ctx)
suite.client.Question.Delete().ExecX(suite.ctx)
// Seed test data
suite.seedTestQuestions()
}
func (suite *GameServiceTestSuite) TestStartGame_Success() {
// Arrange
playerName := "TestPlayer"
// Act
session, err := suite.gameService.StartGame(suite.ctx, "user123", playerName)
// Assert
require.NoError(suite.T(), err)
assert.NotEmpty(suite.T(), session.ID)
assert.Equal(suite.T(), playerName, session.PlayerName)
assert.Equal(suite.T(), 0, session.TotalScore)
assert.NotNil(suite.T(), session.CurrentQuestion)
assert.True(suite.T(), session.TimeRemaining > 0)
}
func (suite *GameServiceTestSuite) TestStartGame_InvalidPlayerName() {
// Test cases for validation
testCases := []struct {
name string
playerName string
expectedErr string
}{
{"empty name", "", "required"},
{"too short", "a", "min"},
{"too long", strings.Repeat("a", 51), "max"},
{"invalid chars", "player@123", "alphanum_space"},
}
for _, tc := range testCases {
suite.T().Run(tc.name, func(t *testing.T) {
_, err := suite.gameService.StartGame(suite.ctx, "user123", tc.playerName)
require.Error(t, err)
assert.Contains(t, err.Error(), tc.expectedErr)
})
}
}
func (suite *GameServiceTestSuite) TestStartGame_ExistingActiveSession() {
// Arrange
playerName := "TestPlayer"
// Start first game
_, err := suite.gameService.StartGame(suite.ctx, "user123", playerName)
require.NoError(suite.T(), err)
// Act - try to start second game
_, err = suite.gameService.StartGame(suite.ctx, "user123", playerName)
// Assert
require.Error(suite.T(), err)
assert.Equal(suite.T(), services.ErrGameInProgress, err)
}
func (suite *GameServiceTestSuite) seedTestQuestions() {
questions := []struct {
theme string
text string
answer string
hint string
}{
{"Geography", "What is the capital of France?", "Paris", "City of Light"},
{"History", "Who painted the Mona Lisa?", "Leonardo da Vinci", "Renaissance artist"},
{"Science", "What is the largest planet?", "Jupiter", "Gas giant"},
}
for _, q := range questions {
suite.client.Question.Create().
SetTheme(q.theme).
SetText(q.text).
SetAnswer(q.answer).
SetHint(q.hint).
SetIsActive(true).
ExecX(suite.ctx)
}
}
func TestGameServiceSuite(t *testing.T) {
suite.Run(t, new(GameServiceTestSuite))
}
// Benchmark tests
func BenchmarkGameService_StartGame(b *testing.B) {
// Setup
ctx := context.Background()
client := enttest.Open(b, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
gameService := services.NewGameService(client, nil, nil)
// Benchmark
b.ResetTimer()
for i := 0; i < b.N; i++ {
playerName := fmt.Sprintf("Player%d", i)
_, _ = gameService.StartGame(ctx, fmt.Sprintf("user%d", i), playerName)
}
}
TypeScript Testing Patterns
// SolidJS component testing with Vitest and Testing Library
import { render, screen, fireEvent } from '@solidjs/testing-library'
import { describe, expect, it, vi } from 'vitest'
import { GameCard, GameCardProps } from '../GameCard'
// Mock props factory
const createMockProps = (overrides: Partial<GameCardProps> = {}): GameCardProps => ({
question: "What is the capital of France?",
theme: "Geography",
timeRemaining: 1800, // 30 minutes
attemptsLeft: 3,
onSubmitAnswer: vi.fn(),
loading: false,
...overrides,
})
describe('GameCard', () => {
describe('Rendering', () => {
it('should render question and theme correctly', () => {
const props = createMockProps()
render(() => <GameCard {...props} />)
expect(screen.getByText(props.question)).toBeInTheDocument()
expect(screen.getByText(props.theme)).toBeInTheDocument()
})
})
describe('User Interactions', () => {
it('should call onSubmitAnswer when submit button is clicked', async () => {
const props = createMockProps()
const mockOnSubmitAnswer = vi.fn().mockResolvedValue(undefined)
props.onSubmitAnswer = mockOnSubmitAnswer
render(() => <GameCard {...props} />)
const input = screen.getByPlaceholderText('Enter your answer')
fireEvent.input(input, { target: { value: 'Paris' } })
const submitButton = screen.getByRole('button', { name: 'Submit Answer' })
fireEvent.click(submitButton)
expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris')
})
})
})
Git Workflow
# Branch naming conventions
feature/add-game-session-management
bugfix/fix-timer-display-issue
hotfix/security-vulnerability-patch
refactor/improve-database-queries
# Commit message format
type(scope): description
# Types: feat, fix, docs, style, refactor, test, chore
# Examples:
feat(game): add session timeout handling
fix(auth): resolve JWT token validation issue
docs(api): update authentication endpoint documentation
test(services): add comprehensive game service tests
Development Commands
Backend Development
# Navigate to service directory first
cd backend/services/{service-name}
# Start service in development mode with hot reload
go run cmd/main.go
# Service ports are env-driven via shared bootstrap helpers:
# ADMIN_SERVICE_PORT, GAME_SESSION_PORT, GATEWAY_PORT,
# LEADERBOARD_PORT, QUESTION_BANK_PORT, USER_SERVICE_PORT
# Run tests for a specific service
go test ./... -v
# Run tests with coverage
go test ./... -cover
# Build service binary
go build -o bin/{service-name} cmd/main.go
# Generate Ent schemas (from service root)
go generate ./...
# Lint Go code
golangci-lint run
# Format Go code
go fmt ./...
goimports -w .
# Update go.mod of all services
find . -type f -name "*.mod" -printf "%h\0" | sort -zu | xargs -0 -I{} sh -c 'cd "{}" && go mod tidy'
# Run all backend tests
go test $(go list -f '{{.Dir}}/...' -m | xargs)
# Go vet all backend code
go vet $(go list -f '{{.Dir}}/...' -m | xargs)
# Lint all backend code
golangci-lint run $(go list -f '{{.Dir}}/...' -m | xargs)
Development Environment
# Start development stack with Docker Compose
cd infrastructure/dev
docker-compose up -d
# This starts:
# - PostgreSQL database (port 5432)
# - Redis cache (port 6379)
# - Zitadel auth server (port 8080)
Frontend Development
Note: This project uses Yarn with nodeLinker: node-modules. PnP files like .pnp.cjs and .pnp.loader.mjs are not used.
Status: Web commands are implemented. Tauri cross-platform commands are planned and require a future frontend/apps/cross-platform workspace.
# Web application
cd frontend
yarn install
yarn dev # Start web development server (SolidJS)
yarn build # Build all frontend workspaces
yarn test # Run Vitest suites
yarn lint # ESLint
yarn format:check # Prettier check
# Cross-platform packaging with Tauri (desktop/mobile)
cd frontend/apps/cross-platform
yarn tauri dev # Run Tauri app with embedded web UI
yarn tauri build # Build release bundles