Files
coding-starter/CLAUDE.md

8.2 KiB

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

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 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:
// src/server/db/tenant.ts
import { db } from './client'

export async function withTenant<T>(
  tenantId: string,
  fn: () => Promise<T>
): Promise<T> {
  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/
// 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
// src/lib/result.ts
export type Result<T, E = Error> =
  | { ok: true; data: T }
  | { ok: false; error: E }

export const ok = <T>(data: T): Result<T> => ({ ok: true, data })
export const err = <E = Error>(error: E): Result<never, E> => ({ ok: false, error })

// Verwendung:
async function sendEmail(jobId: string): Promise<Result<void>> {
  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