diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 6c3acc0..f0d08c2 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -99,7 +99,20 @@ Decimal phases appear between their surrounding integers in numeric order.
3. Module UIs load on demand (lazy loading) -- no bundle bloat from inactive modules
4. User can open the Domaincheck module, enter a domain, and see whether it is registered or available
-**Plans**: TBD
+**Plans**: 4 plans
+
+**Wave 1**
+
+- [ ] 03-01-PLAN.md -- Module SDK, Prisma Registry, Activation API
+
+**Wave 2** *(blocked on Wave 1 completion)*
+
+- [ ] 03-02-PLAN.md -- Domaincheck Module: Backend DNS + Frontend UI
+- [ ] 03-03-PLAN.md -- Lazy Loading, Category Pages, Expanded Module View
+
+**Wave 3** *(blocked on Wave 2 completion)*
+
+- [ ] 03-04-PLAN.md -- Visual Verification: Human confirms module system
**UI hint**: yes
### Phase 4: Marketplace & Portal Navigation
@@ -159,7 +172,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6
|-------|----------------|--------|-----------|
| 1. Foundation & Portal Shell | 2/3 | In Progress | - |
| 2. Authentication & Multi-Tenancy | 2/5 | In Progress| |
-| 3. Module System & Domaincheck | 0/TBD | Not started | - |
+| 3. Module System & Domaincheck | 0/4 | Not started | - |
| 4. Marketplace & Portal Navigation | 0/TBD | Not started | - |
| 5. Dashboard & Calendar | 0/TBD | Not started | - |
| 6. Desktop Client & CI/CD | 0/TBD | Not started | - |
diff --git a/.planning/phases/03-module-system-domaincheck/03-01-PLAN.md b/.planning/phases/03-module-system-domaincheck/03-01-PLAN.md
new file mode 100644
index 0000000..1a1cece
--- /dev/null
+++ b/.planning/phases/03-module-system-domaincheck/03-01-PLAN.md
@@ -0,0 +1,247 @@
+---
+phase: 03-module-system-domaincheck
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - packages/module-sdk/package.json
+ - packages/module-sdk/tsconfig.json
+ - packages/module-sdk/src/index.ts
+ - packages/module-sdk/src/types.ts
+ - apps/api/prisma/schema.prisma
+ - apps/api/prisma/migrations/*
+ - apps/api/src/module-registry/module-registry.module.ts
+ - apps/api/src/module-registry/module-registry.service.ts
+ - apps/api/src/module-registry/module-registry.controller.ts
+ - apps/api/src/module-registry/module.guard.ts
+ - apps/api/src/module-registry/dto/activate-module.dto.ts
+ - apps/api/src/app.module.ts
+ - pnpm-workspace.yaml
+autonomous: true
+requirements:
+ - MOD-01
+ - MOD-02
+ - MOD-03
+must_haves:
+ truths:
+ - "Module SDK package exists with versioned interface contract"
+ - "Modules are stored in a database-driven registry"
+ - "Admin can activate/deactivate a module for their tenant without restart"
+ artifacts:
+ - path: "packages/module-sdk/src/types.ts"
+ provides: "TesseraModule interface, ModuleRoute interface"
+ exports: ["TesseraModule", "ModuleRoute", "ModuleCategory"]
+ - path: "apps/api/src/module-registry/module-registry.service.ts"
+ provides: "CRUD for module registry + per-tenant activation"
+ exports: ["ModuleRegistryService"]
+ - path: "apps/api/src/module-registry/module.guard.ts"
+ provides: "Guard checking module activation per tenant"
+ exports: ["ModuleGuard"]
+ key_links:
+ - from: "apps/api/src/module-registry/module-registry.service.ts"
+ to: "prisma.module"
+ via: "Prisma client queries"
+ pattern: "prisma\\.module\\.(find|create|update)"
+ - from: "apps/api/src/module-registry/module.guard.ts"
+ to: "ModuleRegistryService"
+ via: "DI injection, checks activation"
+ pattern: "moduleRegistryService\\.isModuleActive"
+---
+
+
+Deliver the Module SDK contract and a working module registry with per-tenant activation — the full vertical slice from database schema through API endpoints that an admin can call to register and activate modules.
+
+Purpose: Establishes the foundation that all modules (starting with Domaincheck in Plan 02) will build on. Per D-06, D-07, D-08.
+Output: @tessera/module-sdk package, Module + TenantModuleActivation Prisma models, ModuleRegistryModule with CRUD + activation endpoints.
+
+
+
+@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
+@/home/vicolab/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/03-module-system-domaincheck/03-CONTEXT.md
+@.planning/phases/03-module-system-domaincheck/03-RESEARCH.md
+@apps/api/prisma/schema.prisma
+@apps/api/src/app.module.ts
+@packages/shared/src/index.ts
+@pnpm-workspace.yaml
+
+
+## Phase Goal
+
+**As a** platform admin, **I want to** discover, register, and activate modules dynamically, **so that** tenants get access to new functionality without application restarts.
+
+
+
+
+ Task 1: Module SDK Package + Prisma Schema + Registry Service
+
+ packages/module-sdk/package.json,
+ packages/module-sdk/tsconfig.json,
+ packages/module-sdk/src/index.ts,
+ packages/module-sdk/src/types.ts,
+ apps/api/prisma/schema.prisma,
+ pnpm-workspace.yaml
+
+
+ apps/api/prisma/schema.prisma,
+ packages/shared/src/index.ts,
+ pnpm-workspace.yaml
+
+
+ 1. Create packages/module-sdk/ with package.json (name: @tessera/module-sdk, version: 0.1.0, main: src/index.ts). Add to pnpm-workspace.yaml packages list if not already globbed.
+
+ 2. In packages/module-sdk/src/types.ts define (per D-06, D-09):
+ - ModuleCategory type: string literal union or string
+ - ModuleRoute interface: method (GET|POST|PUT|DELETE), path (string), handler (string)
+ - TesseraModule interface: id, name, version, slug, category (ModuleCategory), description (Record of string to string for i18n), icon (optional string), routes (optional ModuleRoute array), component (function returning Promise of React.ComponentType), cardComponent (optional function returning Promise of React.ComponentType)
+ - ModuleManifest type: Omit TesseraModule of component and cardComponent (backend-only metadata)
+
+ 3. In packages/module-sdk/src/index.ts re-export everything from types.ts.
+
+ 4. Extend apps/api/prisma/schema.prisma with two models (per D-07):
+ - model Module: id (uuid), slug (unique string), name (string), version (string), category (string), description (Json), icon (optional string), isSystem (boolean default false), createdAt, updatedAt, activations relation to TenantModuleActivation[]
+ - model TenantModuleActivation: id (uuid), tenantId (string), moduleId (string), isActive (boolean default true), activatedAt (DateTime default now), module relation. Unique constraint on [tenantId, moduleId], index on [tenantId].
+
+ 5. Run prisma migrate dev with name "add-module-registry" to create the migration.
+
+ 6. Run pnpm install from monorepo root to link the new package.
+
+
+ cd /home/vicolab/projects/tessera-ctl && pnpm exec prisma migrate status --schema=apps/api/prisma/schema.prisma 2>&1 | grep -q "Database schema is up to date" && node -e "const sdk = require('./packages/module-sdk/src/index.ts'); console.log('SDK exports:', Object.keys(sdk))" 2>/dev/null || pnpm exec tsx -e "import { TesseraModule } from '@tessera/module-sdk'; console.log('SDK type-checks')"
+
+
+ - packages/module-sdk/src/types.ts exports TesseraModule, ModuleRoute, ModuleManifest, ModuleCategory
+ - Prisma schema contains model Module and model TenantModuleActivation with correct fields
+ - Migration applied successfully to PostgreSQL
+ - pnpm-workspace.yaml includes packages/module-sdk path
+
+ SDK package exists with typed interfaces, Prisma models for module registry are migrated and queryable
+
+
+
+ Task 2: ModuleRegistry NestJS Module with CRUD + Activation Endpoints
+
+ apps/api/src/module-registry/module-registry.module.ts,
+ apps/api/src/module-registry/module-registry.service.ts,
+ apps/api/src/module-registry/module-registry.controller.ts,
+ apps/api/src/module-registry/module.guard.ts,
+ apps/api/src/module-registry/dto/activate-module.dto.ts,
+ apps/api/src/app.module.ts
+
+
+ apps/api/src/app.module.ts,
+ apps/api/src/tenant/tenant.middleware.ts,
+ apps/api/src/auth/guards/jwt-auth.guard.ts,
+ apps/api/src/auth/guards/roles.guard.ts,
+ apps/api/src/prisma/prisma.service.ts
+
+
+ 1. Create apps/api/src/module-registry/dto/activate-module.dto.ts:
+ - ActivateModuleDto: moduleId (string, required)
+ - Class-validator decorators: IsUUID for moduleId
+
+ 2. Create apps/api/src/module-registry/module-registry.service.ts:
+ - Inject PrismaService
+ - findAll(): returns all Module records
+ - findBySlug(slug: string): finds module by slug
+ - findActiveForTenant(tenantId: string): returns modules where TenantModuleActivation.isActive=true for given tenant
+ - activateForTenant(tenantId: string, moduleId: string): upsert TenantModuleActivation with isActive=true (per D-08)
+ - deactivateForTenant(tenantId: string, moduleId: string): update TenantModuleActivation set isActive=false (per D-08)
+ - isModuleActive(tenantId: string, moduleSlug: string): boolean check for guard use
+ - seedModule(manifest: object with slug, name, version, category, description, icon, isSystem): upsert Module record by slug
+
+ 3. Create apps/api/src/module-registry/module.guard.ts:
+ - Injectable CanActivate guard
+ - Extract moduleSlug from request route params or a custom decorator
+ - Get tenantId from request (set by TenantMiddleware)
+ - Call moduleRegistryService.isModuleActive(tenantId, moduleSlug)
+ - Throw ForbiddenException if module not active for tenant
+ - Export UseModule decorator: SetMetadata('moduleSlug', slug) + UseGuards(ModuleGuard)
+
+ 4. Create apps/api/src/module-registry/module-registry.controller.ts:
+ - GET /modules — list all modules (any authenticated user)
+ - GET /modules/active — list active modules for current tenant (tenantId from request)
+ - POST /modules/:moduleId/activate — Admin only (@Roles(Role.ADMIN, Role.SUPER_ADMIN)), calls activateForTenant
+ - POST /modules/:moduleId/deactivate — Admin only, calls deactivateForTenant
+
+ 5. Create apps/api/src/module-registry/module-registry.module.ts:
+ - imports: PrismaModule
+ - providers: ModuleRegistryService, ModuleGuard
+ - controllers: ModuleRegistryController
+ - exports: ModuleRegistryService, ModuleGuard
+
+ 6. Register ModuleRegistryModule in apps/api/src/app.module.ts imports array.
+
+
+ cd /home/vicolab/projects/tessera-ctl && pnpm --filter api build 2>&1 | tail -5 && curl -s http://localhost:3001/modules 2>/dev/null | head -1 || echo "Build check passed"
+
+
+ - apps/api builds without errors with ModuleRegistryModule registered
+ - GET /modules endpoint exists and returns array (empty initially)
+ - GET /modules/active returns only tenant-activated modules
+ - POST /modules/:id/activate requires ADMIN role and creates activation record
+ - POST /modules/:id/deactivate sets isActive=false without deleting the record
+ - ModuleGuard is exported and usable by downstream module controllers
+
+ Admin can list, activate, and deactivate modules via API. ModuleGuard available for module-specific endpoints to check activation.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| client -> API | Untrusted input for module activation/deactivation |
+| module guard | Ensures tenant cannot access modules not activated for them |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-03-01 | Spoofing | module activation endpoint | mitigate | @Roles(ADMIN) guard ensures only admins activate modules |
+| T-03-02 | Tampering | moduleId param | mitigate | IsUUID validator on DTO, Prisma parameterized queries |
+| T-03-03 | Information Disclosure | GET /modules | accept | Module catalog is non-sensitive metadata, all authenticated users may see it |
+| T-03-04 | Elevation of Privilege | module guard bypass | mitigate | ModuleGuard checks tenantId from JWT (not user-supplied), ForbiddenException on inactive |
+
+
+
+- pnpm --filter api build succeeds
+- Prisma migration applied, Module and TenantModuleActivation tables exist
+- @tessera/module-sdk package resolves from apps/api
+- API endpoints respond correctly to authenticated requests
+
+
+
+- Module SDK defines TesseraModule interface with all fields from RESEARCH.md
+- Database has Module + TenantModuleActivation tables
+- Admin can activate/deactivate modules per tenant via REST API without restart (D-08)
+- ModuleGuard prevents unauthorized access to inactive modules
+
+
+## Artifacts this phase produces
+
+| Symbol | Location | Type |
+|--------|----------|------|
+| TesseraModule | packages/module-sdk/src/types.ts | interface |
+| ModuleRoute | packages/module-sdk/src/types.ts | interface |
+| ModuleManifest | packages/module-sdk/src/types.ts | type |
+| ModuleCategory | packages/module-sdk/src/types.ts | type |
+| ModuleRegistryService | apps/api/src/module-registry/module-registry.service.ts | class |
+| ModuleRegistryController | apps/api/src/module-registry/module-registry.controller.ts | class |
+| ModuleGuard | apps/api/src/module-registry/module.guard.ts | class |
+| UseModule | apps/api/src/module-registry/module.guard.ts | decorator |
+| Module (Prisma) | apps/api/prisma/schema.prisma | model |
+| TenantModuleActivation (Prisma) | apps/api/prisma/schema.prisma | model |
+
+
diff --git a/.planning/phases/03-module-system-domaincheck/03-02-PLAN.md b/.planning/phases/03-module-system-domaincheck/03-02-PLAN.md
new file mode 100644
index 0000000..e0280d9
--- /dev/null
+++ b/.planning/phases/03-module-system-domaincheck/03-02-PLAN.md
@@ -0,0 +1,273 @@
+---
+phase: 03-module-system-domaincheck
+plan: 02
+type: execute
+wave: 2
+depends_on: ["03-01"]
+files_modified:
+ - apps/api/src/domaincheck/domaincheck.module.ts
+ - apps/api/src/domaincheck/domaincheck.service.ts
+ - apps/api/src/domaincheck/domaincheck.controller.ts
+ - apps/api/src/domaincheck/dto/check-domain.dto.ts
+ - apps/api/src/domaincheck/domaincheck.seed.ts
+ - apps/api/src/app.module.ts
+ - apps/web/src/app/(portal)/modules/domaincheck/page.tsx
+ - apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx
+ - apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx
+ - apps/web/src/app/(portal)/modules/domaincheck/actions.ts
+ - apps/web/messages/de.json
+ - apps/web/messages/en.json
+autonomous: true
+requirements:
+ - DCHK-01
+ - DCHK-02
+ - DCHK-03
+must_haves:
+ truths:
+ - "User can open the Domaincheck module and see an input field"
+ - "User enters a domain name and sees TLD variant results"
+ - "Results show green (available) or red (registered) status per TLD"
+ artifacts:
+ - path: "apps/api/src/domaincheck/domaincheck.service.ts"
+ provides: "DNS-based domain availability checking"
+ exports: ["DomaincheckService"]
+ - path: "apps/api/src/domaincheck/domaincheck.controller.ts"
+ provides: "POST /modules/domaincheck/check endpoint"
+ exports: ["DomaincheckController"]
+ - path: "apps/web/src/app/(portal)/modules/domaincheck/page.tsx"
+ provides: "Domaincheck UI page within portal"
+ key_links:
+ - from: "apps/web/src/app/(portal)/modules/domaincheck/actions.ts"
+ to: "/modules/domaincheck/check"
+ via: "fetch POST to API"
+ pattern: "fetch.*modules/domaincheck/check"
+ - from: "apps/api/src/domaincheck/domaincheck.service.ts"
+ to: "node:dns/promises"
+ via: "dns.resolve for availability check"
+ pattern: "dns\\.resolve"
+---
+
+
+Deliver a working Domaincheck module end-to-end — user enters a domain name, backend checks TLD variants via DNS, frontend shows green/red results. This is the proof-of-concept module validating the entire module system.
+
+Purpose: Per D-01, D-02, D-03 — validates the module system with a real, interactive module. Users get immediate value.
+Output: DomaincheckModule (NestJS), domaincheck frontend page, module seed data.
+
+
+
+@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
+@/home/vicolab/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/phases/03-module-system-domaincheck/03-CONTEXT.md
+@.planning/phases/03-module-system-domaincheck/03-RESEARCH.md
+@.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md
+@apps/api/src/module-registry/module-registry.service.ts
+@apps/api/src/module-registry/module.guard.ts
+@apps/web/src/app/(portal)/layout.tsx
+@apps/web/messages/de.json
+@apps/web/messages/en.json
+
+
+## Phase Goal
+
+**As a** user, **I want to** enter a domain name and see which TLD variants are available or registered, **so that** I can quickly check domain availability without leaving the platform.
+
+
+
+
+ Task 1: Domaincheck Backend — DNS Service + API Endpoint + Module Seed
+
+ apps/api/src/domaincheck/domaincheck.module.ts,
+ apps/api/src/domaincheck/domaincheck.service.ts,
+ apps/api/src/domaincheck/domaincheck.controller.ts,
+ apps/api/src/domaincheck/dto/check-domain.dto.ts,
+ apps/api/src/domaincheck/domaincheck.seed.ts,
+ apps/api/src/app.module.ts
+
+
+ apps/api/src/module-registry/module-registry.service.ts,
+ apps/api/src/module-registry/module.guard.ts,
+ apps/api/src/app.module.ts
+
+
+ 1. Create apps/api/src/domaincheck/dto/check-domain.dto.ts:
+ - CheckDomainDto: domain (string, @IsString, @IsNotEmpty, @Matches regex for valid domain label — alphanumeric + hyphens, no dots)
+ - Optional tlds (string array) — if omitted, use defaults
+
+ 2. Create apps/api/src/domaincheck/domaincheck.service.ts:
+ - DEFAULT_TLDS constant: ['de', 'com', 'net', 'org'] (per D-03 configurable, these are defaults)
+ - Method checkDomain(domain: string, tlds?: string[]): Promise of array of { tld: string, fqdn: string, status: 'available' | 'registered' }
+ - Implementation: use node:dns/promises — dns.resolve(fqdn) for each TLD variant
+ - If resolves (any record) -> status = 'registered'
+ - If throws with code ENOTFOUND or ENODATA -> status = 'available'
+ - If throws with other error -> status = 'registered' (safe default)
+ - Use Promise.allSettled for parallel lookups with 5-second timeout per lookup
+ - Per D-02: system checks all TLD variants automatically from the base domain name
+
+ 3. Create apps/api/src/domaincheck/domaincheck.controller.ts:
+ - POST /modules/domaincheck/check
+ - Protected by UseModule('domaincheck') decorator from module.guard.ts
+ - Accepts CheckDomainDto body
+ - Returns { domain: string, results: array of { tld, fqdn, status } }
+
+ 4. Create apps/api/src/domaincheck/domaincheck.module.ts:
+ - imports: PrismaModule, ModuleRegistryModule (for guard access)
+ - providers: DomaincheckService
+ - controllers: DomaincheckController
+
+ 5. Create apps/api/src/domaincheck/domaincheck.seed.ts:
+ - Export async function seedDomaincheckModule(moduleRegistryService: ModuleRegistryService)
+ - Calls moduleRegistryService.seedModule with: slug='domaincheck', name='Domaincheck', version='1.0.0', category='domain-tools' (per D-09), description={ de: 'Domain-Verfuegbarkeit pruefen', en: 'Check domain availability' }, isSystem=true
+
+ 6. Register DomaincheckModule in apps/api/src/app.module.ts imports.
+
+ 7. Call the seed function from an OnModuleInit hook in DomaincheckModule (or add to an existing seeding mechanism) so the domaincheck module record exists in the database on startup.
+
+
+ cd /home/vicolab/projects/tessera-ctl && pnpm --filter api build 2>&1 | tail -3
+
+
+ - API build succeeds with DomaincheckModule
+ - POST /modules/domaincheck/check accepts { domain: "google" } and returns results array with tld, fqdn, status for each default TLD
+ - DNS check correctly identifies google.com as registered
+ - Module seed creates a "domaincheck" record in Module table on startup
+ - UseModule('domaincheck') guard protects the endpoint — returns 403 if module not activated for tenant
+
+ Domaincheck API endpoint works end-to-end: accepts domain, checks DNS for TLD variants, returns structured results
+
+
+
+ Task 2: Domaincheck Frontend — Input Form + Results Display
+
+ apps/web/src/app/(portal)/modules/domaincheck/page.tsx,
+ apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx,
+ apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx,
+ apps/web/src/app/(portal)/modules/domaincheck/actions.ts,
+ apps/web/messages/de.json,
+ apps/web/messages/en.json
+
+
+ apps/web/src/app/(portal)/page.tsx,
+ apps/web/src/app/(portal)/layout.tsx,
+ apps/web/messages/de.json,
+ apps/web/messages/en.json
+
+
+ 1. Add i18n keys to apps/web/messages/de.json under "domaincheck" namespace:
+ - title: "Domaincheck"
+ - description: "Pruefe ob eine Domain verfuegbar ist"
+ - inputPlaceholder: "Domain eingeben (z.B. beispiel)"
+ - checkButton: "Pruefen"
+ - statusAvailable: "Verfuegbar"
+ - statusRegistered: "Registriert"
+ - noResults: "Gib einen Domain-Namen ein um die Verfuegbarkeit zu pruefen"
+ - checking: "Pruefe..."
+ - error: "Fehler bei der Pruefung"
+
+ 2. Add same keys in English to apps/web/messages/en.json under "domaincheck" namespace:
+ - title: "Domain Check"
+ - description: "Check if a domain is available"
+ - inputPlaceholder: "Enter domain (e.g. example)"
+ - checkButton: "Check"
+ - statusAvailable: "Available"
+ - statusRegistered: "Registered"
+ - noResults: "Enter a domain name to check availability"
+ - checking: "Checking..."
+ - error: "Error checking domain"
+
+ 3. Create apps/web/src/app/(portal)/modules/domaincheck/actions.ts:
+ - Server action or client-side fetch function checkDomainAction(domain: string): fetches POST to API_URL/modules/domaincheck/check with { domain }, passes auth cookie/token, returns typed results array
+
+ 4. Create apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx:
+ - Client component ("use client")
+ - Input field (text) with placeholder from i18n
+ - Submit button
+ - Calls onSubmit prop with the domain value
+ - Shows loading state while checking (per i18n "checking" key)
+
+ 5. Create apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx:
+ - Client component
+ - Receives results array prop: { tld, fqdn, status }[]
+ - Renders list/table of results
+ - Per D-01: green badge/pill for "available", red badge/pill for "registered"
+ - Per D-02: shows all TLD variants with their status
+ - Empty state shows "noResults" message
+
+ 6. Create apps/web/src/app/(portal)/modules/domaincheck/page.tsx:
+ - Page component within (portal) route group (gets AppShell layout)
+ - Uses DomainInput and ResultList components
+ - Manages state with useState: domain input, results array, loading, error
+ - On submit: calls checkDomainAction, updates results
+ - Layout: title at top, input below, results list below input (per specifics in CONTEXT.md)
+ - Use Tailwind classes consistent with existing portal pages
+ - Use shadcn/ui Card component for the module panel (per D-05a)
+
+
+ cd /home/vicolab/projects/tessera-ctl && pnpm --filter web build 2>&1 | tail -5
+
+
+ - Web app builds successfully with the new domaincheck route
+ - /modules/domaincheck page renders within the portal AppShell
+ - Input field accepts text, submit triggers API call
+ - Results display with color-coded status badges (green=available, red=registered)
+ - All visible strings use next-intl translations (no hardcoded text)
+ - Works in both DE and EN locale
+
+ User can navigate to /modules/domaincheck, enter a domain name, and see color-coded availability results for .de, .com, .net, .org TLD variants
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| client -> POST /modules/domaincheck/check | User-supplied domain string |
+| API -> DNS | External DNS resolution (network) |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-03-05 | Tampering | domain input | mitigate | Regex validation in DTO — only alphanumeric + hyphens, max 63 chars, no dots allowed |
+| T-03-06 | Denial of Service | DNS lookups | mitigate | 5-second timeout per lookup, max 10 TLDs per request, Promise.allSettled prevents hanging |
+| T-03-07 | Information Disclosure | DNS results | accept | Domain availability is public information, no sensitive data exposed |
+| T-03-08 | Spoofing | module access | mitigate | UseModule('domaincheck') guard + JwtAuthGuard ensure only authenticated users with activated module can access |
+
+
+
+- pnpm --filter api build && pnpm --filter web build both succeed
+- POST /modules/domaincheck/check returns correct results for known domains
+- Frontend page renders and submits successfully
+- Module guard blocks access when module not activated for tenant
+
+
+
+- User can open /modules/domaincheck in browser (DCHK-01)
+- System checks TLD variants via DNS and returns status (DCHK-02)
+- Results show clear green/red status indicators (DCHK-03, D-01)
+- TLD list uses defaults ['de','com','net','org'] per D-03
+
+
+## Artifacts this phase produces
+
+| Symbol | Location | Type |
+|--------|----------|------|
+| DomaincheckService | apps/api/src/domaincheck/domaincheck.service.ts | class |
+| DomaincheckController | apps/api/src/domaincheck/domaincheck.controller.ts | class |
+| DomaincheckModule | apps/api/src/domaincheck/domaincheck.module.ts | NestJS module |
+| CheckDomainDto | apps/api/src/domaincheck/dto/check-domain.dto.ts | class |
+| seedDomaincheckModule | apps/api/src/domaincheck/domaincheck.seed.ts | function |
+| DomaincheckPage | apps/web/src/app/(portal)/modules/domaincheck/page.tsx | React component |
+| DomainInput | apps/web/src/app/(portal)/modules/domaincheck/components/DomainInput.tsx | React component |
+| ResultList | apps/web/src/app/(portal)/modules/domaincheck/components/ResultList.tsx | React component |
+| checkDomainAction | apps/web/src/app/(portal)/modules/domaincheck/actions.ts | function |
+
+
diff --git a/.planning/phases/03-module-system-domaincheck/03-03-PLAN.md b/.planning/phases/03-module-system-domaincheck/03-03-PLAN.md
new file mode 100644
index 0000000..8a14b41
--- /dev/null
+++ b/.planning/phases/03-module-system-domaincheck/03-03-PLAN.md
@@ -0,0 +1,211 @@
+---
+phase: 03-module-system-domaincheck
+plan: 03
+type: execute
+wave: 2
+depends_on: ["03-01"]
+files_modified:
+ - apps/web/src/app/(portal)/modules/[category]/page.tsx
+ - apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx
+ - apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx
+ - apps/web/src/lib/module-loader.ts
+ - apps/web/src/lib/api.ts
+ - apps/web/messages/de.json
+ - apps/web/messages/en.json
+autonomous: true
+requirements:
+ - MOD-04
+must_haves:
+ truths:
+ - "Module UIs load on demand via dynamic imports — inactive modules are never bundled"
+ - "Category page shows module cards in a grid layout"
+ - "Module can be opened in expanded view from category page"
+ artifacts:
+ - path: "apps/web/src/lib/module-loader.ts"
+ provides: "Dynamic import registry mapping slugs to lazy components"
+ exports: ["loadModuleComponent", "loadModuleCard", "MODULE_REGISTRY"]
+ - path: "apps/web/src/app/(portal)/modules/[category]/page.tsx"
+ provides: "Category page showing activated module cards"
+ - path: "apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx"
+ provides: "Expanded module view using lazy-loaded component"
+ key_links:
+ - from: "apps/web/src/lib/module-loader.ts"
+ to: "next/dynamic"
+ via: "dynamic import with ssr:false"
+ pattern: "dynamic.*import"
+ - from: "apps/web/src/app/(portal)/modules/[category]/page.tsx"
+ to: "/modules/active"
+ via: "API fetch for tenant's active modules"
+ pattern: "fetch.*modules/active"
+---
+
+
+Deliver lazy-loaded module UI rendering — category pages show module cards from the registry, and modules load on demand via Next.js dynamic imports. No bundle bloat from inactive modules.
+
+Purpose: Per D-04, D-05a, D-05b and MOD-04 — modules appear as cards within category pages and can expand to full view, all lazy-loaded.
+Output: Module loader utility, category page with dynamic routing, module card component.
+
+
+
+@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
+@/home/vicolab/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/phases/03-module-system-domaincheck/03-CONTEXT.md
+@.planning/phases/03-module-system-domaincheck/03-RESEARCH.md
+@.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md
+@apps/web/src/app/(portal)/layout.tsx
+@apps/web/src/app/(portal)/page.tsx
+
+
+## Phase Goal
+
+**As a** user, **I want to** browse module categories and open modules without page bloat, **so that** only active modules load their UI code on demand.
+
+
+
+
+ Task 1: Module Loader Utility + Category Page with Lazy Cards
+
+ apps/web/src/lib/module-loader.ts,
+ apps/web/src/app/(portal)/modules/[category]/page.tsx,
+ apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx,
+ apps/web/messages/de.json,
+ apps/web/messages/en.json
+
+
+ apps/web/src/app/(portal)/layout.tsx,
+ apps/web/src/app/(portal)/page.tsx,
+ apps/web/messages/de.json
+
+
+ 1. Create apps/web/src/lib/module-loader.ts:
+ - MODULE_REGISTRY: Record mapping module slugs to { component: () => import(...), cardComponent: () => import(...) }
+ - For domaincheck: component maps to dynamic(() => import('@/app/(portal)/modules/domaincheck/page'), { ssr: false }), cardComponent maps to a DomaincheckCard component
+ - Export loadModuleComponent(slug: string): returns the dynamic component or null if not in registry
+ - Export loadModuleCard(slug: string): returns the card component or a generic fallback card
+ - This is the lazy loading mechanism — components are only imported when rendered (per MOD-04)
+
+ 2. Add i18n keys to both message files under "modules" namespace:
+ - DE: categoryTitle: "Module: {category}", openModule: "Oeffnen", noModules: "Keine aktiven Module in dieser Kategorie"
+ - EN: categoryTitle: "Modules: {category}", openModule: "Open", noModules: "No active modules in this category"
+
+ 3. Create apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx:
+ - Client component ("use client")
+ - Props: module object (name, slug, description, icon, category)
+ - Renders a shadcn/ui Card with: icon area, module name, description (from current locale), "Open" link to /modules/{category}/{slug}
+ - Per D-05a: uniform card appearance, compact, looks good even with single module
+ - Uses Tailwind for consistent spacing and theming
+
+ 4. Create apps/web/src/app/(portal)/modules/[category]/page.tsx:
+ - Server component that receives params.category
+ - Fetches active modules for current tenant from API (GET /modules/active), filters by category matching params.category
+ - Renders a responsive grid of ModuleCard components (grid-cols-1 sm:grid-cols-2 lg:grid-cols-3)
+ - Empty state shows "noModules" i18n message
+ - Title shows category name formatted (capitalize, replace hyphens with spaces)
+ - Per D-04: category page groups all modules of that category
+
+
+ cd /home/vicolab/projects/tessera-ctl && pnpm --filter web build 2>&1 | tail -5
+
+
+ - Web build succeeds with dynamic route [category]
+ - /modules/domain-tools page fetches and displays active modules filtered by "domain-tools" category
+ - ModuleCard renders module info with link to expanded view
+ - module-loader.ts maps 'domaincheck' slug to lazy-imported components
+ - Inactive modules never trigger an import (verified by MODULE_REGISTRY only containing active slug mappings at build time)
+
+ Category page at /modules/domain-tools shows domaincheck as a card, lazy loading is wired via module-loader registry
+
+
+
+ Task 2: Expanded Module View with Dynamic Routing
+
+ apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx,
+ apps/web/src/lib/api.ts
+
+
+ apps/web/src/lib/module-loader.ts,
+ apps/web/src/app/(portal)/modules/[category]/page.tsx
+
+
+ 1. Create or extend apps/web/src/lib/api.ts (if not existing):
+ - Export getActiveModules(cookie: string): fetches GET /modules/active with auth cookie, returns module array
+ - Export getModuleBySlug(slug: string, cookie: string): fetches GET /modules, finds by slug, returns module or null
+ - Base URL from NEXT_PUBLIC_API_URL env var
+
+ 2. Create apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx:
+ - Per D-05b: expanded/full view when user opens a specific module
+ - Server component wrapper that validates moduleSlug exists in registry
+ - Uses loadModuleComponent(params.moduleSlug) from module-loader.ts to get the lazy component
+ - If module not found in registry: show 404/not-found state
+ - If found: render the dynamically imported component within a container
+ - The loaded component for 'domaincheck' is the DomaincheckPage from Plan 02
+ - Wrapped in shadcn/ui Card with back-link to category page
+
+ 3. Ensure the domaincheck page.tsx from Plan 02 can function both as standalone (/modules/domaincheck direct access) AND as the loaded component in expanded view. The [moduleSlug] route provides the canonical expanded path per D-05b: /modules/domain-tools/domaincheck
+
+
+ cd /home/vicolab/projects/tessera-ctl && pnpm --filter web build 2>&1 | tail -5
+
+
+ - /modules/domain-tools/domaincheck route renders the domaincheck module component
+ - Module component loads lazily (not in initial bundle of category page)
+ - Unknown module slugs show appropriate not-found state
+ - Back-link navigates to category page /modules/domain-tools
+
+ User can navigate from category card grid to expanded module view. Module UI code loads on demand only when opened (MOD-04).
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| URL params -> module loader | User can craft arbitrary slugs in URL |
+| API -> frontend | Module list from API determines what renders |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-03-09 | Tampering | [moduleSlug] param | mitigate | Only load from MODULE_REGISTRY whitelist — arbitrary slugs get not-found, no arbitrary imports |
+| T-03-10 | Elevation of Privilege | category page | mitigate | API filters by tenant's active modules — inactive modules never returned, cards never rendered |
+| T-03-11 | Information Disclosure | module metadata | accept | Module names/descriptions are non-sensitive catalog data |
+
+
+
+- pnpm --filter web build succeeds
+- /modules/domain-tools renders category page with module cards
+- /modules/domain-tools/domaincheck loads domaincheck UI lazily
+- Bundle analysis confirms domaincheck code not in category page chunk
+
+
+
+- Module UIs load on demand via next/dynamic with ssr:false (MOD-04)
+- Category page displays modules as cards per D-04, D-05a
+- Expanded view available per D-05b
+- No bundle impact from inactive/unregistered modules
+
+
+## Artifacts this phase produces
+
+| Symbol | Location | Type |
+|--------|----------|------|
+| MODULE_REGISTRY | apps/web/src/lib/module-loader.ts | const |
+| loadModuleComponent | apps/web/src/lib/module-loader.ts | function |
+| loadModuleCard | apps/web/src/lib/module-loader.ts | function |
+| CategoryPage | apps/web/src/app/(portal)/modules/[category]/page.tsx | React component |
+| ModuleCard | apps/web/src/app/(portal)/modules/[category]/components/ModuleCard.tsx | React component |
+| ExpandedModulePage | apps/web/src/app/(portal)/modules/[category]/[moduleSlug]/page.tsx | React component |
+| getActiveModules | apps/web/src/lib/api.ts | function |
+| getModuleBySlug | apps/web/src/lib/api.ts | function |
+
+
diff --git a/.planning/phases/03-module-system-domaincheck/03-04-PLAN.md b/.planning/phases/03-module-system-domaincheck/03-04-PLAN.md
new file mode 100644
index 0000000..bafd135
--- /dev/null
+++ b/.planning/phases/03-module-system-domaincheck/03-04-PLAN.md
@@ -0,0 +1,130 @@
+---
+phase: 03-module-system-domaincheck
+plan: 04
+type: execute
+wave: 3
+depends_on: ["03-02", "03-03"]
+files_modified: []
+autonomous: false
+requirements:
+ - MOD-01
+ - MOD-02
+ - MOD-03
+ - MOD-04
+ - DCHK-01
+ - DCHK-02
+ - DCHK-03
+must_haves:
+ truths:
+ - "All phase success criteria verified by human"
+ - "Module system works end-to-end with domaincheck as proof"
+ artifacts: []
+ key_links: []
+---
+
+
+Human verification that the complete module system and domaincheck module work as intended — admin activation, lazy loading, domain checking with colored results.
+
+Purpose: Final validation before marking Phase 3 complete.
+Output: Human approval or issues list for gap closure.
+
+
+
+@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
+@/home/vicolab/.claude/gsd-core/templates/summary.md
+
+
+
+@.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md
+@.planning/phases/03-module-system-domaincheck/03-02-SUMMARY.md
+@.planning/phases/03-module-system-domaincheck/03-03-SUMMARY.md
+
+
+## Phase Goal
+
+**As a** platform admin, **I want to** discover, register, and activate modules dynamically, **so that** tenants get access to new functionality without application restarts.
+
+
+
+
+ Task 1: Verify Module System End-to-End
+
+ Complete module system with SDK, database registry, per-tenant activation, lazy-loaded frontend, and working Domaincheck module.
+
+
+ 1. Start the Docker Compose stack: docker compose up -d
+ 2. Log in as Admin user
+ 3. Verify module registry API:
+ - GET http://localhost:3001/modules — should show domaincheck module in list
+ - GET http://localhost:3001/modules/active — should show activated modules for tenant
+ 4. If domaincheck not yet activated, activate it:
+ - POST http://localhost:3001/modules/{domaincheck-id}/activate
+ 5. Navigate to http://localhost:3000/modules/domain-tools
+ - Should see category page with Domaincheck card (icon, name, description)
+ - Card should look good as a standalone module in the grid (D-05a)
+ 6. Click "Open" on the Domaincheck card (or navigate to /modules/domain-tools/domaincheck)
+ - Should see expanded view with input field (D-05b)
+ 7. Enter "google" in the domain input and click "Check"
+ - Should see results for google.de, google.com, google.net, google.org
+ - google.com and google.de should show RED "Registered" badge (D-01)
+ 8. Enter a clearly available domain (e.g. "xyzabc123randomtest") and check
+ - Some TLDs should show GREEN "Available" badge
+ 9. Verify i18n: switch language to English
+ - All labels should change to English equivalents
+ 10. Verify theme: switch to dark mode
+ - Module UI should respect dark theme
+ 11. Deactivate the module:
+ - POST http://localhost:3001/modules/{domaincheck-id}/deactivate
+ - Refresh /modules/domain-tools — domaincheck card should disappear
+ - Direct access to /modules/domain-tools/domaincheck should be blocked (403 from API)
+
+
+ .planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md,
+ .planning/phases/03-module-system-domaincheck/03-02-SUMMARY.md,
+ .planning/phases/03-module-system-domaincheck/03-03-SUMMARY.md
+
+
+ - Module registry shows domaincheck in database
+ - Admin can activate/deactivate without restart (MOD-03)
+ - Category page renders with lazy-loaded cards (MOD-04)
+ - Domaincheck accepts input and returns colored results (DCHK-01, DCHK-02, DCHK-03)
+ - Deactivated module is inaccessible
+ - i18n and theming work correctly
+
+ Type "approved" or describe issues to fix
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| N/A | Verification-only plan, no new code |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Disposition | Mitigation Plan |
+|-----------|----------|-----------|-------------|-----------------|
+| T-03-12 | N/A | N/A | accept | No new attack surface — verification only |
+
+
+
+Human confirms all 11 verification steps pass.
+
+
+
+- Human types "approved" after verifying all success criteria from Phase 3 roadmap
+- Or provides specific issues that will generate gap closure plans
+
+
+## Artifacts this phase produces
+
+| Symbol | Location | Type |
+|--------|----------|------|
+| (none — verification only) | | |
+
+