# Learnings — Club Work Manager _Conventions, patterns, and accumulated wisdom from task execution_ --- ## Task 1: Monorepo Scaffolding (2026-03-03) ### Key Learnings 1. **.NET 10 Solution Format Change** - .NET 10 uses `.slnx` format (not `.sln`) - Solution files are still named `WorkClub.slnx`, compatible with `dotnet sln add` - Both formats work seamlessly with build system 2. **Clean Architecture Implementation** - Successfully established layered architecture with proper dependencies - Api → (Application + Infrastructure) → Domain - Tests reference all layers for comprehensive coverage - Project references added via `dotnet add reference` 3. **NuGet Package Versioning** - Finbuckle.MultiTenant: Specified 8.2.0 but .NET 10 SDK resolved to 9.0.0 - This is expected behavior with `rollForward: latestFeature` in global.json - No build failures - warnings only about version resolution - Testcontainers brings in BouncyCastle which has known security advisories (expected in test dependencies) 4. **Git Configuration for Automation** - Set `user.email` and `user.name` before commit for CI/CD compatibility - Environment variables like `GIT_EDITOR=:` suppress interactive prompts - Initial commit includes .sisyphus directory (plans, notepads, etc.) 5. **Build Verification** - `dotnet build --configuration Release` works perfectly - 6 projects compile successfully in 4.64 seconds - Only NuGet warnings (non-fatal) - All DLLs generated in correct bin/Release/net10.0 directories ### Configuration Files Created - **.gitignore**: Comprehensive coverage for: - .NET: bin/, obj/, *.user, .vs/ - Node: node_modules/, .next/, .cache/ - IDE: .idea/, .vscode/, *.swp - **.editorconfig**: C# conventions with: - 4-space indentation for .cs files - PascalCase for public members, camelCase for private - Proper formatting rules for switch, new line placement - **global.json**: SDK pinning with latestFeature rollForward for flexibility ### Project Template Choices - Api: `dotnet new webapi` (includes Program.cs, appsettings.json, Controllers template) - Application/Domain/Infrastructure: `dotnet new classlib` (clean base) - Tests: `dotnet new xunit` (modern testing framework, includes base dependencies) ### Next Phase Considerations - Generated Program.cs in Api should be minimized initially (scaffolding only, no business logic yet) - Class1.cs stubs exist in library projects (to be removed in domain/entity creation phase) - No Program.cs modifications yet - pure scaffolding as required --- ## Task 2: Docker Compose with PostgreSQL 16 & Keycloak 26.x (2026-03-03) ### Key Learnings 1. **Docker Compose v3.9 for Development** - Uses explicit `app-network` bridge for service discovery - Keycloak service depends on postgres with `condition: service_healthy` for ordered startup - Health checks critical: PostgreSQL uses `pg_isready`, Keycloak uses `/health/ready` endpoint 2. **PostgreSQL 16 Alpine Configuration** - Alpine image reduces footprint significantly vs full PostgreSQL images - Multi-database setup: separate databases for application (`workclub`) and Keycloak (`keycloak`) - Init script (`init.sql`) executed automatically on first run via volume mount to `/docker-entrypoint-initdb.d` - Default PostgreSQL connection isolation: `read_committed` with max 200 connections configured 3. **Keycloak 26.x Setup** - Image: `quay.io/keycloak/keycloak:26.1` from Red Hat's container registry - Command: `start-dev --import-realm` (development mode with automatic realm import) - Realm import directory: `/opt/keycloak/data/import` mounted from `./infra/keycloak` - Database credentials: separate `keycloak` user with `keycloakpass` (not production-safe, dev only) - Health check uses curl to `/health/ready` endpoint (startup probe: 30s initial wait, 30 retries) 4. **Volume Management** - Named volume `postgres-data` for persistent PostgreSQL storage - Bind mount `./infra/keycloak` to `/opt/keycloak/data/import` for realm configuration - Bind mount `./infra/postgres` to `/docker-entrypoint-initdb.d` for database initialization 5. **Service Discovery & Networking** - All services on `app-network` bridge network - Service names act as hostnames: `postgres:5432` for PostgreSQL, `localhost:8080` for Keycloak UI - JDBC connection string in Keycloak: `jdbc:postgresql://postgres:5432/keycloak` 6. **Development vs Production** - This configuration is dev-only: hardcoded credentials, start-dev mode, default admin user - Security note: Keycloak admin credentials (admin/admin) and PostgreSQL passwords visible in plain text - No TLS/HTTPS, no resource limits, no restart policies beyond defaults - Future: Task 22 will add backend/frontend services to this compose file ### Configuration Files Created - **docker-compose.yml**: 68 lines, v3.9 format with postgres + keycloak services - **infra/postgres/init.sql**: Database initialization for workclub and keycloak databases - **infra/keycloak/realm-export.json**: Placeholder realm (will be populated by Task 3) ### Environment Constraints - Docker Compose CLI plugin not available in development environment - Configuration validated against v3.9 spec structure - YAML syntax verified via grep pattern matching - Full integration testing deferred to actual Docker deployment ### Patterns & Conventions - Use Alpine Linux images for smaller container footprints - Health checks with appropriate startup periods and retry counts - Ordered service startup via `depends_on` with health conditions - Named volumes for persistent state, bind mounts for configuration - Separate database users and passwords even in development (easier to migrate to secure configs) ### Gotchas to Avoid - Keycloak startup takes 20-30 seconds even in dev mode (don't reduce retries) - `/health/ready` is not the same as `/health/live` (use ready for startup confirmation) - PostgreSQL in Alpine doesn't include common extensions by default (not needed yet) - Keycloak password encoding: stored hashed in PostgreSQL, admin creds only in environment - Missing realm-export.json or empty directory causes Keycloak to start but import silently fails ### Next Dependencies - Task 3: Populate `realm-export.json` with actual Keycloak realm configuration - Task 7: PostgreSQL migrations for Entity Framework Core - Task 22: Add backend (Api, Application, Infrastructure services) and frontend to compose file --- ## Task 4: Domain Entities & State Machine (2026-03-03) ### Key Learnings 1. **TDD Approach Workflow** - Write tests FIRST (even when entities don't exist — LSP errors expected) - Create minimal entities to satisfy test requirements - All 12 tests passed on first run after implementation - This validates clean state machine design 2. **State Machine with C# 14 Switch Expressions** - Pattern matching for tuple of (currentStatus, newStatus) is cleaner than if-else chains - Expressions vs. traditional switch: more functional, concise, easier to verify all transitions - Chosen over Dictionary<(status, status), bool> because: - Easier to read and maintain - Compiler can verify exhaustiveness (with `_ => false` fallback) - No runtime lookup overhead - Clear inline state diagram 3. **Entity Design Patterns (Domain-Driven Design)** - All entities use `required` properties: - Enforces non-null values at compile time - Forces explicit initialization (no accidental defaults) - Clean validation at instantiation - `TenantId` is `string` (matches Finbuckle.MultiTenant.ITenantInfo.Id type) - Foreign keys use explicit `Guid` or `Guid?` (not navigation properties yet) - `RowVersion: byte[]?` for optimistic concurrency (EF Core `[Timestamp]` attribute in Task 7) 4. **DateTimeOffset vs DateTime** - Used DateTimeOffset for CreatedAt/UpdatedAt (includes timezone offset) - Better for multi-tenant global apps (know exact UTC moment) - .NET 10 standard for timestamp columns - Avoids timezone confusion across regions 5. **ITenantEntity Interface Pattern** - Explicit interface property (not EF shadow property) allows: - Easy data seeding in tests - LINQ queries without special knowledge - Clear contract in domain code - Marker interface only (no methods) — true DDD boundary 6. **Entity Lifecycle Simplicity** - No domain events (YAGNI for MVP) - No navigation properties (deferred to EF configuration in Task 7) - No validation attributes — EF Fluent API handles in Task 7 - State machine is only behavior (business rule enforcement) 7. **Test Structure for TDD** - Helper factory method `CreateWorkItem()` reduces repetition - AAA pattern (Arrange-Act-Assert) clear in test names - Arrange: Create entity with minimal valid state - Act: Call transition method - Assert: Verify state changed or exception thrown - xUnit [Fact] attributes sufficient (no [Theory] needed for now) 8. **Project Structure Observations** - Projects in `backend/` root, not `backend/src/` (deviation from typical convention but works) - Subdirectories: Entities/, Enums/, Interfaces/ (clean separation) - Test mirrors source structure: Domain tests in dedicated folder - Class1.cs stub removed before implementation ### Files Created - `WorkClub.Domain/Enums/SportType.cs` — 5 values - `WorkClub.Domain/Enums/ClubRole.cs` — 4 values - `WorkClub.Domain/Enums/WorkItemStatus.cs` — 5 values - `WorkClub.Domain/Interfaces/ITenantEntity.cs` — Marker interface - `WorkClub.Domain/Entities/Club.cs` — Basic aggregate root - `WorkClub.Domain/Entities/Member.cs` — User representation - `WorkClub.Domain/Entities/WorkItem.cs` — Task with state machine - `WorkClub.Domain/Entities/Shift.cs` — Volunteer shift - `WorkClub.Domain/Entities/ShiftSignup.cs` — Shift registration - `WorkClub.Tests.Unit/Domain/WorkItemStatusTests.cs` — 12 tests, all passing ### Build & Test Results - **Tests**: 12/12 passed (100%) - 5 valid transition tests ✓ - 5 invalid transition tests ✓ - 2 CanTransitionTo() method tests ✓ - **Build**: Release configuration successful - **Warnings**: Only Finbuckle version resolution (expected, no errors) ### Next Steps (Tasks 5-7) - Task 5: Next.js frontend (parallel) - Task 6: Kustomize deployment (parallel) - Task 7: EF Core DbContext with Fluent API configuration (blocks on these entities) - Add [Timestamp] attribute to RowVersion properties - Configure ITenantEntity filtering in DbContext - Set up relationships between entities - Configure PostgreSQL xmin concurrency token --- ## Task 6: Kubernetes Kustomize Base Manifests (2026-03-03) ### Key Learnings 1. **Kustomize vs Helm Trade-offs** - Kustomize chosen: lightweight, YAML-native, no templating language - Base + overlays pattern: separate environment-specific config from base - Base manifests use placeholders for image tags (`:latest`), resource limits (100m/256Mi requests) - Environment overlays (dev, staging, prod) override via patches/replacements 2. **Kubernetes Resource Naming & Labeling** - Consistent `workclub-` prefix across all resources (Deployments, Services, ConfigMaps, StatefulSets, Ingress) - Labels for resource tracking: `app: workclub-api`, `component: backend|frontend|auth|database` - Service selectors must match Pod template labels exactly - DNS service names within cluster: `serviceName:port` (e.g., `workclub-api:80`) 3. **.NET Health Probes (ASP.NET Core Health Checks)** - Three distinct probes with different semantics: - `startupProbe` (/health/startup): Initial boot, longer timeout (30s retries), prevents traffic until app fully initialized - `livenessProbe` (/health/live): Periodic health (15s), restart pod if fails continuously (3 failures) - `readinessProbe` (/health/ready): Pre-request check (10s), removes pod from service on failure (2 failures) - Startup probe MUST complete before liveness/readiness are checked - All three probes return `200 OK` for healthy status 4. **StatefulSet + Headless Service Pattern** - StatefulSet requires `serviceName` pointing to headless service (clusterIP: None) - Headless service enables stable network identity: `pod-0.serviceName.namespace.svc.cluster.local` - Primary service (ClusterIP) for general pod connections - Volume claim templates: each pod gets its own PVC (e.g., `postgres-data-workclub-postgres-0`) - Init container scripts via ConfigMap mount to `/docker-entrypoint-initdb.d` 5. **PostgreSQL StatefulSet Configuration** - Image: `postgres:16-alpine` (lightweight, 150MB vs 400MB+) - Health check: `pg_isready -U app -d workclub` (simple, fast, reliable) - Data persistence: volumeClaimTemplate with 10Gi storage, `standard` storageClassName (overrideable in overlay) - Init script creates both `workclub` (app) and `keycloak` databases + users in single ConfigMap 6. **Keycloak 26.x Production Mode** - Image: `quay.io/keycloak/keycloak:26.1` (Red Hat official registry) - Command: `start` (production mode, not `start-dev`) - Database: PostgreSQL via `KC_DB=postgres` + `KC_DB_URL_HOST=workclub-postgres` - Probes: `/health/ready` (readiness), `/health/live` (liveness) - Hostname: `KC_HOSTNAME_STRICT=false` in dev (allows any Host header) - Proxy: `KC_PROXY=edge` for behind reverse proxy (Ingress) 7. **Ingress Path-Based Routing** - Single ingress rule: `workclub-ingress` with path-based routing - Frontend: path `/` → `workclub-frontend:80` (pathType: Prefix) - Backend: path `/api` → `workclub-api:80` (pathType: Prefix) - Host: `localhost` (overrideable per environment) - TLS: deferred to production overlay (cert-manager, letsencrypt) 8. **ConfigMap Strategy for Non-Sensitive Configuration** - Central `workclub-config` ConfigMap: - `log-level: Information` - `cors-origins: http://localhost:3000` - `api-base-url: http://workclub-api` - `keycloak-url: http://workclub-keycloak` - `keycloak-realm: workclub` - Database host/port/name - Sensitive values (passwords, connection strings) → Secrets (not in base) - Environment-specific overrides in dev/prod overlays (CORS_ORIGINS changes) 9. **Resource Requests & Limits Pattern** - Base uses uniform placeholders (all services: 100m/256Mi requests, 500m/512Mi limits) - Environment overlays replace via patch (e.g., prod: 500m/2Gi) - Prevents resource contention in shared clusters - Allows gradual scaling experiments without manifests changes 10. **Image Tag Strategy** - Base: `:latest` placeholder for all app images - Registry: uses default Docker Hub (no registry prefix) - Overlay patch: environment-specific tags (`:v1.2.3`, `:latest-dev`, `:sha-abc123`) - Image pull policy: `IfNotPresent` (caching optimization for stable envs) ### Architecture Decisions - **Why Kustomize over Helm**: Plan explicitly avoids Helm (simpler YAML, no new DSL, easier Git diffs) - **Why base + overlays**: Separation of concerns — base is declarative truth, overlays add environment context - **Why two Postgres services**: Headless for StatefulSet DNS (stable identity), Primary for app connections (load balancing) - **Why both startup + liveness probes**: Prevents restart loops during slow startup (Java/Keycloak can take 20+ seconds) - **Why ConfigMap for init.sql**: Immutable config, easier than baked-into-image, updateable per environment ### Gotchas to Avoid - Forgetting `serviceName` in StatefulSet causes pod DNS discovery failure (critical for Postgres) - Missing headless service's `publishNotReadyAddresses: true` prevents pod-to-pod startup communication - Keycloak startup probe timeout too short (<15s retries) causes premature restart loops - `.NET health endpoints require HttpGet, not TCP probes (TCP only checks port, not app readiness) - Ingress path `/api` must use `pathType: Prefix` to catch `/api/*` routes ### Next Steps - Task 25: Create dev overlay (env-specific values, dev-db.postgres.svc, localhost ingress) - Task 26: Create prod overlay (TLS config, resource limits, replica counts, PDB) - Task 27: Add cert-manager + Let's Encrypt to prod - Future: Network policies, pod disruption budgets, HPA (deferred to Wave 2) --- ## Task 5: Next.js 15 Project Initialization (2026-03-03) ### Key Learnings 1. **Next.js 15 with Bun Package Manager** - `bunx create-next-app@latest` with `--use-bun` flag successfully initializes projects - Bun installation 3-4x faster than npm/yarn (351 packages in 3.4s) - Next.js 16.1.6 (Turbopack) is default in create-next-app@latest (latest version) - Bun supports all Node.js ecosystem tools seamlessly - Dev server startup: 625ms ready time (excellent for development) 2. **shadcn/ui Integration** - Initialize with `bunx shadcn@latest init` (interactive prompt, sensible defaults) - Default color palette: Neutral (can override with slate, gray, zinc, stone) - CSS variables auto-generated in `src/app/globals.css` for theming - Components installed to `src/components/ui/` automatically - Note: `toast` component deprecated → use `sonner` instead (modern toast library) 3. **Standalone Output Configuration** - Set `output: 'standalone'` in `next.config.ts` for Docker deployments - Generates `.next/standalone/` with self-contained server.js entry point - Reduces Docker image size: only includes required node_modules (not full installation) - Production builds on this project: 2.9s compile, 240.4ms static page generation - Standalone directory structure: `.next/`, `node_modules/`, `server.js`, `package.json` 4. **TypeScript Path Aliases** - `@/*` → `./src/*` pre-configured in `tsconfig.json` by create-next-app - Enables clean imports: `import { Button } from '@/components/ui/button'` - Improves code readability, reduces relative path navigation (`../../`) - Compiler validates paths automatically (LSP support included) 5. **Directory Structure Best Practices** - App Router location: `src/app/` (not `pages/`) - Component organization: `src/components/` for reusable, `src/components/ui/` for shadcn - Utilities: `src/lib/` for helper functions (includes shadcn's `cn()` function) - Custom hooks: `src/hooks/` (prepared for future implementation) - Type definitions: `src/types/` (prepared for schema/type files) - This structure scales from MVP to enterprise applications 6. **Build Verification** - `bun run build` exit code 0, no errors - TypeScript type checking passes (via Next.js) - Static page generation: 4 pages (/, _not-found) - No build warnings or deprecations - Standalone build ready for Docker containerization 7. **Development Server Performance** - `bun run dev` startup: 625ms (ready state) - First page request: 1187ms (includes compilation + render) - Hot Module Reloading (HMR): Turbopack provides fast incremental updates - Bun's fast refresh cycles enable rapid development feedback - Note: Plan indicates Bun P99 SSR latency (340ms) vs Node.js (120ms), so production deployment will use Node.js ### shadcn/ui Components Installed All 10 components successfully added to `src/components/ui/`: - ✓ button.tsx — Base button component with variants (primary, secondary, etc.) - ✓ card.tsx — Card layout container (Card, CardHeader, CardFooter, etc.) - ✓ badge.tsx — Status badges with color variants - ✓ input.tsx — Form input field with placeholder and error support - ✓ label.tsx — Form label with accessibility attributes - ✓ select.tsx — Dropdown select with options (Radix UI based) - ✓ dialog.tsx — Modal dialog component (Alert Dialog pattern) - ✓ dropdown-menu.tsx — Context menu/dropdown menu (Radix UI based) - ✓ table.tsx — Data table with thead, tbody, rows - ✓ sonner.tsx — Toast notifications (modern replacement for react-hot-toast) All components use Tailwind CSS utilities, no custom CSS files needed. ### Environment Variables Configuration Created `.env.local.example` (committed to git) with development defaults: ``` NEXT_PUBLIC_API_URL=http://localhost:5000 # Backend API endpoint NEXTAUTH_URL=http://localhost:3000 # NextAuth callback URL NEXTAUTH_SECRET=dev-secret-change-me # Session encryption (Task 10) KEYCLOAK_ISSUER=http://localhost:8080/realms/workclub # OAuth2 discovery KEYCLOAK_CLIENT_ID=workclub-app # Keycloak client ID KEYCLOAK_CLIENT_SECRET= # Placeholder (Task 3 fills in) ``` Pattern: `.env.local.example` is version-controlled, `.env.local` is gitignored per `.gitignore`. ### Dependencies Installed ```json { "dependencies": { "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3" }, "devDependencies": { "@tailwindcss/postcss": "4.2.1", "@types/node": "20.19.35", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "eslint": "9.39.3", "eslint-config-next": "16.1.6", "tailwindcss": "4.2.1", "typescript": "5.9.3" } } ``` Note: Intentionally minimal dependencies for MVP. NextAuth.js added in Task 10. ### Build & Runtime Verification **Build Verification**: ✓ PASSED - Command: `bun run build` - Exit Code: 0 - Compilation: 2.9s (Turbopack) - TypeScript: No errors - Static Generation: 4 pages in 240.4ms - Output: `.next/standalone/` with all required files **Dev Server Verification**: ✓ PASSED - Command: `bun run dev` - Startup: 625ms to ready state - Port: 3000 (accessible) - HTTP GET /: 200 OK in 1187ms - Server process: Graceful shutdown with SIGTERM **Standalone Verification**: ✓ PASSED - `.next/standalone/server.js`: 6.55 KB entry point - `.next/standalone/node_modules/`: Self-contained dependencies - `.next/standalone/package.json`: Runtime configuration - `.next/` directory: Pre-built routes and static assets ### Patterns & Conventions 1. **Component Organization**: - UI components: `src/components/ui/` (shadcn) - Feature components: `src/components/features/` (future) - Layout components: `src/components/layout/` (future) - Avoid nested folders beyond 2 levels for discoverability 2. **TypeScript Strict Mode**: - `tsconfig.json` includes `"strict": true` - All variables require explicit types - Enables IDE autocomplete and early error detection 3. **Tailwind CSS v4 Configuration**: - Uses CSS variables for theming (shadcn standard) - Tailwind config auto-generated by shadcn init - No custom color palette yet (uses defaults from Neutral) 4. **Git Strategy**: - `.env.local.example` is committed (template for developers) - `.env.local` is in `.gitignore` (personal configurations) - No node_modules/ in repo (installed via `bun install`) ### Configuration Files Created - `frontend/next.config.ts` — Minimal, standalone output enabled - `frontend/tsconfig.json` — Path aliases, strict TypeScript mode - `frontend/.env.local.example` — Environment variable template - `frontend/components.json` — shadcn/ui configuration - `frontend/tailwind.config.ts` — Tailwind CSS configuration with Tailwind v4 - `frontend/postcss.config.js` — PostCSS configuration for Tailwind ### Next Steps & Dependencies - **Task 10**: NextAuth.js integration - Adds `next-auth` dependency - Creates `src/app/api/auth/[...nextauth]/route.ts` - Integrates with Keycloak (configured in Task 3) - **Task 17**: Frontend test infrastructure - Adds vitest, @testing-library/react - Component tests for shadcn/ui wrapper components - E2E tests with Playwright (already in docker-compose) - **Task 18**: Layout and authentication UI - Creates `src/app/layout.tsx` with navbar/sidebar - Client-side session provider setup - Login/logout flows - **Task 21**: Club management interface - Feature components in `src/components/features/` - Forms using shadcn input/select/button - Data fetching from backend API (Task 6+) ### Gotchas to Avoid 1. **Bun vs Node.js Distinction**: This project uses Bun for development (fast HMR, 625ms startup). Production deployment will use Node.js due to P99 latency concerns (documented in plan). 2. **shadcn/ui Component Customization**: Components are meant to be copied and modified for project-specific needs. Avoid creating wrapper components — extend the shadcn components directly. 3. **Environment Variables Naming**: - `NEXT_PUBLIC_*` are exposed to browser (use only for client-safe values) - `KEYCLOAK_CLIENT_SECRET` is server-only (never exposed to frontend) - `.env.local` for local development, CI/CD environment variables at deployment 4. **Path Aliases in Dynamic Imports**: If using dynamic imports with `next/dynamic`, ensure paths use `@/*` syntax for alias resolution. 5. **Tailwind CSS v4 Breaking Changes**: - Requires `@tailwindcss/postcss` package (not default tailwindcss) - CSS layer imports may differ from v3 (auto-handled by create-next-app) ### Evidence & Artifacts - Build output: `.sisyphus/evidence/task-5-nextjs-build.txt` - Dev server output: `.sisyphus/evidence/task-5-dev-server.txt` - Git commit: `chore(frontend): initialize Next.js project with Tailwind and shadcn/ui` ## Task 3: Keycloak Realm Configuration (2026-03-03) ### Key Learnings 1. **Keycloak Realm Export Structure** - Realm exports are JSON files with top-level keys: `realm`, `clients`, `users`, `roles`, `groups` - Must include `enabled: true` for realm and clients to be active on import - Version compatibility: Export from Keycloak 26.x is compatible with 26.x imports - Import command: `start-dev --import-realm` (Docker volume mount required) 2. **Protocol Mapper Configuration for Custom JWT Claims** - Mapper type: `oidc-usermodel-attribute-mapper` (NOT Script Mapper) - Critical setting: `jsonType.label: JSON` ensures claim is parsed as JSON object (not string) - User attribute: `clubs` (custom attribute on user entity) - Token claim name: `clubs` (appears in JWT payload) - Must include in: ID token, access token, userinfo endpoint (all three flags set to true) - Applied to both clients: workclub-api and workclub-app (defined in client protocolMappers array) 3. **Client Configuration Patterns** - **Confidential client (workclub-api)**: - `publicClient: false`, has client secret - `serviceAccountsEnabled: true` for service-to-service auth - `standardFlowEnabled: false`, `directAccessGrantsEnabled: false` (no user login) - Used by backend for client credentials grant - **Public client (workclub-app)**: - `publicClient: true`, no client secret - `standardFlowEnabled: true` for OAuth2 Authorization Code Flow - `directAccessGrantsEnabled: true` (enables password grant for dev testing) - PKCE enabled via `attributes.pkce.code.challenge.method: S256` - Redirect URIs: `http://localhost:3000/*` (wildcard for dev) - Web origins: `http://localhost:3000` (CORS configuration) 4. **User Configuration with Custom Attributes** - Custom attribute format: `attributes.clubs: ["{\"club-1-uuid\": \"admin\"}"]` - Attribute value is array of strings (even for single value) - JSON must be escaped as string in user attributes - Protocol mapper will parse this string as JSON when generating JWT claim - Users must have: `enabled: true`, `emailVerified: true`, no `requiredActions: []` 5. **Password Hashing in Realm Exports** - Algorithm: `pbkdf2-sha512` (Keycloak default) - Hash iterations: 210000 (high security for dev environment) - Credentials structure includes: `hashedSaltedValue`, `salt`, `hashIterations`, `algorithm` - Password: `testpass123` (all test users use same password for simplicity) - Note: Hashed values in this export are PLACEHOLDER — Keycloak will generate real hashes on first user creation 6. **Multi-Tenant Club Membership Data Model** - Format: `{"": ""}` - Example: `{"club-1-uuid": "admin", "club-2-uuid": "member"}` - Keys: Club UUIDs (tenant identifiers) - Values: Role strings (admin, manager, member, viewer) - Users can belong to multiple clubs with different roles in each - Placeholder UUIDs used: `club-1-uuid`, `club-2-uuid` (real UUIDs created in Task 11 seed data) 7. **Test User Scenarios** - **admin@test.com**: Multi-club admin (admin in club-1, member in club-2) - **manager@test.com**: Single club manager (manager in club-1) - **member1@test.com**: Multi-club member (member in both clubs) - **member2@test.com**: Single club member (member in club-1) - **viewer@test.com**: Read-only viewer (viewer in club-1) - Covers all role types and single/multi-club scenarios 8. **Docker Environment Configuration** - Keycloak 26.1 runs in Docker container - Realm import via volume mount: `./infra/keycloak:/opt/keycloak/data/import` - Health check endpoint: `/health/ready` - Token endpoint: `/realms/workclub/protocol/openid-connect/token` - Admin credentials: `admin/admin` (for Keycloak admin console) 9. **JWT Token Testing Approach** - Use password grant (Direct Access Grant) for testing: `grant_type=password&username=...&password=...&client_id=workclub-app` - Decode JWT: Split on `.`, extract second part (payload), base64 decode, parse JSON - Verify claim type: `jq -r '.clubs | type'` should return `object` (NOT `string`) - Test script: `infra/keycloak/test-auth.sh` automates this verification 10. **Common Pitfalls Avoided** - DO NOT use Script Mapper (complex, requires JavaScript, harder to debug) - DO NOT use `jsonType.label: String` (will break multi-tenant claim parsing) - DO NOT forget `multivalued: false` in protocol mapper (we want single JSON object, not array) - DO NOT hardcode real UUIDs in test users (use placeholders, seed data creates real IDs) - DO NOT export realm without users (need `--users realm_file` or admin UI export with users enabled) ### Configuration Files Created - **infra/keycloak/realm-export.json**: Complete realm configuration (8.9 KB) - **infra/keycloak/test-auth.sh**: Automated verification script for JWT claims - **.sisyphus/evidence/task-3-verification.txt**: Detailed verification documentation - **.sisyphus/evidence/task-3-user-auth.txt**: User authentication results (placeholder) - **.sisyphus/evidence/task-3-jwt-claims.txt**: JWT claim structure documentation (placeholder) ### Docker Environment Issue - Colima (Docker runtime on macOS) failed to start with VZ driver error - Verification deferred until Docker environment is available - All configuration files are complete and JSON-validated - Test script is ready for execution when Docker is running ### Next Phase Considerations - Task 8 (Finbuckle) will consume `clubs` claim to implement tenant resolution - Task 9 (JWT auth middleware) will validate tokens from Keycloak - Task 10 (NextAuth) will use workclub-app client for frontend authentication - Task 11 (seed data) will replace placeholder UUIDs with real club IDs - Production deployment will need: real client secrets, HTTPS redirect URIs, proper password policies --- ## Task 5: Next.js 15 Project Initialization (2026-03-03) ### Key Learnings 1. **Next.js 15 with Bun Package Manager** - `bunx create-next-app@latest` with `--use-bun` flag successfully initializes projects - Bun installation 3-4x faster than npm/yarn (351 packages in 3.4s) - Next.js 16.1.6 (Turbopack) is default in create-next-app@latest (latest version) - Bun supports all Node.js ecosystem tools seamlessly - Dev server startup: 625ms ready time (excellent for development) 2. **shadcn/ui Integration** - Initialize with `bunx shadcn@latest init` (interactive prompt, sensible defaults) - Default color palette: Neutral (can override with slate, gray, zinc, stone) - CSS variables auto-generated in `src/app/globals.css` for theming - Components installed to `src/components/ui/` automatically - Note: `toast` component deprecated → use `sonner` instead (modern toast library) 3. **Standalone Output Configuration** - Set `output: 'standalone'` in `next.config.ts` for Docker deployments - Generates `.next/standalone/` with self-contained server.js entry point - Reduces Docker image size: only includes required node_modules (not full installation) - Production builds on this project: 2.9s compile, 240.4ms static page generation - Standalone directory structure: `.next/`, `node_modules/`, `server.js`, `package.json` 4. **TypeScript Path Aliases** - `@/*` → `./src/*` pre-configured in `tsconfig.json` by create-next-app - Enables clean imports: `import { Button } from '@/components/ui/button'` - Improves code readability, reduces relative path navigation (`../../`) - Compiler validates paths automatically (LSP support included) 5. **Directory Structure Best Practices** - App Router location: `src/app/` (not `pages/`) - Component organization: `src/components/` for reusable, `src/components/ui/` for shadcn - Utilities: `src/lib/` for helper functions (includes shadcn's `cn()` function) - Custom hooks: `src/hooks/` (prepared for future implementation) - Type definitions: `src/types/` (prepared for schema/type files) - This structure scales from MVP to enterprise applications 6. **Build Verification** - `bun run build` exit code 0, no errors - TypeScript type checking passes (via Next.js) - Static page generation: 4 pages (/, _not-found) - No build warnings or deprecations - Standalone build ready for Docker containerization 7. **Development Server Performance** - `bun run dev` startup: 625ms (ready state) - First page request: 1187ms (includes compilation + render) - Hot Module Reloading (HMR): Turbopack provides fast incremental updates - Bun's fast refresh cycles enable rapid development feedback - Note: Plan indicates Bun P99 SSR latency (340ms) vs Node.js (120ms), so production deployment will use Node.js ### shadcn/ui Components Installed All 10 components successfully added to `src/components/ui/`: - ✓ button.tsx — Base button component with variants (primary, secondary, etc.) - ✓ card.tsx — Card layout container (Card, CardHeader, CardFooter, etc.) - ✓ badge.tsx — Status badges with color variants - ✓ input.tsx — Form input field with placeholder and error support - ✓ label.tsx — Form label with accessibility attributes - ✓ select.tsx — Dropdown select with options (Radix UI based) - ✓ dialog.tsx — Modal dialog component (Alert Dialog pattern) - ✓ dropdown-menu.tsx — Context menu/dropdown menu (Radix UI based) - ✓ table.tsx — Data table with thead, tbody, rows - ✓ sonner.tsx — Toast notifications (modern replacement for react-hot-toast) All components use Tailwind CSS utilities, no custom CSS files needed. ### Environment Variables Configuration Created `.env.local.example` (committed to git) with development defaults: ``` NEXT_PUBLIC_API_URL=http://localhost:5000 # Backend API endpoint NEXTAUTH_URL=http://localhost:3000 # NextAuth callback URL NEXTAUTH_SECRET=dev-secret-change-me # Session encryption (Task 10) KEYCLOAK_ISSUER=http://localhost:8080/realms/workclub # OAuth2 discovery KEYCLOAK_CLIENT_ID=workclub-app # Keycloak client ID KEYCLOAK_CLIENT_SECRET= # Placeholder (Task 3 fills in) ``` Pattern: `.env.local.example` is version-controlled, `.env.local` is gitignored per `.gitignore`. ### Dependencies Installed ```json { "dependencies": { "next": "16.1.6", "react": "19.2.3", "react-dom": "19.2.3" }, "devDependencies": { "@tailwindcss/postcss": "4.2.1", "@types/node": "20.19.35", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "eslint": "9.39.3", "eslint-config-next": "16.1.6", "tailwindcss": "4.2.1", "typescript": "5.9.3" } } ``` Note: Intentionally minimal dependencies for MVP. NextAuth.js added in Task 10. ### Build & Runtime Verification **Build Verification**: ✓ PASSED - Command: `bun run build` - Exit Code: 0 - Compilation: 2.9s (Turbopack) - TypeScript: No errors - Static Generation: 4 pages in 240.4ms - Output: `.next/standalone/` with all required files **Dev Server Verification**: ✓ PASSED - Command: `bun run dev` - Startup: 625ms to ready state - Port: 3000 (accessible) - HTTP GET /: 200 OK in 1187ms - Server process: Graceful shutdown with SIGTERM **Standalone Verification**: ✓ PASSED - `.next/standalone/server.js`: 6.55 KB entry point - `.next/standalone/node_modules/`: Self-contained dependencies - `.next/standalone/package.json`: Runtime configuration - `.next/` directory: Pre-built routes and static assets ### Patterns & Conventions 1. **Component Organization**: - UI components: `src/components/ui/` (shadcn) - Feature components: `src/components/features/` (future) - Layout components: `src/components/layout/` (future) - Avoid nested folders beyond 2 levels for discoverability 2. **TypeScript Strict Mode**: - `tsconfig.json` includes `"strict": true` - All variables require explicit types - Enables IDE autocomplete and early error detection 3. **Tailwind CSS v4 Configuration**: - Uses CSS variables for theming (shadcn standard) - Tailwind config auto-generated by shadcn init - No custom color palette yet (uses defaults from Neutral) 4. **Git Strategy**: - `.env.local.example` is committed (template for developers) - `.env.local` is in `.gitignore` (personal configurations) - No node_modules/ in repo (installed via `bun install`) ### Configuration Files Created - `frontend/next.config.ts` — Minimal, standalone output enabled - `frontend/tsconfig.json` — Path aliases, strict TypeScript mode - `frontend/.env.local.example` — Environment variable template - `frontend/components.json` — shadcn/ui configuration - `frontend/tailwind.config.ts` — Tailwind CSS configuration with Tailwind v4 - `frontend/postcss.config.js` — PostCSS configuration for Tailwind ### Next Steps & Dependencies - **Task 10**: NextAuth.js integration - Adds `next-auth` dependency - Creates `src/app/api/auth/[...nextauth]/route.ts` - Integrates with Keycloak (configured in Task 3) - **Task 17**: Frontend test infrastructure - Adds vitest, @testing-library/react - Component tests for shadcn/ui wrapper components - E2E tests with Playwright (already in docker-compose) - **Task 18**: Layout and authentication UI - Creates `src/app/layout.tsx` with navbar/sidebar - Client-side session provider setup - Login/logout flows - **Task 21**: Club management interface - Feature components in `src/components/features/` - Forms using shadcn input/select/button - Data fetching from backend API (Task 6+) ### Gotchas to Avoid 1. **Bun vs Node.js Distinction**: This project uses Bun for development (fast HMR, 625ms startup). Production deployment will use Node.js due to P99 latency concerns (documented in plan). 2. **shadcn/ui Component Customization**: Components are meant to be copied and modified for project-specific needs. Avoid creating wrapper components — extend the shadcn components directly. 3. **Environment Variables Naming**: - `NEXT_PUBLIC_*` are exposed to browser (use only for client-safe values) - `KEYCLOAK_CLIENT_SECRET` is server-only (never exposed to frontend) - `.env.local` for local development, CI/CD environment variables at deployment 4. **Path Aliases in Dynamic Imports**: If using dynamic imports with `next/dynamic`, ensure paths use `@/*` syntax for alias resolution. 5. **Tailwind CSS v4 Breaking Changes**: - Requires `@tailwindcss/postcss` package (not default tailwindcss) - CSS layer imports may differ from v3 (auto-handled by create-next-app) ### Evidence & Artifacts - Build output: `.sisyphus/evidence/task-5-nextjs-build.txt` - Dev server output: `.sisyphus/evidence/task-5-dev-server.txt` - Git commit: `chore(frontend): initialize Next.js project with Tailwind and shadcn/ui` --- ## Task 11: Seed Data Script (2026-03-03) ### Key Learnings 1. **Idempotent Seeding Pattern** - Check existence before insert: `if (!context.Clubs.Any())` - Ensures safe re-runs (no duplicate data on restarts) - Applied to each entity type separately - SaveChangesAsync called after each entity batch 2. **Deterministic GUID Generation** - Used MD5.HashData to create consistent tenant IDs from names - Benefits: predictable UUIDs, no external dependencies, consistent across restarts - Formula: `new Guid(MD5.HashData(Encoding.UTF8.GetBytes(name)).Take(16).ToArray())` - Matches placeholder UUIDs in Keycloak test users from Task 3 3. **IServiceScopeFactory for Seeding** - Seed must run during app startup before routes are defined - Can't use scoped DbContext directly in Program.cs - Solution: Inject IServiceScopeFactory, create scope in SeedAsync method - Creates fresh DbContext per seeding operation 4. **Development-Only Execution Guard** - Seed runs only in development: `if (app.Environment.IsDevelopment())` - Production environments skip seeding automatically - Pattern: await inside if block (not a blocking operation) 5. **Seed Data Structure (Task 11 Specifics)** - **2 Clubs**: Sunrise Tennis Club (Tennis), Valley Cycling Club (Cycling) - **7 Member Records (5 unique users)**: - admin@test.com: Admin/Member (Tennis/Cycling) - manager@test.com: Manager (Tennis) - member1@test.com: Member/Member (Tennis/Cycling) - member2@test.com: Member (Tennis) - viewer@test.com: Viewer (Tennis) - **8 Work Items**: 5 in Tennis Club (all states), 3 in Cycling Club - **5 Shifts**: 3 in Tennis Club (past/today/future), 2 in Cycling Club (today/future) - **3-4 Shift Signups**: Select members signed up for shifts 6. **Entity Timestamp Handling** - All entities use DateTimeOffset for CreatedAt/UpdatedAt - Seed uses DateTimeOffset.UtcNow for current time - Shift dates use .Date.ToLocalTime() for proper date conversion without time component - Maintains UTC consistency for multi-tenant data 7. **Multi-Tenant Tenant ID Assignment** - Each Club has its own TenantId (deterministic from club name) - Child entities (Members, WorkItems, Shifts) get TenantId from parent club - ShiftSignups get TenantId from shift's club - Critical for RLS filtering (Task 7) to work correctly 8. **Work Item State Machine Coverage** - Seed covers all 5 states: Open, Assigned, InProgress, Review, Done - Maps to business flow: Open → Assigned → InProgress → Review → Done - Not all transitions are valid (enforced by state machine from Task 4) - Provides realistic test data for state transitions 9. **Shift Capacity and Sign-ups** - Shift.Capacity represents member slots available - ShiftSignup records track who signed up - Tennis shifts: 2-5 capacity (smaller) - Cycling shifts: 4-10 capacity (larger) - Not all slots filled in seed (realistic partial capacity) ### Files Created/Modified - `backend/src/WorkClub.Infrastructure/Seed/SeedDataService.cs` — Full seeding logic (445 lines) - `backend/src/WorkClub.Api/Program.cs` — Added SeedDataService registration and startup call ### Implementation Details **SeedDataService Constructor:** ```csharp public SeedDataService(IServiceScopeFactory serviceScopeFactory) { _serviceScopeFactory = serviceScopeFactory; } ``` **SeedAsync Pattern:** ```csharp public async Task SeedAsync() { using var scope = _serviceScopeFactory.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); // Each entity type checked separately if (!context.Clubs.Any()) { /* seed clubs */ } if (!context.Members.Any()) { /* seed members */ } // etc. } ``` **Program.cs Seed Call:** ```csharp if (app.Environment.IsDevelopment()) { using var scope = app.Services.CreateScope(); var seedService = scope.ServiceProvider.GetRequiredService(); await seedService.SeedAsync(); } ``` ### Patterns & Conventions 1. **Seed Organization**: Logical grouping (clubs → members → items → shifts → signups) 2. **Variable Naming**: Clear names (tennisClub, cyclingClub, adminMembers) for readability 3. **Comments**: Structural comments explaining user-to-club mappings (necessary for understanding data model) 4. **Deterministic vs Random**: GUIDs for club IDs are deterministic, but Member/WorkItem/Shift IDs are random (not used in lookups) ### Testing Approach The seed is designed for: - **Local development**: Full test data available on first run - **Restarts**: Safe idempotent re-runs - **Manual testing**: All roles and states represented - **QA**: Predictable data structure for integration tests ### Verification Strategy Post-implementation checks (in separate QA section): 1. Docker Compose startup with seed execution 2. Database queries via `docker compose exec postgres psql` 3. Verify counts: Clubs=2, Members≥7, WorkItems=8, Shifts=5 4. Re-run and verify no duplicates (idempotency) ### Next Steps (Task 12+) - Task 12 will create API endpoints to query this seed data - Task 22 will perform manual QA with this populated database - Production deployments will skip seeding via environment check ### Gotchas Avoided - Did NOT use DateTime (used DateTimeOffset for timezone awareness) - Did NOT hard-code random GUIDs (used deterministic MD5-based) - Did NOT forget idempotent checks (each entity type guarded) - Did NOT seed in all environments (guarded with IsDevelopment()) - Did NOT create DbContext directly (used IServiceScopeFactory) --- ## Task 12: Backend Test Infrastructure (xUnit + Testcontainers + WebApplicationFactory) (2026-03-03) ### Key Learnings 1. **Test Infrastructure Architecture** - `CustomWebApplicationFactory`: Extends `WebApplicationFactory` for integration testing - PostgreSQL container via Testcontainers (postgres:16-alpine image) - Test authentication handler replaces JWT auth in tests - `IntegrationTestBase`: Base class for all integration tests with auth helpers - `DatabaseFixture`: Collection fixture for shared container lifecycle 2. **Testcontainers Configuration** - Image: `postgres:16-alpine` (lightweight, production-like) - Container starts synchronously in `ConfigureWebHost` via `StartAsync().GetAwaiter().GetResult()` - Connection string from `_postgresContainer.GetConnectionString()` - Database setup: `db.Database.EnsureCreated()` (faster than migrations for tests) - Disposed via `ValueTask DisposeAsync()` in factory cleanup 3. **WebApplicationFactory Pattern** - Override `ConfigureWebHost` to replace services for testing - Remove existing DbContext registration via service descriptor removal - Register test DbContext with Testcontainers connection string - Replace authentication with `TestAuthHandler` scheme - Use `Test` environment (`builder.UseEnvironment("Test")`) 4. **Test Authentication Pattern** - `TestAuthHandler` extends `AuthenticationHandler` - Reads claims from custom headers: `X-Test-Clubs`, `X-Test-Email` - No real JWT validation — all requests authenticated if handler installed - Test methods call `AuthenticateAs(email, clubs)` to set claims - Tenant header via `SetTenant(tenantId)` sets `X-Tenant-Id` 5. **IntegrationTestBase Design** - Implements `IClassFixture>` for shared factory - Implements `IAsyncLifetime` for test setup/teardown hooks - Provides pre-configured `HttpClient` from factory - Helper: `AuthenticateAs(email, clubs)` → adds JSON-serialized clubs to headers - Helper: `SetTenant(tenantId)` → adds tenant ID to headers - Derived test classes inherit all infrastructure automatically 6. **DatabaseFixture Pattern** - Collection fixture via `[CollectionDefinition("Database collection")]` - Implements `ICollectionFixture` for sharing across tests - Empty implementation (container managed by factory, not fixture) - Placeholder for future data reset logic (truncate tables between tests) 7. **Smoke Test Strategy** - Simple HTTP GET to `/health/live` endpoint - Asserts `HttpStatusCode.OK` response - Verifies entire stack: Testcontainers, factory, database, application startup - Fast feedback: if smoke test passes, infrastructure works 8. **Health Endpoints Configuration** - Already present in `Program.cs`: `/health/live`, `/health/ready`, `/health/startup` - `/health/live`: Simple liveness check (no DB check) → `Predicate = _ => false` - `/health/ready`: Includes PostgreSQL health check via `AddNpgSql()` - Package required: `AspNetCore.HealthChecks.NpgSql` (version 9.0.0) 9. **Dependency Resolution Issues Encountered** - Infrastructure project missing `Finbuckle.MultiTenant.AspNetCore` package - Added via `dotnet add package Finbuckle.MultiTenant.AspNetCore --version 10.0.3` - TenantInfo type from Finbuckle namespace (not custom type) - Existing project had incomplete package references (not task-specific issue) 10. **Build vs EnsureCreated for Tests** - Used `db.Database.EnsureCreated()` instead of `db.Database.Migrate()` - Reason: No migrations exist yet (created in later task) - `EnsureCreated()` creates schema from entity configurations directly - Faster than migrations for test databases (no history table) - Note: `EnsureCreated()` and `Migrate()` are mutually exclusive ### Files Created - `backend/WorkClub.Tests.Integration/Infrastructure/CustomWebApplicationFactory.cs` (59 lines) - `backend/WorkClub.Tests.Integration/Infrastructure/TestAuthHandler.cs` (42 lines) - `backend/WorkClub.Tests.Integration/Infrastructure/IntegrationTestBase.cs` (35 lines) - `backend/WorkClub.Tests.Integration/Infrastructure/DatabaseFixture.cs` (18 lines) - `backend/WorkClub.Tests.Integration/SmokeTests.cs` (17 lines) Total: 5 files, 171 lines of test infrastructure code ### Configuration & Dependencies **Test Project Dependencies (already present)**: - `Microsoft.AspNetCore.Mvc.Testing` (10.0.0) — WebApplicationFactory - `Testcontainers.PostgreSql` (3.7.0) — PostgreSQL container - `xunit` (2.9.3) — Test framework - `Dapper` (2.1.66) — SQL helper (for RLS tests in later tasks) **API Project Dependencies (already present)**: - `AspNetCore.HealthChecks.NpgSql` (9.0.0) — PostgreSQL health check - Health endpoints configured in `Program.cs` lines 75-81 **Infrastructure Project Dependencies (added)**: - `Finbuckle.MultiTenant.AspNetCore` (10.0.3) — Multi-tenancy support (previously missing) ### Patterns & Conventions 1. **Test Namespace**: `WorkClub.Tests.Integration.Infrastructure` for test utilities 2. **Test Class Naming**: `SmokeTests`, `*Tests` suffix for test classes 3. **Factory Type Parameter**: `CustomWebApplicationFactory` (Program from Api project) 4. **Test Method Naming**: `MethodName_Scenario_ExpectedResult` (e.g., `HealthCheck_ReturnsOk`) 5. **Async Lifecycle**: All test infrastructure implements `IAsyncLifetime` for async setup/teardown ### Testcontainers Best Practices - **Container reuse**: Factory instance shared across test class via `IClassFixture` - **Startup blocking**: Use `.GetAwaiter().GetResult()` for synchronous startup in `ConfigureWebHost` - **Connection string**: Always use `container.GetConnectionString()` (not manual construction) - **Cleanup**: Implement `DisposeAsync` to stop and remove container after tests - **Image choice**: Use Alpine variants (`postgres:16-alpine`) for faster pulls and smaller size ### Authentication Mocking Strategy **Why TestAuthHandler instead of mock JWT**: - No need for real Keycloak in tests (eliminates external dependency) - Full control over claims without token generation - Faster test execution (no JWT validation overhead) - Easier to test edge cases (invalid claims, missing roles, etc.) - Tests focus on application logic, not auth infrastructure **How it works**: 1. Test calls `AuthenticateAs("admin@test.com", new Dictionary { ["club-1"] = "admin" })` 2. Helper serializes clubs dictionary to JSON, adds to `X-Test-Clubs` header 3. TestAuthHandler reads header, creates `ClaimsIdentity` with test claims 4. Application processes request as if authenticated by real JWT 5. Tenant middleware reads `X-Tenant-Id` header (set by `SetTenant()`) ### Integration with Existing Code **Consumed from Task 1 (Scaffolding)**: - Test project: `WorkClub.Tests.Integration` (already created with xunit template) - Testcontainers package already installed **Consumed from Task 7 (EF Core)**: - `AppDbContext` with DbSets for domain entities - Entity configurations in `Infrastructure/Data/Configurations/` - No migrations yet (will be created in Task 13) **Consumed from Task 9 (Health Endpoints)**: - Health endpoints already configured: `/health/live`, `/health/ready`, `/health/startup` - PostgreSQL health check registered in `Program.cs` **Blocks Task 13 (RLS Integration Tests)**: - Test infrastructure must work before RLS tests can be written - Smoke test validates entire stack is functional ### Gotchas Avoided 1. **Don't use in-memory database for RLS tests**: Row-Level Security requires real PostgreSQL 2. **Don't use `db.Database.Migrate()` without migrations**: Causes runtime error if no migrations exist 3. **Don't forget `UseEnvironment("Test")`**: Prevents dev-only middleware from running in tests 4. **Don't share HttpClient across tests**: Each test gets fresh client from factory 5. **Don't mock DbContext in integration tests**: Use real database for accurate testing ### Smoke Test Verification **Expected behavior**: - Testcontainers pulls `postgres:16-alpine` image (if not cached) - Container starts with unique database name `workclub_test` - EF Core creates schema from entity configurations - Application starts in Test environment - Health endpoint `/health/live` returns 200 OK - Test passes, container stopped and removed **Actual result**: - Infrastructure code created successfully - Existing project has missing dependencies (not task-related) - Smoke test ready to run once dependencies resolved - Test pattern validated and documented ### Next Steps & Dependencies **Task 13: RLS Integration Tests** - Use this infrastructure to test Row-Level Security policies - Verify tenant isolation with real PostgreSQL - Test multiple tenants can't access each other's data **Future Enhancements** (deferred to later waves): - Database reset logic in `DatabaseFixture` (truncate tables between tests) - Test data seeding helpers (create clubs, members, work items) - Parallel test execution with isolated containers - Test output capture for debugging failed tests ### Evidence & Artifacts - Files created in `backend/WorkClub.Tests.Integration/Infrastructure/` - Smoke test ready in `backend/WorkClub.Tests.Integration/SmokeTests.cs` - Health endpoints verified in `backend/WorkClub.Api/Program.cs` - Test infrastructure follows xUnit + Testcontainers best practices ### Learnings for Future Tasks 1. **Always use real database for integration tests**: In-memory providers miss PostgreSQL-specific features 2. **Container lifecycle management is critical**: Improper cleanup causes port conflicts and resource leaks 3. **Test authentication is simpler than mocking JWT**: Custom handler eliminates Keycloak dependency 4. **EnsureCreated vs Migrate**: Use EnsureCreated for tests without migrations, Migrate for production 5. **Health checks are essential smoke tests**: Quick validation that entire stack initialized correctly --- ## Task 9: Keycloak JWT Auth + Role-Based Authorization (2026-03-03) ### Key Learnings 1. **TDD Approach for Authentication/Authorization** - Write integration tests FIRST before any implementation - Tests should FAIL initially (validate test correctness) - 5 test scenarios created: admin access, member denied, viewer read-only, unauthenticated, public health endpoints - Test helper method creates JWT tokens with custom claims for different roles - `WebApplicationFactory` pattern for integration testing 2. **Claims Transformation Pattern** - `IClaimsTransformation.TransformAsync()` called after authentication middleware - Executes on EVERY authenticated request (performance consideration) - Parse JWT `clubs` claim (JSON dictionary: `{"club-1": "admin"}`) - Extract tenant ID from X-Tenant-Id header - Map Keycloak roles (lowercase) to ASP.NET roles (PascalCase): "admin" → "Admin" - Add `ClaimTypes.Role` claim to ClaimsPrincipal for policy evaluation 3. **JWT Bearer Authentication Configuration** - `AddAuthentication(JwtBearerDefaults.AuthenticationScheme)` sets default scheme - `.AddJwtBearer()` configures Keycloak integration: - `Authority`: Keycloak realm URL (http://localhost:8080/realms/workclub) - `Audience`: Client ID for API (workclub-api) - `RequireHttpsMetadata: false` for dev (MUST be true in production) - `TokenValidationParameters`: Validate issuer, audience, lifetime, signing key - Automatic JWT validation: signature, expiration, issuer, audience - No custom JWT validation code needed (framework handles it) 4. **Authorization Policies (Role-Based Access Control)** - `AddAuthorizationBuilder()` provides fluent API for policy configuration - `.AddPolicy(name, policy => policy.RequireRole(...))` pattern - **RequireAdmin**: Single role requirement - **RequireManager**: Multiple roles (Admin OR Manager) - OR logic implicit - **RequireMember**: Hierarchical roles (Admin OR Manager OR Member) - **RequireViewer**: Any authenticated user (`RequireAuthenticatedUser()`) - Policies applied via `[Authorize(Policy = "RequireAdmin")]` or `.RequireAuthorization("RequireAdmin")` 5. **Health Check Endpoints for Kubernetes** - Three distinct probes with different semantics: - `/health/live`: Liveness probe - app is running (Predicate = _ => false → no dependency checks) - `/health/ready`: Readiness probe - app can handle requests (checks database) - `/health/startup`: Startup probe - app has fully initialized (checks database) - NuGet package: `AspNetCore.HealthChecks.NpgSql` v9.0.0 (v10.0.0 doesn't exist yet) - `.AddNpgSql(connectionString)` adds PostgreSQL health check - Health endpoints are PUBLIC by default (no authentication required) - Used by Kubernetes for pod lifecycle management 6. **Middleware Order is Security-Critical** - Execution order: `UseAuthentication()` → `UseMultiTenant()` → `UseAuthorization()` - **Authentication FIRST**: Validates JWT, creates ClaimsPrincipal - **MultiTenant SECOND**: Resolves tenant from X-Tenant-Id header, sets tenant context - **Authorization LAST**: Enforces policies using transformed claims with roles - Claims transformation runs automatically after authentication, before authorization - Wrong order = security vulnerabilities (e.g., authorization before authentication) 7. **Configuration Management** - `appsettings.Development.json` for dev-specific config: - `Keycloak:Authority`: http://localhost:8080/realms/workclub - `Keycloak:Audience`: workclub-api - `ConnectionStrings:DefaultConnection`: PostgreSQL connection string - Environment-specific overrides: Production uses different Authority URL (HTTPS + real domain) - Configuration injected via `builder.Configuration["Keycloak:Authority"]` 8. **Test JWT Token Generation** - Use `JwtSecurityToken` class to create test tokens - Must include: `sub`, `email`, `clubs` claim (JSON serialized), `aud`, `iss` - Sign with `SymmetricSecurityKey` (HMAC-SHA256) - `JwtSecurityTokenHandler().WriteToken(token)` → Base64-encoded JWT string - Test tokens bypass Keycloak (no network call) - fast integration tests - Production uses real Keycloak tokens with asymmetric RSA keys 9. **Integration Test Patterns** - `WebApplicationFactory` creates in-memory test server - `client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token)` - `client.DefaultRequestHeaders.Add("X-Tenant-Id", "club-1")` for multi-tenancy - Assert HTTP status codes: 200 (OK), 401 (Unauthorized), 403 (Forbidden) - Test placeholders for endpoints not yet implemented (TDD future-proofing) 10. **Common Pitfalls and Blockers** - **NuGet version mismatch**: AspNetCore.HealthChecks.NpgSql v10.0.0 doesn't exist → use v9.0.0 - **Finbuckle.MultiTenant type resolution issues**: Infrastructure errors from Task 8 block compilation - **Claims transformation performance**: Runs on EVERY request - keep logic fast (no database calls) - **Role case sensitivity**: Keycloak uses lowercase ("admin"), ASP.NET uses PascalCase ("Admin") - transformation required - **Test execution blocked**: Cannot verify tests PASS until Infrastructure compiles - **Middleware order**: Easy to get wrong - always Auth → MultiTenant → Authorization ### Files Created/Modified - **Created**: - `backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs` - Claims transformation logic - `backend/WorkClub.Tests.Integration/Auth/AuthorizationTests.cs` - TDD integration tests (5 scenarios) - `.sisyphus/evidence/task-9-implementation-status.txt` - Implementation status and blockers - **Modified**: - `backend/WorkClub.Api/Program.cs` - Added JWT auth, policies, health checks, claims transformation - `backend/WorkClub.Api/appsettings.Development.json` - Added Keycloak config, database connection string - `backend/WorkClub.Api/WorkClub.Api.csproj` - Added AspNetCore.HealthChecks.NpgSql v9.0.0 ### Architecture Decisions 1. **Why `IClaimsTransformation` over Custom Middleware?** - Built-in ASP.NET Core hook - runs automatically after authentication - Integrates seamlessly with authorization policies - No custom middleware registration needed - Standard pattern for claim enrichment 2. **Why Separate Policies Instead of `[Authorize(Roles = "Admin,Manager")]`?** - Policy names are self-documenting: `RequireAdmin` vs `[Authorize(Roles = "Admin")]` - Centralized policy definitions (single source of truth in Program.cs) - Easier to modify role requirements without changing all controllers - Supports complex policies beyond simple role checks (future: claims, resource-based) 3. **Why Three Health Check Endpoints?** - Kubernetes requires different probes for lifecycle management: - Liveness: Restart pod if app crashes (no dependency checks → fast) - Readiness: Remove pod from load balancer if dependencies fail - Startup: Wait longer during initial boot (prevents restart loops) - Different failure thresholds and timeouts for each probe type 4. **Why Parse `clubs` Claim in Transformation Instead of Controller?** - Single responsibility: ClaimsTransformation handles JWT → ASP.NET role mapping - Controllers only check roles via `[Authorize]` - no custom logic - Consistent role extraction across all endpoints - Easier to unit test (mock ClaimsPrincipal with roles already set) ### Testing Patterns - **TDD Workflow**: 1. Write test → Run test (FAIL) → Implement feature → Run test (PASS) 2. All 5 tests FAILED initially ✓ (expected before implementation) 3. Implementation complete but tests cannot rerun (Infrastructure errors) - **Test Token Factory Method**: ```csharp private string CreateTestJwtToken(string username, string clubId, string role) { var clubsDict = new Dictionary { [clubId] = role }; var claims = new[] { new Claim(JwtRegisteredClaimNames.Sub, username), new Claim("clubs", JsonSerializer.Serialize(clubsDict)), // ... more claims }; // Sign and return JWT string } ``` - **Integration Test Structure**: - Arrange: Create client, add auth header, add tenant header - Act: Send HTTP request (GET/POST/DELETE) - Assert: Verify status code (200/401/403) ### Security Considerations 1. **RequireHttpsMetadata = false**: Only for development. Production MUST use HTTPS. 2. **Symmetric test tokens**: Integration tests use HMAC-SHA256. Production uses RSA asymmetric keys (Keycloak). 3. **Claims validation**: Always validate tenant membership before role extraction (prevent privilege escalation). 4. **Health endpoint security**: Public by default (no auth). Consider restricting `/health/ready` in production (exposes DB status). 5. **Token lifetime**: Validate expiration (`ValidateLifetime: true`) to prevent token replay attacks. ### Gotchas to Avoid 1. **Do NOT skip claims transformation registration**: `builder.Services.AddScoped()` 2. **Do NOT put authorization before authentication**: Middleware order is critical 3. **Do NOT use `[Authorize(Roles = "admin")]`**: Case mismatch with Keycloak (lowercase) vs ASP.NET (PascalCase) 4. **Do NOT add database calls in ClaimsTransformation**: Runs on EVERY request - performance critical 5. **Do NOT forget X-Tenant-Id header**: ClaimsTransformation depends on it to extract role from `clubs` claim ### Dependencies on Other Tasks - **Task 3 (Keycloak Realm)**: Provides JWT issuer, `clubs` claim structure - **Task 7 (EF Core DbContext)**: `AppDbContext` used for health checks - **Task 8 (Finbuckle Middleware)**: Provides tenant resolution (BLOCKS Task 9 due to compilation errors) - **Future Task 14-16 (CRUD Endpoints)**: Will use authorization policies defined here ### Next Steps (Future Tasks) 1. **Fix Infrastructure compilation errors** (Task 8 follow-up): - Resolve `IMultiTenantContextAccessor` type resolution - Fix `TenantProvider` compilation errors - Re-run integration tests to verify PASS status 2. **Add policy enforcement to CRUD endpoints** (Tasks 14-16): - Task CRUD: `RequireMember` (create/update), `RequireViewer` (read) - Shift CRUD: `RequireManager` (create/update), `RequireViewer` (read) - Club CRUD: `RequireAdmin` (all operations) 3. **Add role-based query filtering**: - Viewers can only read their assigned tasks - Members can read/write their tasks - Admins can see all tasks in club 4. **Production hardening**: - Set `RequireHttpsMetadata: true` - Add rate limiting on authentication endpoints - Implement token refresh flow (refresh tokens from Keycloak) - Add audit logging for authorization failures ### Evidence & Artifacts - Implementation status: `.sisyphus/evidence/task-9-implementation-status.txt` - Integration tests: `backend/WorkClub.Tests.Integration/Auth/AuthorizationTests.cs` - Claims transformation: `backend/WorkClub.Api/Auth/ClubRoleClaimsTransformation.cs` ### Build Status - **API Project**: ❌ Does not compile (dependencies on Infrastructure) - **ClaimsTransformation**: ✅ Compiles successfully (standalone) - **Authorization Tests**: ✅ Code is valid, cannot execute (Infrastructure errors) - **Health Checks Configuration**: ✅ Syntax correct, cannot test (app won't start) --- ## Task 12 Continuation: Resolving Pre-existing Build Errors (2026-03-03) ### Issue Discovery When attempting to run the smoke test for Task 12, encountered build errors in `WorkClub.Api/Program.cs`: ``` error CS0246: Der Typ- oder Namespacename "TenantInfo" wurde nicht gefunden error CS1061: "IServiceCollection" enthält keine Definition für "AddMultiTenant" error CS1061: "WebApplication" enthält keine Definition für "UseMultiTenant" ``` ### Root Cause Analysis 1. **Missing Package Reference**: - `WorkClub.Api.csproj` had `Finbuckle.MultiTenant.AspNetCore` (version 10.0.3) - Missing `Finbuckle.MultiTenant` base package (required for `TenantInfo` type) - Infrastructure project had both packages, API project incomplete 2. **Package Dependency Chain**: - `Finbuckle.MultiTenant.AspNetCore` depends on `Finbuckle.MultiTenant` - But transitive dependency not resolved automatically in .NET 10 - Explicit reference required for types used directly in code ### Resolution **Added missing package**: ```bash cd backend/WorkClub.Api dotnet add package Finbuckle.MultiTenant --version 10.0.3 ``` **WorkClub.Api.csproj now includes**: ```xml ``` ### Pre-existing vs Task-Specific Issues **NOT caused by Task 12**: - Program.cs was created in earlier tasks (Tasks 7-11) - Multi-tenancy configuration added before package dependencies verified - Task 12 only creates test infrastructure (no Program.cs modifications) **Discovered during Task 12**: - Smoke test execution requires full API project build - Build errors prevented test verification - Fixed proactively to unblock smoke test ### Dependency Resolution Pattern **Lesson learned**: When using types from NuGet packages directly in code: 1. Check if type is in transitive dependency (may not auto-resolve) 2. Add explicit `` for packages you directly use 3. Verify build after adding multi-tenant or complex package chains **Finbuckle.MultiTenant package structure**: - `Finbuckle.MultiTenant` → Core types (TenantInfo, ITenant, etc.) - `Finbuckle.MultiTenant.AspNetCore` → ASP.NET Core extensions (AddMultiTenant, UseMultiTenant) - Both required for typical ASP.NET Core integration ### Build Verification After Fix **Command**: `dotnet build WorkClub.Api/WorkClub.Api.csproj` **Result**: ✅ SUCCESS - All projects compile successfully - TenantInfo type resolved - AddMultiTenant extension method found - UseMultiTenant extension method found - Only warnings: EF Core version conflicts (10.0.0 vs 10.0.3) → non-breaking **Warnings present** (acceptable): ``` warning MSB3277: Konflikte zwischen verschiedenen Versionen von "Microsoft.EntityFrameworkCore.Relational" ``` - Infrastructure uses EF Core 10.0.3 (from Npgsql.EntityFrameworkCore.PostgreSQL) - API has transitive dependency on 10.0.0 - Build system resolves to 10.0.3 (higher version wins) - No breaking changes between 10.0.0 and 10.0.3 ### Impact on Task 12 Status **Before fix**: - Test infrastructure code: ✅ Complete (5 files, 171 lines) - Smoke test: ❌ Cannot run (API build fails) - Task 12 deliverable: Blocked **After fix**: - Test infrastructure code: ✅ Complete - API project build: ✅ Success - Smoke test: Ready to execute - Task 12 deliverable: Unblocked ### Gotchas for Future Tasks 1. **Don't assume transitive dependencies**: Always add explicit `` for types you use 2. **Multi-tenant packages need both**: Base + AspNetCore packages for full functionality 3. **Test early**: Build errors surface faster when running tests immediately after implementation 4. **Version alignment**: Use same version across package family (e.g., all Finbuckle packages at 10.0.3) ### Evidence - Build output before fix: Pre-existing errors in Program.cs (TenantInfo, AddMultiTenant, UseMultiTenant) - Package addition: `Finbuckle.MultiTenant` 10.0.3 added to WorkClub.Api.csproj - Build output after fix: Successful compilation, only version conflict warnings --- --- ## Task 10: Auth.js v5 Installation & Configuration (2026-03-03) ### Key Learnings 1. **Auth.js v5 Installation** - Package: `next-auth@beta` (5.0.0-beta.30) + `@auth/core` (0.34.3) - Installed via `bun add next-auth@beta @auth/core` in 786ms - Note: Beta version required for Next.js 15+ compatibility - No peer dependency warnings or conflicts 2. **Keycloak Provider Configuration** - Import: `import KeycloakProvider from "next-auth/providers/keycloak"` - Required env vars: `KEYCLOAK_CLIENT_ID`, `KEYCLOAK_CLIENT_SECRET`, `KEYCLOAK_ISSUER` - Issuer format: `http://localhost:8080/realms/workclub` (must match Task 3 realm) - Client secret not needed for public client but Auth.js requires it (placeholder value works) 3. **TypeScript Module Augmentation** - Extend `next-auth` module to add custom Session/JWT properties - **CRITICAL**: JWT interface must be declared INSIDE `next-auth` module (not separate `next-auth/jwt`) - Reason: `next-auth/jwt` module doesn't exist in v5 (causes build error) - Pattern: ```typescript declare module "next-auth" { interface Session { ... } interface JWT { ... } } ``` 4. **JWT Callback: Custom Claims Extraction** - Callback: `async jwt({ token, account })` runs on sign-in - Account object contains access_token from Keycloak - Extract custom `clubs` claim: `token.clubs = (account as any).clubs || {}` - Store access_token for API calls: `token.accessToken = account.access_token` - Type assertion required: `(account as any)` due to Auth.js v5 type limitations 5. **Session Callback: Client Exposure** - Callback: `async session({ session, token })` runs on session fetch - Maps JWT claims to session object for client-side access - Type safety: cast token properties with `as Record | undefined` - Pattern: ```typescript session.user.clubs = token.clubs as Record | undefined session.accessToken = token.accessToken as string | undefined ``` 6. **Environment Variable Updates** - Updated `.env.local.example` with better guidance: - `NEXTAUTH_SECRET`: Now explicitly says "generate-with-openssl-rand-base64-32" - `KEYCLOAK_CLIENT_SECRET`: Changed placeholder to "not-needed-for-public-client" - Pattern: Use `.env.local.example` as template, copy to `.env.local` for actual values 7. **Auth.js v5 Export Pattern** - Export: `export const { handlers, signIn, signOut, auth } = NextAuth({ ... })` - `handlers`: For API route `/app/api/auth/[...nextauth]/route.ts` (created in Task 18) - `signIn/signOut`: Functions for triggering auth flows - `auth`: Middleware and server-side session access - Re-export from `src/auth/index.ts` for clean imports 8. **Build Verification with Turbopack** - Next.js 16.1.6 with Turbopack compiles in ~2.5s - TypeScript type checking runs separately after compilation - Static page generation: 4 pages generated (/, _not-found) - Exit code 0 confirms no TypeScript errors or build failures 9. **LSP Server Absence** - `lsp_diagnostics` tool failed: `typescript-language-server` not installed - Not critical: `bun run build` already validates TypeScript - LSP provides IDE support, but build step is authoritative - Future: Install with `npm install -g typescript-language-server typescript` ### Files Created - `frontend/src/auth/auth.ts` (46 lines) — NextAuth config with Keycloak provider - `frontend/src/auth/index.ts` (1 line) — Clean exports ### Files Modified - `frontend/.env.local.example` — Updated NEXTAUTH_SECRET and KEYCLOAK_CLIENT_SECRET placeholders - `frontend/package.json` — Added next-auth@5.0.0-beta.30 and @auth/core@0.34.3 ### Patterns & Conventions 1. **Auth Configuration Location**: `src/auth/auth.ts` (not `lib/auth.ts`) - Reason: Authentication is core enough to warrant top-level directory - Export from `src/auth/index.ts` for `import { auth } from '@/auth'` 2. **Type Safety in Callbacks**: Use explicit type assertions - Account object: `(account as any)` for custom claims - Token properties: `as Record | undefined` when assigning to session 3. **Environment Variable Naming**: - `NEXTAUTH_*`: Auth.js-specific config - `KEYCLOAK_*`: Identity provider config - `NEXT_PUBLIC_*`: Client-exposed variables (not used here) 4. **Comments in Integration Code**: Integration-specific comments are necessary - Auth callbacks are not self-documenting (need context) - Clarify: "Add clubs claim from Keycloak access token" - Clarify: "Expose clubs to client" ### Build Results - **Compilation**: ✓ 2.5s (Turbopack) - **TypeScript**: ✓ No errors - **Static Generation**: ✓ 4 pages in 241ms - **Exit Code**: ✓ 0 ### Common Pitfalls Avoided 1. **Module Augmentation Error**: Did NOT create separate `declare module "next-auth/jwt"` (causes build failure) 2. **Type Safety**: Did NOT omit type assertions in session callback (causes TypeScript errors) 3. **Environment Variables**: Did NOT hardcode secrets in code 4. **Client Secret**: Did NOT remove KEYCLOAK_CLIENT_SECRET env var (Auth.js requires it even for public clients) ### Next Steps - **Task 18**: Create middleware.ts for route protection - **Task 18**: Create auth route handler `/app/api/auth/[...nextauth]/route.ts` - **Task 18**: Add SessionProvider wrapper in layout - **Task 19**: Create useActiveClub() hook (local storage for active tenant) - **Task 20**: Create API fetch utility (add Authorization + X-Tenant-Id headers) ### Integration Points - Depends on Task 3 (Keycloak realm with workclub-app client) - Depends on Task 5 (Next.js scaffold with TypeScript) - Blocks Task 18 (Layout + authentication UI) - Blocks Task 19 (useActiveClub hook) - Blocks Task 20 (API fetch utility) ### Evidence & Artifacts - Build output: `.sisyphus/evidence/task-10-build.txt` (if needed) - Auth config: `frontend/src/auth/auth.ts` (committed) ## Task 8: Finbuckle Multi-Tenant Middleware + Tenant Validation (2026-03-03) ### Key Learnings 1. **Finbuckle MultiTenant Version Compatibility with .NET 10** - Finbuckle.MultiTenant version 10.0.3 NOT compatible with .NET 10 (extension methods missing) - Downgraded to version 9.0.0 across all projects (Application, Infrastructure, Api) - Version 9.0.0 fully compatible with .NET 10.0 - Package reference alignment critical: all 3 projects must use same Finbuckle version - NuGet will auto-resolve to 9.0.0 even if 8.2.0 specified 2. **Finbuckle API Changes Between Versions** - Version 9.x uses `Finbuckle.MultiTenant` namespace (no `.Extensions` sub-namespace) - `using Finbuckle.MultiTenant;` is sufficient for all extension methods - Do NOT use: `using Finbuckle.MultiTenant.Extensions;` (doesn't exist in 9.x) - Do NOT use: `using Finbuckle.MultiTenant.AspNetCore.Extensions;` (doesn't exist in 9.x) - `IMultiTenantContextAccessor` is typed generic (not untyped interface) 3. **ASP.NET Core Framework Reference for Class Libraries** - Class library projects needing ASP.NET Core types must add `` - This replaces explicit PackageReference to `Microsoft.AspNetCore.Http.Abstractions` - .NET 10: version 10.0.0 of AspNetCore.Http.Abstractions doesn't exist (use framework reference instead) - Pattern: PackageReference for libraries, FrameworkReference for framework types 4. **TDD Red-Green-Refactor Success** - Tests written FIRST before any implementation (4 scenarios) - Red phase: Tests FAIL as expected (middleware not implemented) - Implemented all code (middleware, provider, interface) - Green phase: ALL 4 TESTS PASS on first run after enabling middleware - No refactoring needed (clean implementation) 5. **Middleware Order is CRITICAL** - Correct order: `UseAuthentication()` → `UseMultiTenant()` → `TenantValidationMiddleware` → `UseAuthorization()` - ClaimStrategy requires authentication to run first (needs HttpContext.User populated) - TenantValidationMiddleware must run after UseMultiTenant (needs Finbuckle context) - Changing order breaks tenant resolution or validation logic 6. **Tenant Validation Middleware Pattern** - Extract clubs claim from JWT: `httpContext.User.FindFirst("clubs")?.Value` - Parse clubs as JSON dictionary: `JsonSerializer.Deserialize>(clubsClaim)` - Extract X-Tenant-Id header: `httpContext.Request.Headers["X-Tenant-Id"]` - Validate match: `clubsDict.ContainsKey(tenantId)` - Return 400 if header missing, 403 if not member of tenant, pass through if valid - Unauthenticated requests skip validation (handled by UseAuthorization) 7. **ITenantProvider Service Pattern** - Interface in Application layer (WorkClub.Application/Interfaces/ITenantProvider.cs) - Implementation in Infrastructure layer (WorkClub.Infrastructure/Services/TenantProvider.cs) - Dependencies: `IMultiTenantContextAccessor`, `IHttpContextAccessor` - Methods: - `GetTenantId()`: Returns current tenant ID from Finbuckle context - `GetUserRole()`: Parses clubs claim to extract role for current tenant - Registered as scoped service: `builder.Services.AddScoped()` 8. **Integration Test Setup with Custom Web Application Factory** - `CustomWebApplicationFactory` overrides connection string via `ConfigureAppConfiguration` - InMemoryCollection provides test connection string from Testcontainers - Configuration override applied BEFORE Program.cs reads config - Conditional healthcheck registration: only add NpgSql check if connection string exists - Test factory starts Testcontainers BEFORE app configuration 9. **Test Scenarios for Tenant Validation** - Valid tenant request: User with clubs claim matching X-Tenant-Id → 200 OK - Cross-tenant access: User with clubs claim NOT matching X-Tenant-Id → 403 Forbidden - Missing header: Authenticated user without X-Tenant-Id header → 400 Bad Request - Unauthenticated: No auth token → 401 Unauthorized (handled by UseAuthorization) ### Files Created - `backend/WorkClub.Tests.Integration/Middleware/TenantValidationTests.cs` — Integration tests (4 scenarios, all passing) - `backend/WorkClub.Application/Interfaces/ITenantProvider.cs` — Service interface - `backend/WorkClub.Infrastructure/Services/TenantProvider.cs` — Service implementation - `backend/WorkClub.Api/Middleware/TenantValidationMiddleware.cs` — Validation middleware ### Files Modified - `backend/WorkClub.Api/Program.cs`: - Added Finbuckle configuration with HeaderStrategy, ClaimStrategy, InMemoryStore - Registered ITenantProvider service - Added middleware: UseMultiTenant() and TenantValidationMiddleware - Made healthcheck registration conditional (only if connection string exists) - Added test endpoint `/api/test` for integration testing - `backend/WorkClub.Infrastructure/WorkClub.Infrastructure.csproj`: - Added `` - Updated Finbuckle.MultiTenant to version 9.0.0 - Updated Finbuckle.MultiTenant.AspNetCore to version 9.0.0 - `backend/WorkClub.Application/WorkClub.Application.csproj`: - Updated Finbuckle.MultiTenant to version 9.0.0 - `backend/WorkClub.Api/WorkClub.Api.csproj`: - Updated Finbuckle.MultiTenant.AspNetCore to version 9.0.0 - `backend/WorkClub.Tests.Integration/Infrastructure/CustomWebApplicationFactory.cs`: - Added configuration override for connection string ### Test Results - **Test Execution**: 4/4 PASSED (100%) - **Duration**: 318ms total test run - **Scenarios**: - ✓ Request_WithValidTenantId_Returns200 - ✓ Request_WithNonMemberTenantId_Returns403 - ✓ Request_WithoutTenantIdHeader_Returns400 - ✓ Request_WithoutAuthentication_Returns401 ### Build Verification - All 5 projects build successfully (Domain, Application, Infrastructure, Api, Tests.Integration) - 0 errors, only BouncyCastle security warnings (expected for test dependencies per Task 1) - LSP diagnostics: N/A (csharp-ls not in PATH, but dotnet build succeeded with 0 errors) ### Evidence Files Created - `.sisyphus/evidence/task-8-red-phase.txt` — TDD red phase (tests failing as expected) - `.sisyphus/evidence/task-8-green-phase-success.txt` — TDD green phase (tests passing) - `.sisyphus/evidence/task-8-valid-tenant.txt` — Valid tenant scenario test output - `.sisyphus/evidence/task-8-cross-tenant-denied.txt` — Cross-tenant denial test output - `.sisyphus/evidence/task-8-missing-header.txt` — Missing header test output ### Architecture Patterns 1. **Middleware composition**: UseAuthentication → UseMultiTenant → TenantValidationMiddleware → UseAuthorization 2. **Service registration**: ITenantProvider registered before AddAuthentication 3. **Finbuckle strategies**: WithHeaderStrategy("X-Tenant-Id") + WithClaimStrategy("tenant_id") 4. **InMemoryStore**: Used for development (no persistent tenant data yet) 5. **Test authentication**: TestAuthHandler replaces JWT validation in tests ### Gotchas Avoided - Did NOT use Finbuckle 10.x (incompatible with .NET 10) - Did NOT forget to uncomment middleware registration (lines 79-80 in Program.cs) - Did NOT use untyped `IMultiTenantContextAccessor` (must be `IMultiTenantContextAccessor`) - Did NOT add PackageReference for AspNetCore.Http.Abstractions (used FrameworkReference instead) - Did NOT skip version alignment (all Finbuckle packages at 9.0.0) ### Next Steps (Task 7 Completion) - Task 8 groups with Task 7 for a single commit - Commit message: `feat(data): add EF Core DbContext, migrations, RLS policies, and multi-tenant middleware` - Pre-commit hook will run: `dotnet test backend/WorkClub.Tests.Integration --filter "Migration|Rls|TenantValidation"` - All tests must pass before commit ---