Files
work-club-manager/.sisyphus/notepads/club-work-manager/learnings.md
Sisyphus CI b7854e9571 feat(seed): add development seed data script
- Create SeedDataService in Infrastructure/Seed with idempotent seeding
- Seed 2 clubs: Sunrise Tennis Club, Valley Cycling Club
- Seed 7 member records (5 unique Keycloak test users)
- Seed 8 work items covering all status states
- Seed 5 shifts with date variety (past, today, future)
- Seed shift signups for realistic partial capacity
- Register SeedDataService in Program.cs with development-only guard
- Use deterministic GUID generation from club names
- Ensure all tenant IDs match for RLS compliance
- Track in learnings.md and evidence files for Task 22 QA
2026-03-03 14:23:50 +01:00

45 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)