Files
tessera-ctl/.planning/phases/03-module-system-domaincheck/03-01-PLAN.md
T
schalli 89f559ce62 docs(03): create phase plan for Module System & Domaincheck
4 plans across 3 waves: SDK + registry (W1), Domaincheck module +
lazy loading (W2), visual verification (W3). Covers MOD-01..04,
DCHK-01..03.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 12:13:20 +02:00

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
03-module-system-domaincheck 01 execute 1
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
true
MOD-01
MOD-02
MOD-03
truths artifacts key_links
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
path provides exports
packages/module-sdk/src/types.ts TesseraModule interface, ModuleRoute interface
TesseraModule
ModuleRoute
ModuleCategory
path provides exports
apps/api/src/module-registry/module-registry.service.ts CRUD for module registry + per-tenant activation
ModuleRegistryService
path provides exports
apps/api/src/module-registry/module.guard.ts Guard checking module activation per tenant
ModuleGuard
from to via pattern
apps/api/src/module-registry/module-registry.service.ts prisma.module Prisma client queries prisma.module.(find|create|update)
from to via pattern
apps/api/src/module-registry/module.guard.ts ModuleRegistryService DI injection, checks activation 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.

<execution_context> @/home/vicolab/.claude/gsd-core/workflows/execute-plan.md @/home/vicolab/.claude/gsd-core/templates/summary.md </execution_context>

@.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.

<threat_model>

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
</threat_model>
- 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

<success_criteria>

  • 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 </success_criteria>

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
Create `.planning/phases/03-module-system-domaincheck/03-01-SUMMARY.md` when done