Architecture & Key Flows¶
This document describes Team Health Check's system architecture, component interactions, and the three most critical user flows. Read this before touching the backend or adding new API routes.
System Overview¶
Team Health Check is a decoupled frontend/backend application:
Browser
|-- loads UI ----------> Next.js 15 (port 3000)
| App Router, TypeScript, Tailwind CSS
|
|-- HTTP/JSON ---------> Go / Gin backend (port 8080)
Domain-Driven Design, database/sql + lib/pq
|
| SQL (database/sql, lib/pq driver)
v
PostgreSQL 17 (port 5432)
The browser calls the Go backend directly: frontend/lib/api/client.ts sets API_BASE_URL to NEXT_PUBLIC_API_URL when set (e.g. http://localhost:8080 in local dev) or to an empty string otherwise, so in production (same-origin, unified container) requests go to relative /api/v1/... paths served by the same Go binary. There is no Next.js route handler under frontend/app/api/. frontend/next.config.ts does define a dev-only rewrites() proxy from /api/:path* to http://localhost:8080/api/:path*, but it's unused in practice because the API client always calls an absolute NEXT_PUBLIC_API_URL in local dev; cross-origin requests in that scenario are handled by the backend's own CORSMiddleware (backend/interfaces/api/middleware/cors.go), registered in backend/cmd/api/main.go.
Technology Stack¶
Frontend¶
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 15.x | React framework, App Router |
| TypeScript | 5.x | Type safety |
| Tailwind CSS | 3.x | Utility-first styling |
| Recharts | 3.x | Radar, bar, and line charts |
| Lucide React | Latest | Icons |
| js-cookie | Latest | Cookie-based auth token |
Backend¶
| Technology | Version | Purpose |
|---|---|---|
| Go | 1.25+ | Application language |
| Gin | Latest | HTTP router and middleware |
| database/sql + lib/pq | v1.10.9 | PostgreSQL driver |
| golang-migrate | Latest | Database migrations |
| golang.org/x/crypto/bcrypt | External package | Password hashing (bcrypt.DefaultCost, cost 10) |
Testing¶
| Technology | Purpose |
|---|---|
| Ginkgo v2 | BDD test framework for Go |
| Gomega | Assertion / matcher library |
| Playwright | Browser automation for E2E tests |
Directory Structure¶
teams360/
├── frontend/ # Next.js 15 application
│ ├── app/ # App Router pages (no /api proxy — calls backend directly)
│ │ ├── home/ # Team Member home page
│ │ ├── survey/ # Health check survey
│ │ ├── dashboard/ # Team Lead dashboard
│ │ ├── manager/ # Manager / VP dashboard
│ │ ├── admin/ # Admin panel
│ │ ├── login/ # Authentication page
│ │ └── auth/callback/ # SSO OAuth callback
│ ├── components/ # Reusable React components
│ ├── lib/ # Utilities, types, API client
│ │ ├── api/ # API client functions
│ │ ├── auth.ts # Authentication utilities
│ │ ├── types.ts # TypeScript type definitions
│ │ └── assessment-period.ts # Auto-detect assessment period
│ └── middleware.ts # Route protection (role-based redirects)
│
├── backend/ # Go API server
│ ├── cmd/api/main.go # Entry point: bootstraps server and registers routes
│ ├── domain/ # DOMAIN LAYER — business logic only
│ │ ├── user/
│ │ ├── team/
│ │ ├── healthcheck/
│ │ └── organization/
│ ├── application/ # APPLICATION LAYER — use cases
│ │ ├── commands/ # Write operations
│ │ └── queries/ # Read operations
│ ├── infrastructure/ # INFRASTRUCTURE LAYER — external concerns
│ │ ├── persistence/postgres/
│ │ │ ├── migrations/ # SQL migration files (000001 - 000020+)
│ │ │ ├── user_repository.go
│ │ │ ├── team_repository.go
│ │ │ ├── health_check_repository.go
│ │ │ └── organization_repository.go
│ │ └── email/ # SES/SMTP email sending
│ └── interfaces/ # INTERFACES LAYER — HTTP handlers
│ ├── api/v1/ # Gin handlers
│ ├── dto/ # Data Transfer Objects
│ └── middleware/ # Auth middleware
│
├── tests/ # E2E acceptance tests (full stack)
│ └── acceptance/
│ ├── suite_test.go # Starts real servers + Playwright
│ ├── e2e_authentication_test.go
│ ├── e2e_survey_submission_test.go
│ └── e2e_manager_dashboard_test.go
│
└── docs/ # This documentation
DDD Layer Responsibilities¶
| Layer | Location | What lives here |
|---|---|---|
| Domain | backend/domain/ |
Entities, value objects, repository interfaces, domain events. Zero external dependencies. |
| Application | backend/application/ |
Use-case orchestration. Calls domain + infrastructure via interfaces. |
| Infrastructure | backend/infrastructure/ |
PostgreSQL repository implementations (database/sql + lib/pq), migrations, email sending. |
| Interfaces | backend/interfaces/ |
Gin HTTP handlers, DTOs, middleware. Thin layer: parse request, call application, return response. |
DDD Aggregates¶
| Aggregate | Root entity | Value objects |
|---|---|---|
| User | User |
— |
| Team | Team |
TeamMember, SupervisorLink |
| HealthCheck | HealthCheckSession |
HealthCheckResponse |
| Organization | OrganizationConfig |
HierarchyLevel, HealthDimension, Permissions |
Role-Based Routing¶
| Role | Route | Capabilities |
|---|---|---|
| Team Member | /home |
Take survey, view personal history |
| Team Lead | /dashboard |
Team health, radar chart, individual responses |
| Manager / Director / VP | /manager |
Multi-team health cards, trends, radar comparison |
| Admin | /admin |
CRUD for users, teams, hierarchy levels, dimensions |
frontend/middleware.ts reads the user cookie on every request and redirects unauthenticated users to /login. It also enforces role-level route guards based on hierarchy_level permissions.
Critical Flow 1: Authentication (Login)¶
Entry point: GET /login → user fills form → POST /api/v1/auth/login
Browser Go / Gin PostgreSQL
| | |
| 1. POST /api/v1/auth/login | |
| username + password | |
| ──────────────────────────────────────> | |
| | 2. SELECT user |
| | WHERE username |
| | ─────────────────> |
| | |
| | 3. Return user row |
| | <───────────────── |
| | |
| | 4. bcrypt.Compare |
| | (pw, hash) |
| | |
| 5. 200 + JWT access/refresh | |
| <────────────────────────────────────── | |
| | |
| 6. Store tokens in localStorage and set `user` cookie |
| 7. Redirect to role dashboard | |
Components involved:
- frontend/app/login/page.tsx — login form; calls the backend directly at NEXT_PUBLIC_API_URL (no proxy route)
- backend/interfaces/api/v1/auth_handler.go — validates credentials, issues JWTs
- backend/infrastructure/persistence/postgres/user_repository.go — FindByUsername()
- frontend/lib/auth.ts — stores accessToken/refreshToken in localStorage and the user cookie
- frontend/middleware.ts — enforces the user cookie on subsequent page loads
Data touched: users table (read-only), password_hash column (bcrypt compare)
SSO variant (OIDC / PKCE):
- Frontend redirects to provider's /authorize endpoint
- Provider redirects back to frontend/app/auth/callback/page.tsx with auth code
- Frontend exchanges code via POST /api/v1/auth/sso/callback
- Backend calls provider's token endpoint, extracts email from ID token, looks up user in DB
- Same cookie-set flow follows
Critical Flow 2: Survey Submission¶
Entry point: /survey page → user completes 11 dimensions → submits
The Next.js server only renders the /survey page shell; all API calls below go directly from browser JS to the Go backend (NEXT_PUBLIC_API_URL), not through Next.js:
Browser (Team Member) Go / Gin PostgreSQL
| | |
| 1. GET /survey (Next.js renders page shell) | |
| | |
| 2. GET /api/v1/health-dimensions | |
| ────────────────────────────────────────────>| |
| | 3. SELECT * FROM |
| | health_dimensions|
| | WHERE is_active |
| | ─────────────────> |
| | <───────────────── |
| 4. 11 dimensions | |
| <────────────────────────────────────────────| |
| 5. Render survey form | |
| | |
| 6. User selects score + trend for | |
| each dimension | |
| | |
| 7. POST /api/v1/health-checks | |
| {teamId, userId, responses[11]} | |
| (authenticatedFetch, Bearer JWT) | |
| ────────────────────────────────────────────>| |
| | 8. BEGIN TX |
| | INSERT session |
| | INSERT 11 rows |
| | into responses |
| | COMMIT |
| | ─────────────────> |
| | <───────────────── |
| 9. 201 session ID | |
| <────────────────────────────────────────────| |
| 10. Redirect to /home | |
Components involved:
- frontend/app/survey/page.tsx — 11-question survey form
- frontend/lib/assessment-period.ts — auto-computes period from submission date
- backend/interfaces/api/v1/health_check_handler.go — SubmitHealthCheck()
- backend/infrastructure/persistence/postgres/health_check_repository.go — transaction: insert session + responses
Data touched:
- health_dimensions (read)
- health_check_sessions (write)
- health_check_responses (write — 11 rows per submission)
Assessment period logic: cadence-driven, computed by frontend/lib/assessment-period.ts — see Domain Model for the full format table and legacy-period mapping.
Critical Flow 3: Manager Dashboard Data Load¶
Entry point: Manager navigates to /manager
Next.js renders the /manager page shell; the health data call goes directly from browser JS to the Go backend:
Browser (Manager) Go / Gin PostgreSQL
| | |
| 1. GET /manager (Next.js renders shell) | |
| | |
| 2. GET /api/v1/managers/{id}/teams/health | |
| (authenticatedFetch, Bearer JWT) | |
| ──────────────────────────────────────────>| |
| | 3. Find supervised |
| | teams via |
| | team_supervisors |
| | ───────────────── > |
| | |
| | 4. For each team: |
| | aggregate scores |
| | by dimension |
| | for current |
| | period |
| | ───────────────── > |
| | <──────────────── |
| | |
| 5. JSON array of team health objects | |
| <──────────────────────────────────────────| |
| | |
| 6. Render team cards + radar charts | |
| (Recharts) | |
Components involved:
- frontend/app/manager/page.tsx — filters teams using canUserAccessTeam() based on supervisor chain
- frontend/lib/org-config.ts — canUserAccessTeam(), getSubordinates()
- backend/interfaces/api/v1/manager_handler.go — query supervised teams + aggregate
- backend/infrastructure/persistence/postgres/ — joins team_supervisors, health_check_sessions, health_check_responses
Data touched:
- team_supervisors (read — find teams under this manager)
- health_check_sessions (read — filter by team + assessment period)
- health_check_responses (read — aggregate scores per dimension)
- health_dimensions (read — dimension names for chart labels)
Critical implementation note: Routes for the manager dashboard must be registered in backend/cmd/api/main.go. A past bug where v1.SetupManagerRoutes() was omitted caused 404s that integration tests did not catch — only E2E tests caught it.
Troubleshooting Matrix¶
| Symptom | Likely cause | First check |
|---|---|---|
/manager returns 404 from backend |
Route not registered in main.go |
Run grep "SetupManagerRoutes" backend/cmd/api/main.go |
Login succeeds but /dashboard shows no data |
Team lead not assigned to any team | Check team_members table: SELECT * FROM team_members WHERE user_id = 'teamlead1' |
| Survey submit returns 500 | Transaction rollback — duplicate session or FK violation | Check backend logs; run SELECT * FROM health_check_sessions WHERE user_id = '...' ORDER BY created_at DESC LIMIT 5 |
| Manager sees 0 teams | Supervisor chain not populated | Check team_supervisors table for manager's user_id |
| Dimension scores show 0 or NaN | No completed sessions in current assessment period | Confirm period label matches: SELECT DISTINCT assessment_period FROM health_check_sessions |
| Frontend blank white page | Next.js build error or NEXT_PUBLIC_API_URL misconfigured | Check browser console; verify NEXT_PUBLIC_API_URL=http://localhost:8080 |
bcrypt: too many arguments error on login |
password_hash seeded as plaintext, not bcrypt hash | Run make db-reset && make db-setup to re-seed with hashed passwords |
| SSO login returns "user not found" | User email does not match any DB record | Pre-create user in Admin panel with matching email; SSO does not auto-provision accounts |
port already in use on startup |
Prior server process still running | lsof -ti:3000 | xargs kill -9 and lsof -ti:8080 | xargs kill -9 |
SWC error on Mac ARM64 |
Wrong native SWC binary installed | Run npm install --force @next/swc-darwin-arm64 in frontend/ |
Observability Hooks¶
The backend emits OpenTelemetry spans and metrics on every request. See Operations & Observability for the full catalogue. Key spans:
| Span name | Triggered by |
|---|---|
auth.login |
POST /api/v1/auth/login |
healthcheck.submit |
POST /api/v1/health-checks |
manager.teams.health |
GET /api/v1/managers/{id}/teams/health |
db.query |
Every database/sql query execution |