8.2 KiB
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
.envoder Secrets überschreiben- Git push / force push
Datenbankregeln
PostgreSQL (Operational Data)
- Schema-per-Tenant — Tenant-Context wird über
SET search_pathgesetzt - 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 insrc/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
interfacevortypefü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
anyin 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
- Vor jeder größeren Änderung Plan beschreiben und Freigabe abwarten
- Bei Versand-Logik: Suppression-Check explizit zeigen
- Bei DB-Änderungen: Migration zeigen, nicht ausführen
- Bei Queue-Änderungen: Auswirkung auf laufende Jobs beschreiben
- Nach jeder Aufgabe: kurze Zusammenfassung was geändert wurde und warum
- Ausgaben immer in Deutscher Sprache