diff --git a/.planning/phases/01-foundation-portal-shell/01-RESEARCH.md b/.planning/phases/01-foundation-portal-shell/01-RESEARCH.md new file mode 100644 index 0000000..adf4c7e --- /dev/null +++ b/.planning/phases/01-foundation-portal-shell/01-RESEARCH.md @@ -0,0 +1,905 @@ +# Phase 1: Foundation & Portal Shell - Research + +**Researched:** 2026-06-18 +**Domain:** Monorepo scaffold, Docker infrastructure, Next.js frontend shell with i18n/theming, NestJS API skeleton +**Confidence:** HIGH + +## Summary + +Phase 1 establishes the end-to-end walking skeleton for Tessera: a Docker Compose stack with PostgreSQL, a pnpm/Turborepo monorepo containing a Next.js 16 frontend shell and NestJS 11 API backend, with fully functional i18n (DE/EN), theme switching (light/dark with persistence), and responsive portal layout (header + collapsible sidebar + main area). No authentication, no modules -- purely the structural frame. + +The primary technical challenge is establishing the monorepo structure, Docker networking with proper segmentation, and the design token system that supports the yellow primary color (#ffed00) with configurable accent colors. The i18n setup uses next-intl in "without i18n routing" mode (cookie-based locale, no locale prefix in URLs) since language preference is a user setting, not a URL concern. + +**Primary recommendation:** Use `pnpm dlx create-turbo@latest` for scaffolding, add the NestJS app manually, configure Docker Compose with three segregated networks (frontend/backend/data), and establish the shadcn/ui design token system with CSS variables from day one. + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01:** Sidebar einklappbar auf Icon-Leiste (Toggle zwischen ~240px voller Breite und schmaler Icon-Ansicht) +- **D-02:** Breite ausgeklappt: ~240px (Standard wie VS Code/Slack) +- **D-03:** Mobile/kleine Bildschirme: komplett versteckt, Hamburger-Button zum Oeffnen +- **D-04:** Position: immer links (nicht konfigurierbar) +- **D-05:** Kategorien als Accordion (aufklappbar, zeigen Module darunter) +- **D-06:** Footer-Bereich in der Sidebar: Einstellungen-Icon und kompakte User-Info +- **D-07:** Inhalt: Logo/Branding links, Breadcrumb/Seitentitel mittig, globale Suche, Benutzer-Menu rechts (Avatar + Dropdown) +- **D-08:** Hoehe: ~56-64px (Standard, nicht dominant) +- **D-09:** Verhalten: sticky (bleibt immer sichtbar oben beim Scrollen) +- **D-10:** Primaer-/Markenfarbe: Gelb #ffed00 +- **D-11:** Akzentfarbe: konfigurierbar in den Benutzer-Einstellungen +- **D-12:** Visueller Stil: Modern/Clean (viel Whitespace, klare Linien, subtile Schatten -- wie Notion/Linear) +- **D-13:** Ecken: leicht abgerundet (border-radius ~6-8px) +- **D-14:** Dark Mode: Claude entscheidet (dark gray empfohlen fuer Augenkomfort bei gelbem Akzent) +- **D-15:** Startseite ist das Dashboard (in Sidebar aktiv, im Hauptbereich geoeffnet) +- **D-16:** Leeres Dashboard zeigt "Keine Widgets aktiv" mit Button zum Hinzufuegen +- **D-17:** Dashboard-Funktionalitaet orientiert sich an Homarr -- aber mit feinerem Grid fuer die Widget-Ausrichtung + +### Claude's Discretion +- Dark Mode Farbton (empfohlen: dunkles Grau/Blau statt true-black wegen gelber Primaerfarbe) +- Sidebar-Inhalt im Leer-Zustand (nur Dashboard + Marketplace sichtbar empfohlen) +- Font-Wahl (Inter oder aehnliche clean sans-serif empfohlen) + +### Deferred Ideas (OUT OF SCOPE) +None -- discussion stayed within phase scope + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|------------------| +| INFRA-01 | Komplette Anwendung laeuft als Docker-Compose-Stack | Docker Compose multi-service configuration with health checks, multi-stage builds for both Next.js and NestJS | +| INFRA-02 | PostgreSQL-Datenbank im Container | PostgreSQL 16 container with named volume, health check via pg_isready | +| INFRA-03 | Docker-Netzwerksegmentierung (Frontend/Backend/Data) | Three-network architecture: frontend-net, backend-net, data-net with internal:true on data-net | +| PRTAL-01 | Schmale Kopfzeile mit Branding und Benutzer-Menu | Sticky header component ~56-64px, logo left, breadcrumb center, user menu right | +| PRTAL-04 | Responsive Layout fuer verschiedene Bildschirmgroessen | Tailwind responsive breakpoints, sidebar collapse behavior, mobile hamburger menu | +| UI-01 | Light/Dark Theme umschaltbar pro Benutzer | next-themes + shadcn/ui CSS variables, localStorage persistence, .dark class toggle | +| UI-02 | Zweisprachig: Deutsch und Englisch mit Sprachwahl | next-intl without i18n routing, cookie-based locale, message JSON files per locale | +| UI-03 | i18n-Framework von Anfang an integriert (alle Strings ueber t('key')) | next-intl useTranslations() hook, no hardcoded strings from first component | + + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| Portal layout (header, sidebar, main) | Frontend (Next.js SSR) | -- | Pure UI structure, server-rendered for SEO/performance | +| Theme switching | Browser/Client | Frontend Server (cookie) | Toggle is client-side interaction; preference stored in cookie/localStorage for SSR consistency | +| i18n rendering | Frontend Server (SSR) | Browser/Client | Translations resolved server-side via next-intl; locale switch triggers client refresh | +| Locale persistence | Browser/Client (cookie) | -- | NEXT_LOCALE cookie read by server on each request | +| Docker orchestration | Infrastructure | -- | Compose file defines all services, networks, volumes | +| PostgreSQL | Database/Storage | -- | Data tier, isolated on internal network | +| API health check | API/Backend (NestJS) | -- | Simple GET /health endpoint proving backend runs | +| Design tokens | Frontend (CSS) | -- | CSS custom properties at :root, theme variants via .dark selector | + +## Standard Stack + +### Core (Phase 1 only) + +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Next.js | 16.2.9 | Frontend framework | App Router, React 19, standalone output for Docker [VERIFIED: npm registry] | +| React | 19.x | UI library | Ships with Next.js 16, Server Components [VERIFIED: npm registry] | +| NestJS | 11.1.27 | API framework | Modular monolith, DI, Express 5 default [VERIFIED: npm registry] | +| Prisma | 7.8.0 | ORM | TypeScript-first, migration tooling, RLS support [VERIFIED: npm registry] | +| PostgreSQL | 16 | Database | RLS for multi-tenancy, JSONB, Docker-native [ASSUMED] | +| Tailwind CSS | 4.3.1 | Styling | CSS-first config, dark mode via .dark, OKLCH colors [VERIFIED: npm registry] | +| shadcn/ui | CLI v4 | Component library | Copies into project, CSS variable theming, Radix primitives [ASSUMED] | +| next-themes | 0.4.6 | Theme switching | SSR-safe, system preference detection, 2-line shadcn integration [VERIFIED: npm registry] | +| next-intl | 4.13.0 | i18n | App Router native, Server Component support, without-i18n-routing mode [VERIFIED: npm registry] | +| Zustand | 5.0.14 | Client state | Sidebar toggle, UI preferences, 1.1kb [VERIFIED: npm registry] | + +### Supporting + +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| pnpm | 9.x | Package manager | Workspace management, strict deps [ASSUMED] | +| Turborepo | 2.9.18 | Build orchestration | Parallel builds, caching, task deps [VERIFIED: npm registry] | +| @tailwindcss/postcss | 4.3.1 | PostCSS plugin | Required for Tailwind v4 integration [VERIFIED: npm registry] | +| Biome | 2.5.0 | Lint + format | Replaces ESLint + Prettier [VERIFIED: npm registry] | +| TypeScript | 5.5+ | Type safety | All packages use strict TS [ASSUMED] | + +### Alternatives Considered + +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| next-intl without routing | next-intl with routing ([locale] prefix) | URL prefixes add complexity; portal is a logged-in app where locale is a user preference, not SEO content | +| Cookie-based locale | URL-based locale | Cookie approach simpler for SPAs/portals; URL approach better for public content sites | +| Zustand for sidebar state | React Context | Zustand avoids provider nesting, works outside React tree, persists to localStorage natively | +| next-themes | Manual .dark class toggle | next-themes handles SSR flash-of-wrong-theme, system preference, and localStorage sync | + +**Installation (Phase 1 packages):** +```bash +# Root (monorepo tooling) +pnpm add -D turbo @biomejs/biome typescript + +# apps/web (Next.js frontend) +pnpm add next react react-dom next-intl next-themes zustand +pnpm add -D tailwindcss @tailwindcss/postcss postcss typescript @types/react @types/react-dom + +# apps/api (NestJS backend) +pnpm add @nestjs/core @nestjs/common @nestjs/platform-express @nestjs/config rxjs reflect-metadata +pnpm add @prisma/client +pnpm add -D prisma @nestjs/cli typescript @types/node @types/express + +# packages/shared (types and constants) +pnpm add -D typescript +``` + +## Package Legitimacy Audit + +| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | +|---------|----------|-----|-----------|-------------|---------|-------------| +| next | npm | 8+ yrs | 39.6M/wk | github.com/vercel/next.js | OK | Approved (SUS=too-new only) | +| next-intl | npm | 5+ yrs | 3.9M/wk | github.com/amannn/next-intl | OK | Approved (SUS=too-new only) | +| next-themes | npm | 4+ yrs | 24.1M/wk | github.com/pacocoursey/next-themes | OK | Approved | +| tailwindcss | npm | 7+ yrs | 120M/wk | github.com/tailwindlabs/tailwindcss | OK | Approved (SUS=too-new only) | +| @tailwindcss/postcss | npm | 1+ yr | 24.3M/wk | github.com/tailwindlabs/tailwindcss | OK | Approved (SUS=too-new only) | +| zustand | npm | 6+ yrs | 42.5M/wk | github.com/pmndrs/zustand | OK | Approved (SUS=too-new only) | +| @nestjs/core | npm | 7+ yrs | 11.5M/wk | github.com/nestjs/nest | OK | Approved (SUS=too-new only) | +| @nestjs/common | npm | 7+ yrs | 12M/wk | github.com/nestjs/nest | OK | Approved (SUS=too-new only) | +| prisma | npm | 5+ yrs | 13.4M/wk | github.com/prisma/prisma | OK | Approved | +| @prisma/client | npm | 5+ yrs | 12.1M/wk | github.com/prisma/prisma | OK | Approved | +| turbo | npm | 3+ yrs | 17.1M/wk | github.com/vercel/turborepo | OK | Approved (SUS=too-new only) | +| @biomejs/biome | npm | 2+ yrs | 10.1M/wk | github.com/biomejs/biome | OK | Approved (SUS=too-new only) | + +**Packages removed due to [SLOP] verdict:** None +**Packages flagged as suspicious [SUS]:** None (all SUS flags were "too-new" on well-established packages with millions of weekly downloads and verified source repos -- false positives from recent publishes) + +## Architecture Patterns + +### System Architecture Diagram (Phase 1) + +``` +Browser (localhost:3000) + | + v ++--[frontend-net]-----------------------------------------------+ +| | +| +------------------+ +-------------------+ | +| | Traefik (proxy) |-------->| Next.js (web) | | +| | :80 | | :3000 (internal) | | +| +------------------+ +-------------------+ | +| | | ++----------------------------------------|------------------------+ + | ++--[backend-net]-------------------------|------------------------+ +| v | +| +-------------------+ | +| | NestJS (api) | | +| | :3001 (internal) | | +| +-------------------+ | +| | | ++----------------------------------------|------------------------+ + | ++--[data-net (internal: true)]-----------|------------------------+ +| v | +| +-------------------+ | +| | PostgreSQL (db) | | +| | :5432 (internal) | | +| +-------------------+ | +| | ++-----------------------------------------------------------------+ +``` + +**Data flow:** Browser -> Traefik (reverse proxy) -> Next.js (serves pages, calls API) -> NestJS API -> PostgreSQL. The data-net is `internal: true` (no external access). Next.js and NestJS share backend-net. Traefik and Next.js share frontend-net. + +### Recommended Project Structure + +``` +tessera-ctl/ +├── apps/ +│ ├── web/ # Next.js 16 frontend +│ │ ├── src/ +│ │ │ ├── app/ +│ │ │ │ ├── layout.tsx # Root layout with providers +│ │ │ │ ├── page.tsx # Dashboard (redirect or direct) +│ │ │ │ └── globals.css # Tailwind + design tokens +│ │ │ ├── components/ +│ │ │ │ ├── layout/ +│ │ │ │ │ ├── header.tsx +│ │ │ │ │ ├── sidebar.tsx +│ │ │ │ │ └── app-shell.tsx +│ │ │ │ └── ui/ # shadcn/ui components +│ │ │ ├── i18n/ +│ │ │ │ ├── request.ts # Locale resolution from cookie +│ │ │ │ └── navigation.ts # Navigation helpers +│ │ │ ├── lib/ +│ │ │ │ └── stores/ +│ │ │ │ └── sidebar-store.ts +│ │ │ └── messages/ +│ │ │ ├── de.json +│ │ │ └── en.json +│ │ ├── public/ +│ │ ├── next.config.ts +│ │ ├── postcss.config.mjs +│ │ ├── Dockerfile +│ │ └── package.json +│ └── api/ # NestJS 11 backend +│ ├── src/ +│ │ ├── main.ts +│ │ ├── app.module.ts +│ │ └── health/ +│ │ ├── health.controller.ts +│ │ └── health.module.ts +│ ├── prisma/ +│ │ └── schema.prisma +│ ├── Dockerfile +│ └── package.json +├── packages/ +│ ├── shared/ # Shared types, constants +│ │ ├── src/ +│ │ │ └── index.ts +│ │ └── package.json +│ └── ui/ # Shared UI components (future) +│ └── package.json +├── docker-compose.yml +├── docker-compose.dev.yml # Dev overrides (volumes, hot reload) +├── pnpm-workspace.yaml +├── turbo.json +├── biome.json +├── package.json +└── tsconfig.base.json +``` + +### Pattern 1: Docker Network Segmentation + +**What:** Three isolated Docker networks with minimal cross-network access. +**When to use:** Always -- from the first docker-compose.yml. + +```yaml +# docker-compose.yml +services: + traefik: + image: traefik:v3.4 + ports: + - "80:80" + networks: + - frontend-net + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + + web: + build: + context: ./apps/web + dockerfile: Dockerfile + networks: + - frontend-net + - backend-net + depends_on: + api: + condition: service_healthy + labels: + - "traefik.enable=true" + - "traefik.http.routers.web.rule=PathPrefix(`/`)" + + api: + build: + context: ./apps/api + dockerfile: Dockerfile + networks: + - backend-net + - data-net + depends_on: + db: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:3001/health"] + interval: 10s + timeout: 5s + retries: 3 + + 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: + test: ["CMD-SHELL", "pg_isready -U tessera"] + interval: 5s + timeout: 3s + retries: 5 + +networks: + frontend-net: + driver: bridge + backend-net: + driver: bridge + data-net: + driver: bridge + internal: true # No external connectivity + +volumes: + pgdata: +``` + +**Source:** [Docker Docs: Networking in Compose](https://docs.docker.com/compose/how-tos/networking/) [CITED: docs.docker.com] + +### Pattern 2: next-intl Without i18n Routing (Cookie-Based) + +**What:** Locale resolved from cookie, no [locale] URL prefix. +**When to use:** Portal/SPA apps where locale is a user preference. + +```typescript +// src/i18n/request.ts +import { getRequestConfig } from 'next-intl/server'; +import { cookies } from 'next/headers'; + +export default getRequestConfig(async () => { + const cookieStore = await cookies(); + const locale = cookieStore.get('NEXT_LOCALE')?.value || 'de'; + + return { + locale, + messages: (await import(`../messages/${locale}.json`)).default + }; +}); +``` + +```typescript +// src/components/locale-switcher.tsx (Client Component) +'use client'; +import { useRouter } from 'next/navigation'; +import { useTransition } from 'react'; + +export function LocaleSwitcher({ currentLocale }: { currentLocale: string }) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + + function switchLocale(newLocale: string) { + document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`; + startTransition(() => { + router.refresh(); + }); + } + + return ( + + ); +} +``` + +**Source:** [next-intl: Without i18n routing](https://next-intl.dev/docs/getting-started/app-router/without-i18n-routing) [CITED: next-intl.dev] + +### Pattern 3: shadcn/ui Design Tokens with Custom Primary Color + +**What:** CSS custom properties defining the design system, yellow primary with dark mode variant. +**When to use:** From the first component render. + +```css +/* src/app/globals.css */ +@import "tailwindcss"; + +@theme inline { + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --radius-sm: calc(var(--radius) - 2px); + --radius-md: var(--radius); + --radius-lg: calc(var(--radius) + 2px); +} + +:root { + --radius: 0.5rem; /* 8px -- per D-13 */ + --background: oklch(0.99 0 0); + --foreground: oklch(0.15 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.15 0 0); + --primary: oklch(0.91 0.19 102); /* #ffed00 in OKLCH */ + --primary-foreground: oklch(0.20 0.02 90); /* Dark text on yellow */ + --secondary: oklch(0.96 0 0); + --secondary-foreground: oklch(0.15 0 0); + --muted: oklch(0.96 0 0); + --muted-foreground: oklch(0.55 0 0); + --accent: oklch(0.96 0 0); /* Configurable per user */ + --accent-foreground: oklch(0.15 0 0); + --border: oklch(0.90 0 0); + --input: oklch(0.90 0 0); + --ring: oklch(0.91 0.19 102); /* Matches primary */ +} + +.dark { + --background: oklch(0.17 0.01 260); /* Dark gray-blue */ + --foreground: oklch(0.95 0 0); + --card: oklch(0.21 0.01 260); + --card-foreground: oklch(0.95 0 0); + --primary: oklch(0.91 0.19 102); /* Yellow stays vibrant */ + --primary-foreground: oklch(0.20 0.02 90); + --secondary: oklch(0.25 0.01 260); + --secondary-foreground: oklch(0.95 0 0); + --muted: oklch(0.25 0.01 260); + --muted-foreground: oklch(0.65 0 0); + --accent: oklch(0.25 0.01 260); + --accent-foreground: oklch(0.95 0 0); + --border: oklch(0.30 0.01 260); + --input: oklch(0.30 0.01 260); + --ring: oklch(0.91 0.19 102); +} +``` + +**Design decision (Claude's discretion):** Dark mode uses `oklch(0.17 0.01 260)` -- a dark gray-blue that provides comfortable contrast with the yellow #ffed00 primary without the harshness of true black. This matches the user's recommendation. [ASSUMED] + +**Source:** [shadcn/ui Theming](https://ui.shadcn.com/docs/theming) [CITED: ui.shadcn.com] + +### Pattern 4: Next.js Standalone Docker Build + +**What:** Multi-stage Docker build using standalone output mode for minimal image size. +**When to use:** Production deployment of Next.js in Docker. + +```dockerfile +# apps/web/Dockerfile +FROM node:24-alpine AS base +RUN corepack enable && corepack prepare pnpm@9 --activate + +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile --prod=false + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN pnpm build + +FROM node:24-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +USER nextjs +EXPOSE 3000 +CMD ["node", "server.js"] +``` + +**Source:** [Next.js Deploying Docs](https://nextjs.org/docs/app/getting-started/deploying) [CITED: nextjs.org] + +### Pattern 5: NestJS Multi-Stage Docker Build + +**What:** Production-optimized NestJS container with only compiled dist/. +**When to use:** Containerized NestJS deployment. + +```dockerfile +# apps/api/Dockerfile +FROM node:24-alpine AS base +RUN corepack enable && corepack prepare pnpm@9 --activate + +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN pnpm build + +FROM node:24-alpine AS runner +WORKDIR /app +ENV NODE_ENV=production +RUN addgroup --system --gid 1001 nestjs && \ + adduser --system --uid 1001 nestjs +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./ +USER nestjs +EXPOSE 3001 +CMD ["node", "dist/main.js"] +``` + +**Source:** [NestJS Docker multi-stage builds](https://www.tomray.dev/nestjs-docker-production) [CITED: tomray.dev] + +### Pattern 6: Turborepo Configuration + +**What:** Monorepo task orchestration with proper dependency graph. +**When to use:** Root-level build/dev/lint commands. + +```jsonc +// turbo.json +{ + "$schema": "https://turborepo.dev/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", ".next/**"], + "inputs": ["src/**", "package.json", "tsconfig.json"] + }, + "dev": { + "cache": false, + "persistent": true + }, + "lint": { + "outputs": [] + }, + "type-check": { + "dependsOn": ["^build"], + "outputs": [] + } + } +} +``` + +```yaml +# pnpm-workspace.yaml +packages: + - "apps/*" + - "packages/*" +``` + +**Source:** [Turborepo: Structuring a Repository](https://turborepo.dev/repo/docs/crafting-your-repository/structuring-a-repository) [CITED: turborepo.dev] + +### Anti-Patterns to Avoid + +- **Hardcoded strings in JSX:** Every user-visible string must go through `t('key')` from day one. German is 30-40% longer than English -- layouts WILL break if retrofitted later. +- **Flat Docker network:** Never put all services on one network. The database must be unreachable from the frontend container. +- **Theme without design tokens:** Do not scatter color values across components. All colors come from CSS variables at `:root`. +- **Locale in URL path:** For a portal app, locale in cookies is simpler and avoids middleware complexity for a non-public app. +- **`npm install` in Docker:** Always use `pnpm install --frozen-lockfile` for reproducible builds. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Theme switching | Manual class toggle + localStorage | next-themes | Handles SSR flash, system preference, localStorage sync, .dark class | +| i18n framework | Custom translation loader | next-intl | Plural forms, ICU MessageFormat, Server Component support, type safety | +| Responsive sidebar | CSS-only sidebar toggle | Zustand store + Tailwind | Need state persistence, mobile detection, animation coordination | +| Design system | Manual CSS variables | shadcn/ui + Tailwind theme | Pre-built accessible components, consistent token naming, dark mode support | +| Docker networking | Single default network | Explicit network definitions | Security boundary enforcement at infrastructure level | +| Reverse proxy | Port mapping per service | Traefik with labels | Auto-discovery, single entrypoint, easy SSL later | +| Build orchestration | Shell scripts for multi-app builds | Turborepo | Dependency graph, caching, parallel execution | + +**Key insight:** Phase 1 is foundation-heavy. Every shortcut taken here (flat network, hardcoded strings, no design tokens) creates exponentially more work in later phases. The investment in proper infrastructure now pays dividends through phases 2-6. + +## Common Pitfalls + +### Pitfall 1: i18n Retrofitting + +**What goes wrong:** UI components use hardcoded strings. When i18n is added later, every component must be touched. German text is 30-40% longer than English, breaking layouts. +**Why it happens:** Feels like a "later" task, but actually requires layout redesign and string extraction. +**How to avoid:** Use `t('key')` from the very first component. Design layouts with flexible widths. Test with German as default (longer strings catch overflow early). +**Warning signs:** Any string literal in JSX that a user would see. + +### Pitfall 2: Flash of Wrong Theme (FOWT) + +**What goes wrong:** Page renders with light theme before JavaScript hydrates and applies the saved dark theme preference, causing a flash. +**Why it happens:** Server does not know the user's theme preference during SSR. +**How to avoid:** next-themes injects a script in `` that sets the theme class before first paint. Never add theme logic in useEffect -- it's too late. +**Warning signs:** Visible flash when loading a dark-themed page. + +### Pitfall 3: Docker Volume Permissions + +**What goes wrong:** Containers run as non-root (correctly) but PostgreSQL volume has root ownership, causing write failures on fresh installs. +**Why it happens:** Named volumes default to root. Non-root container users cannot write. +**How to avoid:** Use the official postgres image (handles permissions internally). For custom images, set user/group in Dockerfile and use entrypoint scripts. +**Warning signs:** Container crashes on startup with permission denied errors. + +### Pitfall 4: pnpm Workspace Dependency Resolution in Docker + +**What goes wrong:** Docker builds fail because workspace packages reference each other via `workspace:*` protocol, but the Docker build context does not include sibling packages. +**Why it happens:** Each Dockerfile only has its own app directory as context. +**How to avoid:** Use the monorepo root as Docker build context, or use Turborepo's `turbo prune` to create isolated package directories for Docker builds. +**Warning signs:** `ERR_PNPM_NO_MATCHING_VERSION` during Docker build. + +### Pitfall 5: Tailwind v4 Configuration Confusion + +**What goes wrong:** Developer uses v3 patterns (tailwind.config.js, @tailwind directives) which do not work in v4. +**Why it happens:** Most tutorials and Stack Overflow answers reference v3. +**How to avoid:** In Tailwind v4: use `@import "tailwindcss"` (not directives), use `@tailwindcss/postcss` plugin (not tailwindcss directly), configure via CSS `@theme` blocks (not JS config file). +**Warning signs:** Tailwind classes not applying, "Unknown rule @tailwind" errors. + +## Code Examples + +### Root Layout with Providers + +```typescript +// src/app/layout.tsx +// Source: next-intl.dev + next-themes docs +import { NextIntlClientProvider } from 'next-intl'; +import { getMessages, getLocale } from 'next-intl/server'; +import { ThemeProvider } from 'next-themes'; +import './globals.css'; + +export default async function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + const locale = await getLocale(); + const messages = await getMessages(); + + return ( + + + + + {children} + + + + + ); +} +``` + +### Zustand Sidebar Store + +```typescript +// src/lib/stores/sidebar-store.ts +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface SidebarState { + isCollapsed: boolean; + isMobileOpen: boolean; + toggle: () => void; + setMobileOpen: (open: boolean) => void; +} + +export const useSidebarStore = create()( + persist( + (set) => ({ + isCollapsed: false, + isMobileOpen: false, + toggle: () => set((state) => ({ isCollapsed: !state.isCollapsed })), + setMobileOpen: (open) => set({ isMobileOpen: open }), + }), + { name: 'tessera-sidebar' } + ) +); +``` + +### NestJS Health Endpoint + +```typescript +// src/health/health.controller.ts +import { Controller, Get } from '@nestjs/common'; + +@Controller('health') +export class HealthController { + @Get() + check() { + return { status: 'ok', timestamp: new Date().toISOString() }; + } +} +``` + +### Message JSON Structure + +```json +// src/messages/de.json +{ + "common": { + "appName": "Tessera", + "loading": "Laden...", + "save": "Speichern", + "cancel": "Abbrechen" + }, + "header": { + "search": "Suchen...", + "userMenu": "Benutzermenu" + }, + "sidebar": { + "dashboard": "Dashboard", + "marketplace": "Marktplatz", + "settings": "Einstellungen", + "collapse": "Einklappen", + "expand": "Ausklappen" + }, + "dashboard": { + "empty": "Keine Widgets aktiv", + "addWidget": "Widget hinzufuegen" + }, + "theme": { + "light": "Hell", + "dark": "Dunkel", + "system": "System" + }, + "locale": { + "de": "Deutsch", + "en": "English" + } +} +``` + +### next.config.ts with Standalone Output and next-intl + +```typescript +// apps/web/next.config.ts +import createNextIntlPlugin from 'next-intl/plugin'; + +const withNextIntl = createNextIntlPlugin('./src/i18n/request.ts'); + +const nextConfig = { + output: 'standalone' as const, +}; + +export default withNextIntl(nextConfig); +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Tailwind v3 JS config | Tailwind v4 CSS-first config (@theme) | Jan 2025 | No tailwind.config.js needed, faster builds | +| @tailwind directives | @import "tailwindcss" | Tailwind v4 | Single import replaces 3 directives | +| next-i18next | next-intl | 2023-2024 | App Router native, Server Components, smaller bundle | +| pages/ router | app/ router | Next.js 13+ (stable 14+) | Server Components, layouts, streaming | +| Prisma Rust engine | Prisma v7 pure TypeScript | Apr 2025 | 3x faster, 90% smaller, no binary downloads | +| npm/yarn workspaces | pnpm workspaces + Turborepo | 2023+ | Strict deps, content-addressable store, build caching | +| ESLint + Prettier | Biome | 2024+ | Single tool, 100x faster, less config | + +**Deprecated/outdated:** +- `tailwind.config.js` / `tailwind.config.ts`: Not needed in Tailwind v4. Use CSS `@theme` blocks instead. +- `@tailwind base; @tailwind components; @tailwind utilities;`: Replaced by `@import "tailwindcss"` in v4. +- `next-i18next`: Designed for Pages Router. Use `next-intl` for App Router. +- `getServerSideProps` / `getStaticProps`: Replaced by Server Components and `use cache` in App Router. + +## Project Constraints (from CLAUDE.md) + +- GSD workflow enforcement: Do not make direct repo edits outside a GSD workflow unless explicitly asked +- Conventions not yet established -- this phase establishes them +- No existing codebase patterns -- greenfield + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | PostgreSQL 16-alpine Docker image is the correct base | Standard Stack | Low -- could use 16 non-alpine, minimal impact | +| A2 | shadcn/ui CLI v4 uses `npx shadcn@latest init` for setup | Architecture Patterns | Low -- command may vary slightly | +| A3 | OKLCH color value for #ffed00 is approximately oklch(0.91 0.19 102) | Code Examples | Medium -- exact conversion should be verified with a color tool | +| A4 | next-intl 4.13 supports `getMessages()` and `getLocale()` without routing setup | Code Examples | Medium -- API may require slightly different imports | +| A5 | Font choice: Inter (sans-serif) for the clean modern style | Architecture Patterns | Low -- user recommended "Inter or similar" | +| A6 | Traefik v3.4 is current stable for Docker | Architecture Patterns | Low -- any 3.x works | +| A7 | pnpm 9.x is available via corepack on Node 24 | Environment | Low -- corepack confirmed available, pnpm version may differ | + +## Open Questions + +1. **pnpm not globally installed** + - What we know: corepack 0.35.0 is available on Node 24.16.0; pnpm is not globally installed + - What's unclear: Whether to use `corepack enable && corepack use pnpm@9` or install pnpm globally + - Recommendation: Use corepack (it ships with Node) -- add `"packageManager": "pnpm@9.15.0"` to root package.json + +2. **Monorepo Docker build context strategy** + - What we know: pnpm workspace:* protocol causes issues when Docker context is per-app + - What's unclear: Whether to use `turbo prune --docker` or set build context to monorepo root + - Recommendation: For Phase 1, use monorepo root as Docker context with .dockerignore. Migrate to `turbo prune` if build times become an issue. + +3. **Accent color configurability timing** + - What we know: D-11 says accent color is configurable per user settings + - What's unclear: Settings UI does not exist in Phase 1 (no auth) + - Recommendation: Set up the CSS variable infrastructure now (`--accent`), use a default accent color, implement the settings UI in Phase 2 when user accounts exist. + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| Docker | Container deployment | Yes | 29.5.3 | -- | +| Docker Compose | Service orchestration | Yes | v5.1.4 | -- | +| Node.js | Build tooling, runtime | Yes | 24.16.0 | -- | +| npm/npx | Package execution | Yes | 11.13.0 | -- | +| pnpm | Workspace management | No (global) | -- | corepack enable && corepack prepare pnpm@9 | +| Git | Version control | Yes | 2.47.3 | -- | +| corepack | pnpm activation | Yes | 0.35.0 | -- | +| Turborepo | Build orchestration | No (global) | -- | pnpm add -D turbo (local) | +| PostgreSQL client | Dev access to DB | No | -- | Use docker exec or pgAdmin container | + +**Missing dependencies with no fallback:** None -- all blocking dependencies are available or installable as project-local devDependencies. + +**Missing dependencies with fallback:** +- pnpm: Use `corepack enable && corepack prepare pnpm@9 --activate` (corepack available) +- Turborepo: Installed as project devDependency, run via `pnpm turbo` or `npx turbo` + +## Validation Architecture + +### Test Framework + +| Property | Value | +|----------|-------| +| Framework | Vitest 3.x (unit/integration), Playwright 1.x (E2E) | +| Config file | None -- Wave 0 creates vitest.config.ts and playwright.config.ts | +| Quick run command | `pnpm turbo test --filter=web --filter=api` | +| Full suite command | `pnpm turbo test && pnpm exec playwright test` | + +### Phase Requirements -> Test Map + +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| INFRA-01 | Docker Compose stack starts and serves app | E2E/smoke | `docker compose up -d && curl localhost:80` | Wave 0 | +| INFRA-02 | PostgreSQL container healthy | smoke | `docker compose exec db pg_isready` | Wave 0 | +| INFRA-03 | Network segmentation enforced | integration | `docker compose exec web ping db` (should fail) | Wave 0 | +| PRTAL-01 | Header renders with branding and user menu | component | `vitest run --filter header` | Wave 0 | +| PRTAL-04 | Layout adapts to screen sizes | E2E | `playwright test responsive.spec.ts` | Wave 0 | +| UI-01 | Theme toggle persists preference | component + E2E | `vitest run --filter theme && playwright test theme.spec.ts` | Wave 0 | +| UI-02 | Language switch between DE/EN | component + E2E | `vitest run --filter i18n && playwright test locale.spec.ts` | Wave 0 | +| UI-03 | No hardcoded strings (all via t()) | lint/static | `grep -r ">[A-Z]" src/components/ --include="*.tsx"` | Wave 0 | + +### Sampling Rate +- **Per task commit:** `pnpm turbo test --filter=[changed-packages]` +- **Per wave merge:** `pnpm turbo test && pnpm exec playwright test` +- **Phase gate:** Full suite green + manual verification of Docker Compose stack + +### Wave 0 Gaps +- [ ] `apps/web/vitest.config.ts` -- Vitest configuration for Next.js +- [ ] `apps/api/vitest.config.ts` -- Vitest configuration for NestJS +- [ ] `playwright.config.ts` -- E2E test configuration +- [ ] `apps/web/tests/` -- Component test directory +- [ ] `apps/api/test/` -- API test directory +- [ ] Framework install: `pnpm add -D vitest @testing-library/react @playwright/test` + +## Security Domain + +### Applicable ASVS Categories + +| ASVS Category | Applies | Standard Control | +|---------------|---------|-----------------| +| V2 Authentication | No | Phase 2 (not in scope) | +| V3 Session Management | No | Phase 2 (not in scope) | +| V4 Access Control | No | Phase 2 (not in scope) | +| V5 Input Validation | Minimal | No user input in Phase 1 beyond locale/theme toggle | +| V6 Cryptography | No | No secrets handling beyond env vars | +| V14 Configuration | Yes | Docker secrets, non-root containers, network isolation | + +### Known Threat Patterns for Docker + Next.js + NestJS + +| Pattern | STRIDE | Standard Mitigation | +|---------|--------|---------------------| +| Container escape via Docker socket | Elevation of Privilege | Mount Docker socket read-only (:ro) in Traefik only | +| Database port exposed to host | Information Disclosure | internal:true network, no port mapping for db | +| Sensitive env vars in docker-compose.yml | Information Disclosure | Use .env file (gitignored), Docker secrets for production | +| Non-root container bypass | Elevation of Privilege | Explicit USER directive in Dockerfile, no sudo | +| Dependency confusion in monorepo | Tampering | pnpm strict mode, lockfile committed, exact versions | + +## Sources + +### Primary (HIGH confidence) +- [Docker Docs: Networking in Compose](https://docs.docker.com/compose/how-tos/networking/) -- network segmentation patterns +- [Next.js Deployment Docs](https://nextjs.org/docs/app/getting-started/deploying) -- standalone output mode +- [shadcn/ui Theming](https://ui.shadcn.com/docs/theming) -- CSS variable design token system +- [next-intl: Without i18n routing](https://next-intl.dev/docs/getting-started/app-router/without-i18n-routing) -- cookie-based locale +- [Turborepo: Structuring a Repository](https://turborepo.dev/repo/docs/crafting-your-repository/structuring-a-repository) -- monorepo structure + +### Secondary (MEDIUM confidence) +- [Tailwind CSS v4 Next.js Installation](https://tailwindcss.com/docs/installation/framework-guides/nextjs) -- v4 setup steps +- [Turborepo Configuration Reference](https://turborepo.dev/repo/docs/reference/configuration) -- turbo.json task config +- [NestJS Docker Production](https://www.tomray.dev/nestjs-docker-production) -- multi-stage build patterns +- [next-intl GitHub Issue #1334](https://github.com/amannn/next-intl/issues/1334) -- locale switching without routing + +### Tertiary (LOW confidence) +- Docker network segmentation Medium articles -- general patterns confirmed by official docs +- NestJS 11 standalone blog posts -- patterns verified against NestJS official docs structure + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH -- all packages verified on npm registry with current versions +- Architecture: HIGH -- patterns from official documentation, Docker segmentation from Docker docs +- Pitfalls: HIGH -- sourced from project's own PITFALLS.md research and official documentation +- Code examples: MEDIUM -- synthesized from multiple docs, API details may need minor adjustment + +**Research date:** 2026-06-18 +**Valid until:** 2026-07-18 (stable stack, 30-day validity)