8.9 KiB
Architecture
Analysis Date: 2026-03-11
Pattern Overview
Overall: Monolithic full-stack with clear backend (Go) and frontend (React) separation, communicating via REST API. The Go binary embeds the compiled React frontend and serves the complete SPA with static assets and API routes.
Key Characteristics:
- Monolithic deployment (single Docker container)
- Backend-for-Frontend pattern: Go handles routing, SPA serving, and REST API
- Session-based authentication via HTTP-only cookies and JWT tokens
- Server-side budget totals computation (not stored in DB)
- Frontend client-side routing within embedded SPA
- User-scoped data isolation at database level
Layers
Presentation (Frontend):
- Purpose: Render UI, handle user interactions, manage form state and navigation
- Location:
frontend/src/ - Contains: React components, pages, hooks, translation files, styling (Tailwind)
- Depends on: REST API via
lib/api.ts, i18n for localization, React Router for navigation - Used by: Users accessing the application through the browser
API/Handlers (Backend):
- Purpose: HTTP request routing, validation, and response serialization
- Location:
backend/internal/api/ - Contains:
router.go(chi mux setup, middleware, routes),handlers.go(HTTP handler functions) - Depends on: Database queries, authentication, models
- Used by: Frontend making API calls, middleware chain
Authentication (Backend):
- Purpose: Credential verification, session token generation/validation, user context injection
- Location:
backend/internal/auth/ - Contains: Password hashing (bcrypt), JWT token generation/validation, session cookies, auth middleware
- Depends on: Models (User), external crypto libraries
- Used by: API handlers, router middleware
Database Layer (Backend):
- Purpose: Persist and retrieve user data (users, categories, budgets, budget items)
- Location:
backend/internal/db/ - Contains:
db.go(connection pool, migrations runner),queries.go(CRUD operations) - Depends on: PostgreSQL 16, pgx driver, models
- Used by: API handlers, migration system
Models (Backend):
- Purpose: Domain type definitions shared between API and database layers
- Location:
backend/internal/models/ - Contains: User, Category, Budget, BudgetItem, BudgetTotals, BudgetDetail structs with JSON tags
- Depends on: External types (uuid.UUID, decimal.Decimal)
- Used by: API handlers, database queries, JSON serialization
Frontend Hooks:
- Purpose: Encapsulate API data fetching and state management
- Location:
frontend/src/hooks/ - Contains:
useAuth.ts(login, register, logout, user state),useBudgets.ts(budget list, selection, loading state) - Depends on: React hooks, API client
- Used by: Page components and nested component trees
Frontend API Client:
- Purpose: Centralize HTTP requests with common configuration, error handling, type definitions
- Location:
frontend/src/lib/api.ts - Contains: Type definitions (User, Category, Budget, BudgetItem, BudgetDetail), request helper, namespace functions for routes (auth, categories, budgets, budgetItems, settings)
- Depends on: Fetch API, error handling
- Used by: All hooks and components making API calls
Data Flow
User Registration/Login Flow:
- User submits credentials via LoginPage or RegisterPage
- Frontend calls
auth.register()orauth.login()fromlib/api.ts - API request goes to
/api/auth/registeror/api/auth/login - Backend handler (
handlers.go) validates credentials, hashes password (bcrypt) - Database layer creates user or validates stored password
- Backend generates JWT token via
auth.GenerateToken()and sets HTTP-only session cookie - Frontend receives User object, stores in
useAuth()state - Frontend redirects to authenticated view (AppLayout with Router)
Budget Data Flow:
- DashboardPage or BudgetSetup component loads via page render
- useBudgets hook calls
budgets.list()on mount - API request goes to
/api/budgets - Auth middleware validates session cookie and injects userID into context
- Handler retrieves budgets for authenticated user via
queries.ListBudgets(userID) - Database joins budget items with categories and computes totals server-side
- Backend responds with Budget list (or BudgetDetail with Items and Totals on
/api/budgets/{id}) - Frontend receives data, updates component state
- Components render UI using fetched data
State Management:
- Authentication: Centralized in
useAuth()hook; persists in React state during session, restored from/api/auth/meon page load - Budget data: Managed per-page/component via
useBudgets()hook; re-fetched when creating/updating/deleting - Form state: Local component state (useState) for forms like BudgetSetup, VariableExpenses
- i18n: Global i18n instance in
frontend/src/i18n/index.tssynced with user preference from backend
Key Abstractions
JWT Session Token:
- Purpose: Stateless authentication without server-side session storage
- Examples: Generated in
backend/internal/auth/auth.goviaGenerateToken(), validated inMiddleware()andMe()handler - Pattern: Token contains user ID, expires in 7 days; stored in HTTP-only cookie
session
User-Scoped Data:
- Purpose: All queries filter by authenticated user ID to prevent cross-user data leaks
- Examples: Categories, Budgets, BudgetItems all require userID in queries (
queries.ListCategories(userID),queries.ListBudgets(userID)) - Pattern: userID injected into request context by auth middleware, extracted via
auth.UserIDFromContext()
Category Types:
- Purpose: Categorize budget items (bill, variable_expense, debt, saving, investment, income)
- Examples: In
backend/internal/models/models.goas enum, mirrored in frontend API types, mapped to category_type ENUM in PostgreSQL - Pattern: Controls how budget totals are computed and displayed (separate rows for each type)
BudgetDetail with Computed Totals:
- Purpose: Single response containing budget, items, and pre-computed category totals
- Examples: Returned by
GetBudgetWithItems(), includes BudgetTotals with budget/actual for each category type - Pattern: Server-side computation reduces client logic; frontend only displays values
API Client Namespaces:
- Purpose: Group related endpoints for discoverability and organization
- Examples:
auth.*,categories.*,budgets.*,budgetItems.*,settings.*infrontend/src/lib/api.ts - Pattern: Declarative CRUD-style methods (list, create, get, update, delete) mapped to HTTP verbs and paths
Entry Points
Backend:
- Location:
backend/cmd/server/main.go - Triggers:
go run ./cmd/serveror binary execution in Docker - Responsibilities: Reads environment variables, connects to PostgreSQL, runs migrations, initializes API router with embedded frontend, starts HTTP server with graceful shutdown
Frontend:
- Location:
frontend/src/main.tsx - Triggers: Vite dev server or loaded index.html in production
- Responsibilities: Mounts React app into DOM root element, applies StrictMode for dev warnings
Frontend App Root:
- Location:
frontend/src/App.tsx - Triggers: Mounted by main.tsx
- Responsibilities: Initializes BrowserRouter, manages auth state via useAuth(), conditionally renders LoginPage/RegisterPage or authenticated app with Routes
Error Handling
Strategy: RESTful status codes with JSON error messages. Frontend catches API errors and displays user-friendly messages.
Patterns:
- Backend HTTP errors: writeError() helper sets HTTP status (400, 401, 404, 409, 500) and JSON body
{"error": "message"} - Frontend API errors: ApiError class wraps HTTP status and message; caught in try/catch blocks
- Auth failures: Return 401 Unauthorized with "unauthorized" message; frontend redirects to login
- Validation failures: Return 400 Bad Request with specific field or format error messages
- Unique constraint violations: Return 409 Conflict for duplicate email, duplicate OIDC subject
- Not found: Return 404 for missing resources (budget, category, user)
- Server errors: Return 500 for unexpected failures (password hashing, database errors)
Cross-Cutting Concerns
Logging: Backend uses Go's log package; logs connection errors, migrations, server startup/shutdown. Frontend uses console (no dedicated logger).
Validation: Backend validates on handler entry (required fields, date formats). Frontend validates form state (required inputs, date picker constraints). No shared validation schema.
Authentication: Session cookie with JWT payload. Middleware on protected routes extracts userID from context. Frontend stores user in state and refetches on navigation.
Internationalization: Frontend uses i18n-next with translation files in frontend/src/i18n/ (de.json, en.json). User preference stored in database (preferred_locale), synced via useAuth on login.
CORS: Enabled for localhost:5173 (Vite dev) and localhost:8080 (Docker production). Credentials included in requests.
Architecture analysis: 2026-03-11