Files
work-club-manager/.sisyphus/notepads/club-work-manager/learnings.md
WorkClub Automation 28964c6767 feat(backend): add PostgreSQL schema, RLS policies, and multi-tenant middleware
- Add EF Core migrations for initial schema (clubs, members, work_items, shifts, shift_signups)
- Implement RLS policies with SET LOCAL for tenant isolation
- Add Finbuckle multi-tenant middleware with ClaimStrategy + HeaderStrategy fallback
- Create TenantValidationMiddleware to enforce JWT claims match X-Tenant-Id header
- Add tenant-aware DB interceptors (SaveChangesTenantInterceptor, TenantDbConnectionInterceptor)
- Configure AppDbContext with tenant scoping and RLS support
- Add test infrastructure: CustomWebApplicationFactory, TestAuthHandler, DatabaseFixture
- Write TDD integration tests for multi-tenant isolation and RLS enforcement
- Add health check null safety for connection string

Tasks: 7 (PostgreSQL schema + migrations + RLS), 8 (Finbuckle multi-tenancy + validation), 12 (test infrastructure)
2026-03-03 14:32:21 +01:00

70 KiB

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 /apiworkclub-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=<from-keycloak>           # Placeholder (Task 3 fills in)

Pattern: .env.local.example is version-controlled, .env.local is gitignored per .gitignore.

Dependencies Installed

{
  "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: {"<tenant-id>": "<role>"}
    • 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

  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=<from-keycloak>           # Placeholder (Task 3 fills in)

Pattern: .env.local.example is version-controlled, .env.local is gitignored per .gitignore.

Dependencies Installed

{
  "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:

public SeedDataService(IServiceScopeFactory serviceScopeFactory)
{
    _serviceScopeFactory = serviceScopeFactory;
}

SeedAsync Pattern:

public async Task SeedAsync()
{
    using var scope = _serviceScopeFactory.CreateScope();
    var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    
    // Each entity type checked separately
    if (!context.Clubs.Any()) { /* seed clubs */ }
    if (!context.Members.Any()) { /* seed members */ }
    // etc.
}

Program.cs Seed Call:

if (app.Environment.IsDevelopment())
{
    using var scope = app.Services.CreateScope();
    var seedService = scope.ServiceProvider.GetRequiredService<SeedDataService>();
    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<TProgram>: Extends WebApplicationFactory<Program> 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<AuthenticationSchemeOptions>
    • 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<CustomWebApplicationFactory<Program>> 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<DatabaseFixture> 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> (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<Program> 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:
    • 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<Program> 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:

    private string CreateTestJwtToken(string username, string clubId, string role)
    {
        var clubsDict = new Dictionary<string, string> { [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<IClaimsTransformation, ClubRoleClaimsTransformation>()
  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:

cd backend/WorkClub.Api
dotnet add package Finbuckle.MultiTenant --version 10.0.3

WorkClub.Api.csproj now includes:

<PackageReference Include="Finbuckle.MultiTenant" Version="10.0.3" />
<PackageReference Include="Finbuckle.MultiTenant.AspNetCore" Version="10.0.3" />

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 <PackageReference> 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 <PackageReference> 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