docs(phase-1): research foundation & portal shell domain

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 09:27:41 +02:00
parent 173856e508
commit 5415d552bb
@@ -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>
## 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):**
```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 (
<button onClick={() => switchLocale(currentLocale === 'de' ? 'en' : 'de')}>
{currentLocale === 'de' ? 'EN' : 'DE'}
</button>
);
}
```
**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 `<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
```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 (
<html lang={locale} suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<NextIntlClientProvider messages={messages}>
{children}
</NextIntlClientProvider>
</ThemeProvider>
</body>
</html>
);
}
```
### 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<SidebarState>()(
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)