39 KiB
Learnings — Club Work Manager
Conventions, patterns, and accumulated wisdom from task execution
Task 1: Monorepo Scaffolding (2026-03-03)
Key Learnings
-
.NET 10 Solution Format Change
- .NET 10 uses
.slnxformat (not.sln) - Solution files are still named
WorkClub.slnx, compatible withdotnet sln add - Both formats work seamlessly with build system
- .NET 10 uses
-
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
-
NuGet Package Versioning
- Finbuckle.MultiTenant: Specified 8.2.0 but .NET 10 SDK resolved to 9.0.0
- This is expected behavior with
rollForward: latestFeaturein global.json - No build failures - warnings only about version resolution
- Testcontainers brings in BouncyCastle which has known security advisories (expected in test dependencies)
-
Git Configuration for Automation
- Set
user.emailanduser.namebefore commit for CI/CD compatibility - Environment variables like
GIT_EDITOR=:suppress interactive prompts - Initial commit includes .sisyphus directory (plans, notepads, etc.)
- Set
-
Build Verification
dotnet build --configuration Releaseworks 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
-
Docker Compose v3.9 for Development
- Uses explicit
app-networkbridge for service discovery - Keycloak service depends on postgres with
condition: service_healthyfor ordered startup - Health checks critical: PostgreSQL uses
pg_isready, Keycloak uses/health/readyendpoint
- Uses explicit
-
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_committedwith max 200 connections configured
-
Keycloak 26.x Setup
- Image:
quay.io/keycloak/keycloak:26.1from Red Hat's container registry - Command:
start-dev --import-realm(development mode with automatic realm import) - Realm import directory:
/opt/keycloak/data/importmounted from./infra/keycloak - Database credentials: separate
keycloakuser withkeycloakpass(not production-safe, dev only) - Health check uses curl to
/health/readyendpoint (startup probe: 30s initial wait, 30 retries)
- Image:
-
Volume Management
- Named volume
postgres-datafor persistent PostgreSQL storage - Bind mount
./infra/keycloakto/opt/keycloak/data/importfor realm configuration - Bind mount
./infra/postgresto/docker-entrypoint-initdb.dfor database initialization
- Named volume
-
Service Discovery & Networking
- All services on
app-networkbridge network - Service names act as hostnames:
postgres:5432for PostgreSQL,localhost:8080for Keycloak UI - JDBC connection string in Keycloak:
jdbc:postgresql://postgres:5432/keycloak
- All services on
-
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_onwith 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/readyis 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.jsonwith 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
-
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
-
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
_ => falsefallback) - No runtime lookup overhead
- Clear inline state diagram
-
Entity Design Patterns (Domain-Driven Design)
- All entities use
requiredproperties:- Enforces non-null values at compile time
- Forces explicit initialization (no accidental defaults)
- Clean validation at instantiation
TenantIdisstring(matches Finbuckle.MultiTenant.ITenantInfo.Id type)- Foreign keys use explicit
GuidorGuid?(not navigation properties yet) RowVersion: byte[]?for optimistic concurrency (EF Core[Timestamp]attribute in Task 7)
- All entities use
-
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
-
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
- Explicit interface property (not EF shadow property) allows:
-
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)
-
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)
- Helper factory method
-
Project Structure Observations
- Projects in
backend/root, notbackend/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
- Projects in
Files Created
WorkClub.Domain/Enums/SportType.cs— 5 valuesWorkClub.Domain/Enums/ClubRole.cs— 4 valuesWorkClub.Domain/Enums/WorkItemStatus.cs— 5 valuesWorkClub.Domain/Interfaces/ITenantEntity.cs— Marker interfaceWorkClub.Domain/Entities/Club.cs— Basic aggregate rootWorkClub.Domain/Entities/Member.cs— User representationWorkClub.Domain/Entities/WorkItem.cs— Task with state machineWorkClub.Domain/Entities/Shift.cs— Volunteer shiftWorkClub.Domain/Entities/ShiftSignup.cs— Shift registrationWorkClub.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
-
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
-
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)
- Consistent
-
.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 initializedlivenessProbe(/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 OKfor healthy status
- Three distinct probes with different semantics:
-
StatefulSet + Headless Service Pattern
- StatefulSet requires
serviceNamepointing 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
- StatefulSet requires
-
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,
standardstorageClassName (overrideable in overlay) - Init script creates both
workclub(app) andkeycloakdatabases + users in single ConfigMap
- Image:
-
Keycloak 26.x Production Mode
- Image:
quay.io/keycloak/keycloak:26.1(Red Hat official registry) - Command:
start(production mode, notstart-dev) - Database: PostgreSQL via
KC_DB=postgres+KC_DB_URL_HOST=workclub-postgres - Probes:
/health/ready(readiness),/health/live(liveness) - Hostname:
KC_HOSTNAME_STRICT=falsein dev (allows any Host header) - Proxy:
KC_PROXY=edgefor behind reverse proxy (Ingress)
- Image:
-
Ingress Path-Based Routing
- Single ingress rule:
workclub-ingresswith path-based routing - Frontend: path
/→workclub-frontend:80(pathType: Prefix) - Backend: path
/api→workclub-api:80(pathType: Prefix) - Host:
localhost(overrideable per environment) - TLS: deferred to production overlay (cert-manager, letsencrypt)
- Single ingress rule:
-
ConfigMap Strategy for Non-Sensitive Configuration
- Central
workclub-configConfigMap:log-level: Informationcors-origins: http://localhost:3000api-base-url: http://workclub-apikeycloak-url: http://workclub-keycloakkeycloak-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)
- Central
-
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
-
Image Tag Strategy
- Base:
:latestplaceholder 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)
- Base:
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
serviceNamein StatefulSet causes pod DNS discovery failure (critical for Postgres) - Missing headless service's
publishNotReadyAddresses: trueprevents 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
/apimust usepathType: Prefixto 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
-
Next.js 15 with Bun Package Manager
bunx create-next-app@latestwith--use-bunflag 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)
-
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.cssfor theming - Components installed to
src/components/ui/automatically - Note:
toastcomponent deprecated → usesonnerinstead (modern toast library)
- Initialize with
-
Standalone Output Configuration
- Set
output: 'standalone'innext.config.tsfor 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
- Set
-
TypeScript Path Aliases
@/*→./src/*pre-configured intsconfig.jsonby 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)
-
Directory Structure Best Practices
- App Router location:
src/app/(notpages/) - Component organization:
src/components/for reusable,src/components/ui/for shadcn - Utilities:
src/lib/for helper functions (includes shadcn'scn()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
- App Router location:
-
Build Verification
bun run buildexit 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
-
Development Server Performance
bun run devstartup: 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
-
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
- UI components:
-
TypeScript Strict Mode:
tsconfig.jsonincludes"strict": true- All variables require explicit types
- Enables IDE autocomplete and early error detection
-
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)
-
Git Strategy:
.env.local.exampleis committed (template for developers).env.localis in.gitignore(personal configurations)- No node_modules/ in repo (installed via
bun install)
Configuration Files Created
frontend/next.config.ts— Minimal, standalone output enabledfrontend/tsconfig.json— Path aliases, strict TypeScript modefrontend/.env.local.example— Environment variable templatefrontend/components.json— shadcn/ui configurationfrontend/tailwind.config.ts— Tailwind CSS configuration with Tailwind v4frontend/postcss.config.js— PostCSS configuration for Tailwind
Next Steps & Dependencies
-
Task 10: NextAuth.js integration
- Adds
next-authdependency - Creates
src/app/api/auth/[...nextauth]/route.ts - Integrates with Keycloak (configured in Task 3)
- Adds
-
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.tsxwith navbar/sidebar - Client-side session provider setup
- Login/logout flows
- Creates
-
Task 21: Club management interface
- Feature components in
src/components/features/ - Forms using shadcn input/select/button
- Data fetching from backend API (Task 6+)
- Feature components in
Gotchas to Avoid
-
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).
-
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.
-
Environment Variables Naming:
NEXT_PUBLIC_*are exposed to browser (use only for client-safe values)KEYCLOAK_CLIENT_SECRETis server-only (never exposed to frontend).env.localfor local development, CI/CD environment variables at deployment
-
Path Aliases in Dynamic Imports: If using dynamic imports with
next/dynamic, ensure paths use@/*syntax for alias resolution. -
Tailwind CSS v4 Breaking Changes:
- Requires
@tailwindcss/postcsspackage (not default tailwindcss) - CSS layer imports may differ from v3 (auto-handled by create-next-app)
- Requires
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
-
Keycloak Realm Export Structure
- Realm exports are JSON files with top-level keys:
realm,clients,users,roles,groups - Must include
enabled: truefor 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)
- Realm exports are JSON files with top-level keys:
-
Protocol Mapper Configuration for Custom JWT Claims
- Mapper type:
oidc-usermodel-attribute-mapper(NOT Script Mapper) - Critical setting:
jsonType.label: JSONensures 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)
- Mapper type:
-
Client Configuration Patterns
- Confidential client (workclub-api):
publicClient: false, has client secretserviceAccountsEnabled: truefor service-to-service authstandardFlowEnabled: false,directAccessGrantsEnabled: false(no user login)- Used by backend for client credentials grant
- Public client (workclub-app):
publicClient: true, no client secretstandardFlowEnabled: truefor OAuth2 Authorization Code FlowdirectAccessGrantsEnabled: 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)
- Confidential client (workclub-api):
-
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, norequiredActions: []
- Custom attribute format:
-
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
- Algorithm:
-
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)
- Format:
-
Test User Scenarios
- admin@test.com: Multi-club admin (admin in club-1, member in club-2)
- manager@test.com: Single club manager (manager in club-1)
- member1@test.com: Multi-club member (member in both clubs)
- member2@test.com: Single club member (member in club-1)
- viewer@test.com: Read-only viewer (viewer in club-1)
- Covers all role types and single/multi-club scenarios
-
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)
-
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 returnobject(NOTstring) - Test script:
infra/keycloak/test-auth.shautomates this verification
- Use password grant (Direct Access Grant) for testing:
-
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: falsein 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_fileor 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
clubsclaim 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
-
Next.js 15 with Bun Package Manager
bunx create-next-app@latestwith--use-bunflag 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)
-
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.cssfor theming - Components installed to
src/components/ui/automatically - Note:
toastcomponent deprecated → usesonnerinstead (modern toast library)
- Initialize with
-
Standalone Output Configuration
- Set
output: 'standalone'innext.config.tsfor 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
- Set
-
TypeScript Path Aliases
@/*→./src/*pre-configured intsconfig.jsonby 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)
-
Directory Structure Best Practices
- App Router location:
src/app/(notpages/) - Component organization:
src/components/for reusable,src/components/ui/for shadcn - Utilities:
src/lib/for helper functions (includes shadcn'scn()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
- App Router location:
-
Build Verification
bun run buildexit 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
-
Development Server Performance
bun run devstartup: 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
-
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
- UI components:
-
TypeScript Strict Mode:
tsconfig.jsonincludes"strict": true- All variables require explicit types
- Enables IDE autocomplete and early error detection
-
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)
-
Git Strategy:
.env.local.exampleis committed (template for developers).env.localis in.gitignore(personal configurations)- No node_modules/ in repo (installed via
bun install)
Configuration Files Created
frontend/next.config.ts— Minimal, standalone output enabledfrontend/tsconfig.json— Path aliases, strict TypeScript modefrontend/.env.local.example— Environment variable templatefrontend/components.json— shadcn/ui configurationfrontend/tailwind.config.ts— Tailwind CSS configuration with Tailwind v4frontend/postcss.config.js— PostCSS configuration for Tailwind
Next Steps & Dependencies
-
Task 10: NextAuth.js integration
- Adds
next-authdependency - Creates
src/app/api/auth/[...nextauth]/route.ts - Integrates with Keycloak (configured in Task 3)
- Adds
-
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.tsxwith navbar/sidebar - Client-side session provider setup
- Login/logout flows
- Creates
-
Task 21: Club management interface
- Feature components in
src/components/features/ - Forms using shadcn input/select/button
- Data fetching from backend API (Task 6+)
- Feature components in
Gotchas to Avoid
-
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).
-
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.
-
Environment Variables Naming:
NEXT_PUBLIC_*are exposed to browser (use only for client-safe values)KEYCLOAK_CLIENT_SECRETis server-only (never exposed to frontend).env.localfor local development, CI/CD environment variables at deployment
-
Path Aliases in Dynamic Imports: If using dynamic imports with
next/dynamic, ensure paths use@/*syntax for alias resolution. -
Tailwind CSS v4 Breaking Changes:
- Requires
@tailwindcss/postcsspackage (not default tailwindcss) - CSS layer imports may differ from v3 (auto-handled by create-next-app)
- Requires
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