5415d552bb
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
906 lines
40 KiB
Markdown
906 lines
40 KiB
Markdown
# 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)
|