--- phase: 01-foundation-portal-shell plan: 01 type: execute wave: 1 depends_on: [] files_modified: - package.json - pnpm-workspace.yaml - turbo.json - tsconfig.base.json - biome.json - .gitignore - .env - .env.example - .dockerignore - docker-compose.yml - docker-compose.dev.yml - packages/shared/package.json - packages/shared/tsconfig.json - packages/shared/src/index.ts - apps/api/package.json - apps/api/tsconfig.json - apps/api/src/main.ts - apps/api/src/app.module.ts - apps/api/src/health/health.module.ts - apps/api/src/health/health.controller.ts - apps/api/prisma/schema.prisma - apps/api/Dockerfile - apps/web/package.json - apps/web/tsconfig.json - apps/web/next.config.ts - apps/web/postcss.config.mjs - apps/web/src/app/layout.tsx - apps/web/src/app/page.tsx - apps/web/src/app/globals.css - apps/web/Dockerfile autonomous: true requirements: - INFRA-01 - INFRA-02 - INFRA-03 must_haves: truths: - "docker compose up starts all services without errors" - "Browser request to localhost:80 returns a rendered Next.js page" - "GET http://localhost:80/api/health returns JSON with status ok" - "PostgreSQL container passes pg_isready health check" - "Next.js container cannot directly reach PostgreSQL (network segmentation)" artifacts: - path: "docker-compose.yml" provides: "Multi-service orchestration with 3 networks" contains: "internal: true" - path: "apps/api/src/health/health.controller.ts" provides: "Health check endpoint" exports: ["HealthController"] - path: "apps/web/src/app/page.tsx" provides: "Root page rendering" min_lines: 5 - path: "pnpm-workspace.yaml" provides: "Monorepo workspace definition" contains: "apps/*" - path: "apps/api/prisma/schema.prisma" provides: "Database schema foundation" contains: "datasource db" key_links: - from: "docker-compose.yml" to: "apps/api/Dockerfile" via: "build context" pattern: "build.*apps/api" - from: "docker-compose.yml" to: "apps/web/Dockerfile" via: "build context" pattern: "build.*apps/web" - from: "apps/api/src/main.ts" to: "apps/api/src/app.module.ts" via: "NestFactory.create" pattern: "NestFactory\\.create.*AppModule" --- ## Phase Goal **As a** user, **I want to** access a running portal application with responsive layout, theme switching, and bilingual interface, **so that** I have the structural frame into which all workflow modules will be placed. Walking Skeleton: Establish the complete Docker Compose infrastructure with pnpm monorepo, NestJS API (health endpoint + Prisma), Next.js frontend (minimal page), PostgreSQL database, and Traefik reverse proxy -- all connected via three segregated Docker networks per INFRA-01, INFRA-02, INFRA-03. Purpose: Prove the full stack works end-to-end. After this plan, `docker compose up` serves a web page via Traefik, the API responds, and PostgreSQL is healthy -- the foundation every subsequent phase builds upon. Output: Running Docker Compose stack accessible at localhost:80, monorepo with build tooling, both apps containerized. @/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/01-foundation-portal-shell/01-RESEARCH.md Task 1: Monorepo scaffold with root configs and shared package package.json pnpm-workspace.yaml turbo.json tsconfig.base.json biome.json .gitignore .env .env.example .dockerignore packages/shared/package.json packages/shared/tsconfig.json packages/shared/src/index.ts .planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Pattern 6: Turborepo Configuration, Project Structure, Installation section) Initialize the monorepo root. Run `corepack enable && corepack prepare pnpm@9 --activate` to enable pnpm. Create root `package.json` with: - `"name": "tessera"`, `"private": true` - `"packageManager": "pnpm@9.15.0"` - `"scripts"`: `"dev": "turbo dev"`, `"build": "turbo build"`, `"lint": "turbo lint"`, `"type-check": "turbo type-check"` - devDependencies: `turbo`, `@biomejs/biome`, `typescript` Create `pnpm-workspace.yaml` with packages: `["apps/*", "packages/*"]`. Create `turbo.json` per RESEARCH Pattern 6: tasks for build (dependsOn ^build, outputs dist/**/**.next/**), dev (cache false, persistent true), lint (outputs []), type-check (dependsOn ^build, outputs []). Create `tsconfig.base.json` with compilerOptions: strict true, esModuleInterop true, skipLibCheck true, forceConsistentCasingInFileNames true, resolveJsonModule true, isolatedModules true, moduleResolution "bundler", module "ESNext", target "ES2022", declaration true, declarationMap true, sourceMap true, composite false. Create `biome.json` with $schema, organizeImports enabled, formatter (indentStyle "space", indentWidth 2, lineWidth 100), linter enabled with recommended rules. Create `.gitignore` covering: node_modules, .next, dist, .turbo, .env (NOT .env.example), *.log, .DS_Store, coverage, .pnpm-store. Create `.env` with DB_PASSWORD=tessera_dev, DATABASE_URL=postgresql://tessera:tessera_dev@db:5432/tessera, NODE_ENV=development. Create `.env.example` as template (same keys, placeholder values). Create `.dockerignore` excluding: node_modules, .next, dist, .turbo, .git, .env, *.md, coverage. Create `packages/shared/package.json` with name "@tessera/shared", version "0.0.1", main "src/index.ts", types "src/index.ts", scripts (type-check: tsc --noEmit), devDependencies typescript. Create `packages/shared/tsconfig.json` extending ../../tsconfig.base.json with include ["src"]. Create `packages/shared/src/index.ts` exporting a simple APP_NAME constant "Tessera" and a HealthResponse type interface with status string and timestamp string fields. Run `pnpm install` at root to generate lockfile. cd /home/vicolab/projects/tessera-ctl && test -f pnpm-lock.yaml && test -f turbo.json && test -f packages/shared/src/index.ts && pnpm turbo type-check --filter=@tessera/shared 2>&1 | tail -5 - pnpm-lock.yaml exists at root (proves pnpm install succeeded) - turbo.json contains "tasks" key with "build", "dev", "lint", "type-check" - packages/shared/src/index.ts exports APP_NAME and HealthResponse interface - `pnpm turbo type-check --filter=@tessera/shared` exits with code 0 - tsconfig.base.json has "strict": true - biome.json has "formatter" and "linter" sections - .env contains DATABASE_URL with postgresql:// prefix Monorepo root with pnpm workspace, Turborepo, Biome, TypeScript base config, and shared package all type-check successfully. Task 2: NestJS API with health endpoint and Prisma schema apps/api/package.json apps/api/tsconfig.json apps/api/nest-cli.json apps/api/src/main.ts apps/api/src/app.module.ts apps/api/src/health/health.module.ts apps/api/src/health/health.controller.ts apps/api/prisma/schema.prisma apps/api/Dockerfile packages/shared/src/index.ts (HealthResponse type) .planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Pattern 5: NestJS Docker Build, NestJS Health Endpoint, Standard Stack) Create `apps/api/package.json` with name "@tessera/api", scripts: "build": "nest build", "start": "node dist/main.js", "start:dev": "nest start --watch", "type-check": "tsc --noEmit". Dependencies: @nestjs/core@^11, @nestjs/common@^11, @nestjs/platform-express@^11, @nestjs/config@^4, rxjs@^7, reflect-metadata@^0.2, @prisma/client@^7. DevDependencies: prisma@^7, @nestjs/cli@^11, typescript@^5.5, @types/node@^22, @types/express@^5. Create `apps/api/tsconfig.json` extending ../../tsconfig.base.json with compilerOptions: module "commonjs", target "ES2021", outDir "./dist", rootDir "./src", emitDecoratorMetadata true, experimentalDecorators true. Include ["src/**/*"], exclude ["node_modules", "dist"]. Create `apps/api/nest-cli.json` with $schema, collection @nestjs/schematics, sourceRoot "src", compilerOptions deleteOutDir true. Create `apps/api/src/main.ts`: import NestFactory from @nestjs/core, import AppModule. Create app with NestFactory.create(AppModule), enable CORS (origin: true for dev), listen on port 3001. Log "Tessera API running on port 3001". Create `apps/api/src/app.module.ts`: NestJS module importing ConfigModule.forRoot() (isGlobal: true) and HealthModule. Create `apps/api/src/health/health.module.ts`: NestJS module declaring and exporting HealthController. Create `apps/api/src/health/health.controller.ts`: Controller with prefix "health", single GET handler returning { status: "ok", timestamp: new Date().toISOString() } matching the HealthResponse interface from @tessera/shared (import the type for documentation but return plain object -- NestJS serializes it). Create `apps/api/prisma/schema.prisma`: datasource db (provider "postgresql", url env("DATABASE_URL")), generator client (provider "prisma-client-js"). Add a minimal Tenant model with id (uuid, default uuid()), name (String), slug (String, unique), createdAt (DateTime, default now()), updatedAt (DateTime, updatedAt) -- this seeds the multi-tenancy foundation. Create `apps/api/Dockerfile` per RESEARCH Pattern 5: multi-stage (base with corepack/pnpm, deps stage, builder stage running pnpm build, runner stage as non-root user "nestjs" exposing port 3001, CMD node dist/main.js). Use monorepo root as build context -- COPY the full workspace, install, then build only api. Actually for the Dockerfile, since we use monorepo root context: COPY root package.json, pnpm-workspace.yaml, pnpm-lock.yaml, then apps/api/ and packages/shared/, then pnpm install --frozen-lockfile --filter=@tessera/api..., then pnpm --filter=@tessera/api build. Run `cd apps/api && pnpm install` (from monorepo root `pnpm install` should suffice since workspace). cd /home/vicolab/projects/tessera-ctl && pnpm install && pnpm turbo type-check --filter=@tessera/api 2>&1 | tail -5 - apps/api/src/main.ts imports NestFactory and calls listen(3001) - apps/api/src/health/health.controller.ts has @Controller('health') and @Get() decorators - apps/api/prisma/schema.prisma contains "datasource db" with provider "postgresql" - apps/api/prisma/schema.prisma contains "model Tenant" with id, name, slug fields - apps/api/Dockerfile has USER nestjs and EXPOSE 3001 - `pnpm turbo type-check --filter=@tessera/api` exits with code 0 - apps/api/package.json has @nestjs/core and @prisma/client in dependencies NestJS API compiles, has a /health endpoint returning {status, timestamp}, Prisma schema defines PostgreSQL datasource with Tenant model, and Dockerfile is ready for multi-stage build. Task 3: Next.js app + Docker Compose stack with network segmentation apps/web/package.json apps/web/tsconfig.json apps/web/next.config.ts apps/web/postcss.config.mjs apps/web/src/app/layout.tsx apps/web/src/app/page.tsx apps/web/src/app/globals.css apps/web/Dockerfile docker-compose.yml docker-compose.dev.yml apps/api/Dockerfile (to understand build context pattern) .planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Pattern 1: Docker Network Segmentation, Pattern 4: Next.js Docker Build, docker-compose.yml example) Create `apps/web/package.json` with name "@tessera/web", scripts: "dev": "next dev --turbopack", "build": "next build", "start": "next start", "type-check": "tsc --noEmit". Dependencies: next@^16, react@^19, react-dom@^19. DevDependencies: tailwindcss@^4, @tailwindcss/postcss@^4, postcss@^8, typescript@^5.5, @types/react@^19, @types/react-dom@^19. Create `apps/web/tsconfig.json` extending ../../tsconfig.base.json with compilerOptions: jsx "preserve", lib ["dom", "dom.iterable", "esnext"], module "esnext", moduleResolution "bundler", allowJs true, noEmit true, incremental true, plugins [{ name: "next" }], paths {"@/*": ["./src/*"]}. Include ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]. Create `apps/web/next.config.ts` with output: "standalone" as const. Minimal for now -- next-intl plugin added in Plan 02. Create `apps/web/postcss.config.mjs` exporting plugins: {"@tailwindcss/postcss": {}}. Create `apps/web/src/app/globals.css` with just `@import "tailwindcss";` -- full design tokens added in Plan 02. Create `apps/web/src/app/layout.tsx`: basic RootLayout with html (lang="de"), body with className for basic font. Import globals.css. Export metadata with title "Tessera" and description. Create `apps/web/src/app/page.tsx`: simple page rendering an h1 "Tessera" and a paragraph indicating the portal is loading. This is the skeleton placeholder -- Plan 02 replaces with full portal shell. Create `apps/web/Dockerfile` per RESEARCH Pattern 4: multi-stage (base with corepack/pnpm, deps, builder with standalone output, runner as non-root "nextjs" user, EXPOSE 3000, CMD node server.js). Use monorepo root context same strategy as API: COPY root configs, apps/web/, packages/shared/, install filtered, build. Create `docker-compose.yml` per RESEARCH Pattern 1 with three networks: - services: traefik (image traefik:v3.4, ports 80:80, command --api.insecure=true --providers.docker=true --providers.docker.exposedbydefault=false, networks frontend-net, volumes /var/run/docker.sock:/var/run/docker.sock:ro) - web (build context . dockerfile apps/web/Dockerfile, networks frontend-net + backend-net, depends_on api condition service_healthy, labels traefik.enable=true + traefik.http.routers.web.rule=PathPrefix(/) + traefik.http.services.web.loadbalancer.server.port=3000 + traefik.http.routers.web.priority=1) - api (build context . dockerfile apps/api/Dockerfile, networks backend-net + data-net, depends_on db condition service_healthy, environment DATABASE_URL from .env, healthcheck curl -f http://localhost:3001/health interval 10s timeout 5s retries 3, labels traefik.enable=true + traefik.http.routers.api.rule=PathPrefix(/api) + traefik.http.services.api.loadbalancer.server.port=3001 + traefik.http.routers.api.priority=2 + traefik.http.middlewares.api-strip.stripprefix.prefixes=/api + traefik.http.routers.api.middlewares=api-strip) - db (image postgres:16-alpine, networks data-net, volumes pgdata:/var/lib/postgresql/data, environment POSTGRES_USER=tessera POSTGRES_PASSWORD=${DB_PASSWORD:-tessera_dev} POSTGRES_DB=tessera, healthcheck pg_isready -U tessera interval 5s timeout 3s retries 5) - networks: frontend-net (bridge), backend-net (bridge), data-net (bridge, internal: true) - volumes: pgdata Create `docker-compose.dev.yml` with volume mounts for hot reload: web volumes ./apps/web/src:/app/apps/web/src, api volumes ./apps/api/src:/app/apps/api/src. Override web command to "pnpm --filter @tessera/web dev" and api command to "pnpm --filter @tessera/api start:dev". Run `pnpm install` at root to install web dependencies. Then run `docker compose build` to verify images build successfully. Then `docker compose up -d` and verify with curl. cd /home/vicolab/projects/tessera-ctl && docker compose build --no-cache 2>&1 | tail -10 && docker compose up -d && sleep 15 && curl -s http://localhost:80 | grep -o "Tessera" | head -1 && curl -s http://localhost:80/api/health | grep -o '"status":"ok"' && docker compose exec db pg_isready -U tessera && docker compose down - docker compose build completes without errors for all services (web, api, db, traefik) - docker compose up -d starts all 4 containers (traefik, web, api, db) - curl http://localhost:80 returns HTML containing "Tessera" - curl http://localhost:80/api/health returns JSON with "status":"ok" - docker compose exec db pg_isready -U tessera exits with code 0 - docker-compose.yml defines three networks: frontend-net, backend-net, data-net - data-net has "internal: true" (PostgreSQL unreachable from outside) - web service is on frontend-net and backend-net (NOT data-net) - api service is on backend-net and data-net (NOT frontend-net) Full Docker Compose stack runs: Traefik proxies to Next.js (port 80) and NestJS API (/api/*), PostgreSQL is healthy on internal network, network segmentation enforced. ## Artifacts This Phase Produces | Symbol/File | Type | Consumed By | |---|---|---| | `docker-compose.yml` | Docker Compose config | All subsequent phases (service foundation) | | `docker-compose.dev.yml` | Dev overrides | Local development workflow | | `pnpm-workspace.yaml` | Monorepo config | All package operations | | `turbo.json` | Build orchestration | All build/dev/lint commands | | `tsconfig.base.json` | TypeScript config | All apps and packages | | `biome.json` | Linter/formatter config | All code quality checks | | `@tessera/shared` (packages/shared) | Shared types package | apps/web, apps/api | | `HealthResponse` type | Interface | apps/api health controller | | `APP_NAME` constant | String export | UI components | | `apps/api/src/main.ts` | NestJS entrypoint | Docker CMD | | `apps/api/src/health/health.controller.ts` | Health endpoint | Docker healthcheck, monitoring | | `apps/api/prisma/schema.prisma` | Database schema | Prisma migrations, Phase 2 RLS | | `apps/web/src/app/layout.tsx` | Root layout | All pages (Plan 02 adds providers) | | `apps/web/src/app/page.tsx` | Dashboard page | Plan 02 replaces content | | `apps/web/Dockerfile` | Frontend image | docker-compose.yml | | `apps/api/Dockerfile` | Backend image | docker-compose.yml | ## Trust Boundaries | Boundary | Description | |----------|-------------| | Browser -> Traefik | External traffic enters Docker stack | | Traefik -> Next.js | Reverse proxy to frontend container | | Next.js -> NestJS | Frontend calls backend API | | NestJS -> PostgreSQL | Backend accesses database on internal network | | Docker socket -> Traefik | Container management API exposure | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-01-01 | Elevation of Privilege | Docker socket in Traefik | mitigate | Mount /var/run/docker.sock:ro (read-only) | | T-01-02 | Information Disclosure | PostgreSQL port | mitigate | data-net has internal:true, no port mapping to host | | T-01-03 | Information Disclosure | .env file with DB credentials | mitigate | .env in .gitignore, .env.example has placeholder values only | | T-01-04 | Elevation of Privilege | Container running as root | mitigate | Explicit USER directive (nextjs/nestjs) in both Dockerfiles, no sudo | | T-01-05 | Tampering | Dependency confusion | mitigate | pnpm strict mode, lockfile committed, --frozen-lockfile in Docker builds | | T-01-SC | Tampering | npm installs | mitigate | All packages pass legitimacy audit in RESEARCH.md; no [ASSUMED]/[SUS] packages requiring human checkpoint | 1. `docker compose up -d` starts all services without error 2. `curl http://localhost:80` returns rendered HTML with "Tessera" 3. `curl http://localhost:80/api/health` returns `{"status":"ok","timestamp":"..."}` 4. `docker compose exec db pg_isready -U tessera` exits 0 5. `docker compose exec web ping db` fails (network segmentation working) 6. `pnpm turbo type-check` passes for all packages - Docker Compose stack starts with 4 healthy containers (traefik, web, api, db) - Browser at localhost:80 shows rendered Next.js page - API health endpoint returns 200 with JSON body - PostgreSQL is isolated on internal network - Monorepo builds and type-checks cleanly - All files follow the project structure defined in RESEARCH.md Create `.planning/phases/01-foundation-portal-shell/01-01-SUMMARY.md` when done