# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. --- # Newsletter-App (DACH Email Marketing SaaS) ## Projektübersicht Self-hosted Newsletter- und E-Mail-Marketing-Plattform als KlickTipp-Alternative für den DACH-Markt. Multi-Tenant SaaS, eigener MTA auf Hetzner (iRedMail-basiert), volle Kontrolle über Deliverability. --- ## Tech Stack - **Frontend:** Next.js 14 (App Router), TypeScript (strict), Tailwind CSS, shadcn/ui - **Queue:** BullMQ + Redis (E-Mail-Versand-Jobs, Retry-Logic) - **Datenbank primär:** PostgreSQL — schema-per-tenant - **Datenbank Analytics:** ClickHouse (Opens, Clicks, Bounces, Unsubscribes — Event-basiert) - **MTA:** Selbst-gehostet auf Hetzner (Postfix/iRedMail), SMTP-Integration via API - **Auth:** Authentik (OIDC) - **Infra:** Docker Compose, Hetzner Cloud, Zoraxy Reverse Proxy - **CI/CD:** Forgejo Actions --- ## Befehle ```bash pnpm dev # Dev-Server starten pnpm build # Produktions-Build pnpm lint # Linting pnpm test # Alle Tests pnpm test src/queues/email-send.test.ts # Einzelnen Test ausführen # Test-Infrastruktur (Mailhog 1025/8025, ClickHouse 9000, Redis 6380) docker compose -f docker-compose.test.yml up -d docker compose -f docker-compose.test.yml down ``` --- ## Prompt-Vorlagen Für wiederkehrende Aufgaben (neue Feature, Bug-Fix, Migration, DSGVO-Review) stehen fertige Prompts in [`PROMPT.md`](./PROMPT.md) bereit. --- ## Erlaubte Aktionen (autonom) - Code schreiben und refactorn - Tests schreiben (unit + integration) - Datenbankmigrationen erstellen (PostgreSQL UND ClickHouse — NICHT ausführen) ## Verbotene Aktionen (immer Rückfrage) - E-Mails gegen echte Empfänger senden — NIEMALS - `docker compose up/down/restart` — NIEMALS ohne explizite Freigabe - Migrations ausführen - Redis-Queue leeren oder Jobs löschen - Postfix/SMTP-Konfiguration ändern - Suppression-Listen oder Bounce-Daten löschen - `.env` oder Secrets überschreiben - Git push / force push --- ## Datenbankregeln ### PostgreSQL (Operational Data) - Schema-per-Tenant — Tenant-Context wird über `SET search_path` gesetzt - **Pflicht-Pattern für jeden DB-Zugriff** — Helper in `src/server/db/tenant.ts`: ```typescript // src/server/db/tenant.ts import { db } from './client' export async function withTenant( tenantId: string, fn: () => Promise ): Promise { const schema = `tenant_${tenantId}` await db.execute(`SET search_path = ${schema}, public`) try { return await fn() } finally { await db.execute(`SET search_path = public`) } } // Verwendung — IMMER so, niemals direkter Query ohne Tenant-Context: const result = await withTenant(tenantId, () => db.query('SELECT * FROM subscribers WHERE email = $1', [email]) ) ``` - Migrations in `migrations/pg/`, Naming: `YYYY-MM-DD_beschreibung.sql` - Keine Migrations ausführen — nur erstellen - Additive Schema-Änderungen zuerst (kein Breaking Change ohne explizite Freigabe) ### ClickHouse (Analytics Data) - Client: `@clickhouse/client` (npm) — Instanz in `src/server/clickhouse/client.ts` - Nur INSERT und SELECT — kein DELETE/UPDATE ohne explizite Freigabe - Event-Tabellen sind append-only: `email_events` (event_type, tenant_id, campaign_id, recipient_hash, timestamp) - Recipient-Daten in ClickHouse immer als Hash (SHA256) — keine Klartext-E-Mail-Adressen - Migrations in `migrations/ch/` ```typescript // src/server/clickhouse/client.ts import { createClient } from '@clickhouse/client' export const clickhouse = createClient({ url: process.env.CLICKHOUSE_URL, username: process.env.CLICKHOUSE_USER, password: process.env.CLICKHOUSE_PASSWORD, database: 'newsletter', }) ``` --- ## E-Mail / MTA Regeln - **Kein direktes SMTP** aus dem Code ohne Rate-Limiting via BullMQ-Queue - Jeder Versand-Job muss: Tenant-Check → Suppression-Check → Queue → SMTP - Bounces und Unsubscribes werden sofort in Suppression-Liste geschrieben - DKIM/SPF-Validierung ist Infra-Aufgabe — nicht im Code lösen - Test-Modus: immer gegen Mailhog/MailDev, nie gegen echte SMTP-Credentials --- ## BullMQ / Redis Regeln - Job-Definitionen in `src/queues/` - Jeder Job hat: `maxAttempts: 3`, exponentielles Backoff, Dead-Letter-Queue - Keine Jobs direkt in Redis manipulieren — immer über BullMQ API - Queue-Namen: `email:send`, `email:bounce`, `analytics:ingest` --- ## Coding Conventions - Named exports, keine default exports - `interface` vor `type` für Objekt-Shapes - Fehlerbehandlung: Result-Pattern aus `src/lib/result.ts` — kein nacktes try/catch ```typescript // src/lib/result.ts export type Result = | { ok: true; data: T } | { ok: false; error: E } export const ok = (data: T): Result => ({ ok: true, data }) export const err = (error: E): Result => ({ ok: false, error }) // Verwendung: async function sendEmail(jobId: string): Promise> { const suppressed = await checkSuppression(email) if (suppressed) return err(new Error('Recipient suppressed')) // ... return ok(undefined) } ``` - Kein `any` in TypeScript - Logging: strukturiert (JSON), niemals E-Mail-Adressen im Klartext loggen - Kommentare auf Deutsch für Business-Logik (Deliverability, DSGVO), sonst Englisch --- ## Dateistruktur ``` src/ ├── app/ # Next.js App Router (Pages, Layouts, API Routes) │ ├── api/ # API Routes (Webhooks, REST-Endpunkte) │ └── (dashboard)/ # Authenticated App-Bereich ├── components/ # React-Komponenten (shadcn-basiert, wiederverwendbar) │ ├── ui/ # shadcn primitives (Button, Input, Dialog …) │ └── email/ # Domain-spezifische Komponenten (CampaignCard …) ├── lib/ # Shared Utilities │ ├── result.ts # Result-Pattern (siehe unten) │ ├── crypto.ts # SHA256-Hashing für E-Mail-Adressen │ └── validation.ts # Zod-Schemas ├── server/ # Server-only Code (nie im Client importieren) │ ├── db/ # PostgreSQL-Client + Tenant-Context-Helper │ ├── clickhouse/ # ClickHouse-Client (@clickhouse/client) │ ├── smtp/ # SMTP-Client (nodemailer, nur via Queue aufgerufen) │ └── auth/ # Authentik OIDC-Integration ├── queues/ # BullMQ Job-Definitionen und Worker │ ├── email-send.queue.ts │ ├── email-bounce.queue.ts │ └── analytics-ingest.queue.ts ├── analytics/ # ClickHouse Query-Layer (nur SELECT/INSERT) └── types/ # Globale TypeScript-Typen und Interfaces migrations/ ├── pg/ # PostgreSQL Migrations (YYYY-MM-DD_beschreibung.sql) └── ch/ # ClickHouse Migrations ``` --- ## Testregeln - Queue-Jobs bekommen Unit-Tests mit gemocktem Redis - SMTP-Calls immer gegen Mailhog mocken — nie echte Adressen - ClickHouse-Queries gegen Test-Container testen - Testframework: Vitest - Vor Fertigstellung: `pnpm test` + `pnpm lint` --- ## Deliverability-Regeln (kritisch) - Suppression-Check ist PFLICHT vor jedem Versand — kein Opt-out-Empfänger darf E-Mail erhalten - Bounce-Rate-Monitoring: bei >5% Rate Kampagne automatisch pausieren - Unsubscribe-Links müssen One-Click sein (RFC 8058) - List-Unsubscribe-Header in jedem Mailing setzen --- ## DSGVO / Compliance - Double-Opt-In ist Pflicht — kein Single-Opt-In implementieren - Consent-Timestamps mit IP und User-Agent speichern - Datenlöschung: Subscriber-Löschung löscht auch ClickHouse-Events (via tenant_id, nicht recipient_hash allein) - Keine personenbezogenen Daten außerhalb der EU speichern (Hetzner DE) --- ## Was Claude IMMER tun soll 1. Vor jeder größeren Änderung Plan beschreiben und Freigabe abwarten 2. Bei Versand-Logik: Suppression-Check explizit zeigen 3. Bei DB-Änderungen: Migration zeigen, nicht ausführen 4. Bei Queue-Änderungen: Auswirkung auf laufende Jobs beschreiben 5. Nach jeder Aufgabe: kurze Zusammenfassung was geändert wurde und warum 6. Ausgaben immer in Deutscher Sprache