Files
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

355 lines
20 KiB
Markdown

---
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.
<objective>
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.
</objective>
<execution_context>
@/home/vicolab/.claude/gsd-core/workflows/execute-plan.md
@/home/vicolab/.claude/gsd-core/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation-portal-shell/01-RESEARCH.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Monorepo scaffold with root configs and shared package</name>
<files>
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
</files>
<read_first>
.planning/phases/01-foundation-portal-shell/01-RESEARCH.md (Pattern 6: Turborepo Configuration, Project Structure, Installation section)
</read_first>
<action>
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.
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>Monorepo root with pnpm workspace, Turborepo, Biome, TypeScript base config, and shared package all type-check successfully.</done>
</task>
<task type="auto">
<name>Task 2: NestJS API with health endpoint and Prisma schema</name>
<files>
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
</files>
<read_first>
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)
</read_first>
<action>
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).
</action>
<verify>
<automated>cd /home/vicolab/projects/tessera-ctl && pnpm install && pnpm turbo type-check --filter=@tessera/api 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>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.</done>
</task>
<task type="auto">
<name>Task 3: Next.js app + Docker Compose stack with network segmentation</name>
<files>
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
</files>
<read_first>
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)
</read_first>
<action>
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.
</action>
<verify>
<automated>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</automated>
</verify>
<acceptance_criteria>
- 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)
</acceptance_criteria>
<done>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.</done>
</task>
</tasks>
## 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>
<verification>
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
</verification>
<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>
<output>
Create `.planning/phases/01-foundation-portal-shell/01-01-SUMMARY.md` when done
</output>