Files
tessera-ctl/.planning/phases/01-foundation-portal-shell/01-01-PLAN.md
T
schalli fc5a6f3832 docs(01): create phase 1 plans -- walking skeleton + portal shell
Three plans for Foundation & Portal Shell phase:
- 01-01: Monorepo scaffold, Docker Compose with 3-network segmentation, NestJS API, Next.js app, PostgreSQL
- 01-02: Design tokens (yellow #ffed00 primary), i18n (DE/EN), theme switching, responsive portal layout
- 01-03: Visual verification checkpoint for human approval

Includes SKELETON.md documenting architectural decisions for subsequent phases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 09:37:08 +02:00

20 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
01-foundation-portal-shell 01 execute 1
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
true
INFRA-01
INFRA-02
INFRA-03
truths artifacts key_links
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)
path provides contains
docker-compose.yml Multi-service orchestration with 3 networks internal: true
path provides exports
apps/api/src/health/health.controller.ts Health check endpoint
HealthController
path provides min_lines
apps/web/src/app/page.tsx Root page rendering 5
path provides contains
pnpm-workspace.yaml Monorepo workspace definition apps/*
path provides contains
apps/api/prisma/schema.prisma Database schema foundation datasource db
from to via pattern
docker-compose.yml apps/api/Dockerfile build context build.*apps/api
from to via pattern
docker-compose.yml apps/web/Dockerfile build context build.*apps/web
from to via pattern
apps/api/src/main.ts apps/api/src/app.module.ts NestFactory.create 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.

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

<threat_model>

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

<success_criteria>

  • 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 </success_criteria>
Create `.planning/phases/01-foundation-portal-shell/01-01-SUMMARY.md` when done