242 lines
8.2 KiB
Markdown
242 lines
8.2 KiB
Markdown
# 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<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/`
|
|
|
|
```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<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
|
|
|