Files
2026-06-18 09:27:41 +02:00

40 KiB

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>

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

<phase_requirements>

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

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

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

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.

# 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 [CITED: docs.docker.com]

What: Locale resolved from cookie, no [locale] URL prefix. When to use: Portal/SPA apps where locale is a user preference.

// 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
  };
});
// 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 (
    <button onClick={() => switchLocale(currentLocale === 'de' ? 'en' : 'de')}>
      {currentLocale === 'de' ? 'EN' : 'DE'}
    </button>
  );
}

Source: next-intl: 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.

/* 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 [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.

# 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 [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.

# 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 [CITED: tomray.dev]

Pattern 6: Turborepo Configuration

What: Monorepo task orchestration with proper dependency graph. When to use: Root-level build/dev/lint commands.

// 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": []
    }
  }
}
# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

Source: Turborepo: 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 <head> 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

// 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 (
    <html lang={locale} suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          <NextIntlClientProvider messages={messages}>
            {children}
          </NextIntlClientProvider>
        </ThemeProvider>
      </body>
    </html>
  );
}

Zustand Sidebar Store

// 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<SidebarState>()(
  persist(
    (set) => ({
      isCollapsed: false,
      isMobileOpen: false,
      toggle: () => set((state) => ({ isCollapsed: !state.isCollapsed })),
      setMobileOpen: (open) => set({ isMobileOpen: open }),
    }),
    { name: 'tessera-sidebar' }
  )
);

NestJS Health Endpoint

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

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

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

Secondary (MEDIUM confidence)

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)