# 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)