chore: CLAUDE.md, PROMPT.md und Planungs-Docs hinzufügen
This commit is contained in:
241
CLAUDE.md
Normal file
241
CLAUDE.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# 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
|
||||
|
||||
219
PROMPT.md
Normal file
219
PROMPT.md
Normal file
@@ -0,0 +1,219 @@
|
||||
|
||||
## 1. Neue Kampagnen-Funktion
|
||||
|
||||
```
|
||||
Aufgabe: [Login] für Kampagnen implementieren
|
||||
|
||||
Kontext:
|
||||
- Tenant: [TENANT-SCHEMA oder "allgemein"]
|
||||
- Betroffene Queues: [z.B. email:send / keine]
|
||||
- ClickHouse betroffen: [Ja/Nein]
|
||||
|
||||
Schritt 1: Zeig mir welche Dateien du anlegen oder ändern würdest
|
||||
— Liste nur, kein Code
|
||||
|
||||
Schritt 2: Schreib die Typen und Interfaces
|
||||
|
||||
Schritt 3: Schreib die Business-Logik
|
||||
— mit Suppression-Check als erstem Schritt
|
||||
— mit Tenant-Isolation
|
||||
|
||||
Schritt 4: Schreib Unit-Tests — SMTP gegen Mailhog mocken
|
||||
|
||||
Schritt 5: Zeig mir Queue-Job-Änderungen falls nötig
|
||||
|
||||
Warte nach jedem Schritt auf Freigabe.
|
||||
Kein Versand gegen echte Empfänger. Kein Deploy.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. E-Mail Versand-Job (BullMQ)
|
||||
|
||||
```
|
||||
Aufgabe: BullMQ-Job für Kampagnen-Versand] implementieren
|
||||
|
||||
Schritt 1: Zeig mir die Job-Datenstruktur (Payload-Interface)
|
||||
und den Ablauf als Pseudocode — warte auf Freigabe
|
||||
|
||||
Schritt 2: Implementiere den Job in src/queues/
|
||||
— maxAttempts: 3, exponentielles Backoff
|
||||
— Dead-Letter-Queue bei finalem Fehler
|
||||
— Suppression-Check VOR SMTP-Call
|
||||
|
||||
Schritt 3: Schreib Unit-Tests mit gemocktem Redis und gemocktem SMTP
|
||||
|
||||
Schritt 4: Zeig mir wie der Job in die Queue eingereiht wird
|
||||
|
||||
Kein echter Redis-Eingriff. Kein SMTP gegen Produktion.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Subscriber-Management
|
||||
|
||||
```
|
||||
Aufgabe: [ CSV-Import / Segmentierung / Unsubscribe-Flow] implementieren
|
||||
|
||||
Schritt 1: Erkläre welche Tabellen betroffen sind (PostgreSQL-Schema)
|
||||
— ist Double-Opt-In betroffen? Ja/Nein
|
||||
|
||||
Schritt 2: Zeig mir die Validierungslogik
|
||||
— Pflichtfelder, E-Mail-Format, Duplikat-Check, Suppression-Check
|
||||
|
||||
Schritt 3: Schreib den Code — mit Consent-Timestamp-Speicherung (IP + User-Agent)
|
||||
|
||||
Schritt 4: Schreib die Migration falls Schema-Änderung nötig
|
||||
— nicht ausführen
|
||||
|
||||
Schritt 5: Schreib Unit-Tests
|
||||
|
||||
Keine E-Mail-Adressen im Klartext in ClickHouse speichern — nur SHA256-Hash.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. ClickHouse Analytics
|
||||
|
||||
```
|
||||
Aufgabe: [Open-Rate pro Kampagne] implementieren
|
||||
|
||||
Schritt 1: Zeig mir die ClickHouse-Tabellenstruktur die du nutzen würdest
|
||||
— warte auf Freigabe
|
||||
|
||||
Schritt 2: Schreib die Query
|
||||
— immer mit tenant_id als Filter
|
||||
— keine personenbezogenen Daten selektieren (nur Hashes)
|
||||
|
||||
Schritt 3: Schreib den Query-Layer in src/analytics/
|
||||
|
||||
Schritt 4: Schreib einen Test gegen ClickHouse-Test-Container
|
||||
|
||||
Kein DELETE/UPDATE ohne explizite Freigabe.
|
||||
ClickHouse-Tabellen sind append-only.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Datenbankänderung / Migration
|
||||
|
||||
```
|
||||
Aufgabe: [BESCHREIBUNG DER SCHEMAÄNDERUNG]
|
||||
|
||||
Welche DB: [PostgreSQL / ClickHouse / beide]
|
||||
|
||||
Schritt 1: Erkläre welche Tabellen betroffen sind
|
||||
— additiv oder breaking?
|
||||
|
||||
Schritt 2: Zeig mir das geänderte Schema — warte auf Freigabe
|
||||
|
||||
Schritt 3: Generiere die Migration-SQL
|
||||
— PostgreSQL → migrations/pg/YYYY-MM-DD_beschreibung.sql
|
||||
— ClickHouse → migrations/ch/YYYY-MM-DD_beschreibung.sql
|
||||
— NICHT ausführen
|
||||
|
||||
Schritt 4: Zeig mir welcher Code angepasst werden muss
|
||||
|
||||
Führe zu keinem Zeitpunkt Migrations aus.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Bounce / Unsubscribe Handling
|
||||
|
||||
```
|
||||
Aufgabe: [Bounce-Handler / Unsubscribe-Flow] implementieren oder erweitern
|
||||
|
||||
Schritt 1: Zeig mir den aktuellen Flow als Diagramm (Text reicht)
|
||||
— warte auf Freigabe
|
||||
|
||||
Schritt 2: Implementiere die Änderung
|
||||
— Suppression-Liste wird SOFORT geschrieben
|
||||
— kein async ohne Fehlerbehandlung
|
||||
|
||||
Schritt 3: Stelle sicher dass Bounce-Rate-Monitoring greift
|
||||
— bei >5% Kampagne pausieren
|
||||
|
||||
Schritt 4: One-Click-Unsubscribe Header prüfen (RFC 8058)
|
||||
|
||||
Schritt 5: Schreib Tests
|
||||
|
||||
Kein echter SMTP-Call. Kein Löschen aus Suppression-Liste.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. MTA / SMTP Integration
|
||||
|
||||
```
|
||||
Aufgabe: [SMTP-ÄNDERUNG z.B. neuen Versand-Domain einbinden]
|
||||
|
||||
Schritt 1: Erkläre was sich an der SMTP-Konfiguration ändern muss
|
||||
— zeige mir NUR den Code-Teil, nicht die Postfix-Config
|
||||
|
||||
Schritt 2: Schreib den SMTP-Client-Code
|
||||
— Rate-Limiting via BullMQ, kein direkter SMTP-Call
|
||||
|
||||
Schritt 3: Schreib einen Test gegen Mailhog
|
||||
|
||||
Postfix/iRedMail-Konfiguration ist Infra-Aufgabe — nicht im Code lösen.
|
||||
Kein SMTP gegen echte Empfänger.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Bug fixen
|
||||
|
||||
```
|
||||
Bug: [FEHLERBESCHREIBUNG]
|
||||
Fehler-Output: [Stacktrace oder Fehlermeldung]
|
||||
Kontext: [Queue-Job / API / Analytics / Frontend]
|
||||
|
||||
Schritt 1: Finde die Ursache — erkläre sie bevor du etwas änderst
|
||||
— warte auf Bestätigung
|
||||
|
||||
Schritt 2: Schreib einen Test der den Bug reproduziert (failing)
|
||||
|
||||
Schritt 3: Fix — Test muss grün werden
|
||||
|
||||
Schritt 4: Prüfe Seiteneffekte auf Queue-Jobs und Suppression-Liste
|
||||
|
||||
Kein Eingriff in laufende Queue-Jobs ohne Freigabe.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. DSGVO / Compliance Check
|
||||
|
||||
```
|
||||
Aufgabe: DSGVO-Review für [FEATURE oder DATEI]
|
||||
|
||||
Prüfe auf:
|
||||
- Werden E-Mail-Adressen irgendwo im Klartext geloggt?
|
||||
- Ist Double-Opt-In mit Timestamp, IP und User-Agent gespeichert?
|
||||
- Kann ein Tenant Subscriber-Daten eines anderen Tenants sehen?
|
||||
- Werden Daten außerhalb der EU gespeichert oder übertragen?
|
||||
- Ist die Lösch-Logik vollständig (PostgreSQL + ClickHouse)?
|
||||
|
||||
Ausgabe: Findings mit Schweregrad (kritisch / mittel / niedrig)
|
||||
Keine Änderungen ohne Freigabe.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Deliverability-Check
|
||||
|
||||
```
|
||||
Aufgabe: Deliverability-Review für [VERSAND-FEATURE oder KAMPAGNEN-CODE]
|
||||
|
||||
Prüfe auf:
|
||||
- Suppression-Check vorhanden und als erster Schritt?
|
||||
- List-Unsubscribe-Header gesetzt (RFC 8058)?
|
||||
- Bounce-Rate-Monitoring aktiv?
|
||||
- Versand-Rate limitiert (kein Burst)?
|
||||
- DKIM/SPF-Kontext korrekt gesetzt?
|
||||
|
||||
Ausgabe: Findings mit konkreten Code-Stellen
|
||||
Keine Änderungen ohne Freigabe.
|
||||
```
|
||||
|
||||
136
docs/plans/2026-04-17-kampagnen-design.md
Normal file
136
docs/plans/2026-04-17-kampagnen-design.md
Normal file
@@ -0,0 +1,136 @@
|
||||
# Kampagnen-Funktion — Design
|
||||
|
||||
**Datum:** 2026-04-17
|
||||
|
||||
---
|
||||
|
||||
## Ziel
|
||||
|
||||
Newsletter-SaaS-Plattform mit vollständiger Kampagnen-Funktion: einmaliger Versand, zeitgesteuerter Versand und event-basierte Automations — alle mit Tenant-Isolation, Suppression-Check und ClickHouse-Analytics.
|
||||
|
||||
---
|
||||
|
||||
## Entscheidungen
|
||||
|
||||
| Thema | Entscheidung |
|
||||
|---|---|
|
||||
| Kampagnen-Typen | Newsletter + Automation (beide gleichzeitig) |
|
||||
| Empfänger | Feste Liste ODER dynamisches Segment wählbar |
|
||||
| Inhaltsformate | HTML + Plain-Text (beide manuell pflegbar) |
|
||||
| Status-Workflow | `draft → scheduled → sending → sent` |
|
||||
| Versand-Zeitpunkt | Sofort, einmalig geplant, oder Cron-wiederkehrend |
|
||||
| Analytics | Opens, Clicks, Bounces, Unsubscribes (aggregiert) |
|
||||
| Automation-Trigger | Cron UND Event-Trigger kombinierbar |
|
||||
|
||||
---
|
||||
|
||||
## Datenmodell (PostgreSQL, schema-per-tenant)
|
||||
|
||||
```sql
|
||||
-- Kampagne
|
||||
campaigns (
|
||||
id UUID PK,
|
||||
name TEXT NOT NULL,
|
||||
subject TEXT NOT NULL,
|
||||
html_body TEXT NOT NULL,
|
||||
plain_body TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'draft',
|
||||
-- draft | scheduled | sending | sent | paused | cancelled
|
||||
scheduled_at TIMESTAMPTZ,
|
||||
cron_expression TEXT, -- NULL = einmalig
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)
|
||||
|
||||
-- Empfänger-Zuordnung (Liste ODER Segment — genau eines davon NOT NULL)
|
||||
campaign_recipients (
|
||||
id UUID PK,
|
||||
campaign_id UUID NOT NULL REFERENCES campaigns(id),
|
||||
list_id UUID NULL, -- feste Subscriber-Liste
|
||||
segment_id UUID NULL -- dynamisches Segment
|
||||
)
|
||||
|
||||
-- Trigger für Automations
|
||||
campaign_triggers (
|
||||
id UUID PK,
|
||||
campaign_id UUID NOT NULL REFERENCES campaigns(id),
|
||||
trigger_type TEXT NOT NULL, -- cron | event
|
||||
trigger_value TEXT NOT NULL -- Cron-Ausdruck oder Event-Name (z.B. "subscriber.optin")
|
||||
)
|
||||
```
|
||||
|
||||
**ClickHouse** (`email_events`, append-only):
|
||||
- Felder: `event_type`, `tenant_id`, `campaign_id`, `recipient_hash` (SHA256), `timestamp`
|
||||
- Kein Klartext-E-Mail in ClickHouse
|
||||
|
||||
---
|
||||
|
||||
## API Routes (Next.js App Router)
|
||||
|
||||
```
|
||||
POST /api/campaigns → Kampagne erstellen (Status: draft)
|
||||
GET /api/campaigns/:id → Kampagne abrufen
|
||||
PATCH /api/campaigns/:id → Inhalt / Status aktualisieren
|
||||
POST /api/campaigns/:id/send → Sofortversand auslösen
|
||||
POST /api/campaigns/:id/schedule → Zeitplan oder Cron setzen
|
||||
GET /api/campaigns/:id/analytics → Aggregierte ClickHouse-Daten
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Queue-Architektur (BullMQ)
|
||||
|
||||
**Versand-Flow:**
|
||||
```
|
||||
API → withTenant() → Suppression-Check
|
||||
→ BullMQ Job enqueuen (email:send)
|
||||
→ Job: Empfänger auflösen (Liste oder Segment)
|
||||
→ Pro Empfänger: Suppression-Check → SMTP
|
||||
→ ClickHouse INSERT (email_events)
|
||||
```
|
||||
|
||||
**Queues:**
|
||||
- `email:send` — Versand-Jobs (maxAttempts: 3, exponentielles Backoff, DLQ)
|
||||
- `campaign:trigger` — Event-basierte Automation-Auslösung
|
||||
- `analytics:scheduler` — Cron-Kampagnen-Prüfung (scheduled_at + cron_expression)
|
||||
|
||||
**Status-Übergänge:** Atomar in PostgreSQL mit Optimistic Locking — kein Doppelversand.
|
||||
|
||||
---
|
||||
|
||||
## Frontend (Next.js App Router)
|
||||
|
||||
**Seiten:**
|
||||
```
|
||||
/campaigns → Übersicht (CampaignCard-Liste, Status-Badge)
|
||||
/campaigns/new → Wizard: Name → Inhalt → Empfänger → Zeitplan → Review
|
||||
/campaigns/:id → Detail-Ansicht
|
||||
/campaigns/:id/edit → Bearbeiten (nur im draft-Status)
|
||||
/campaigns/:id/analytics → Aggregierte Analytics-Ansicht
|
||||
```
|
||||
|
||||
**Komponenten:**
|
||||
- `CampaignCard` — Status-Badge, Kurzinfo, Aktionen
|
||||
- `CampaignEditor` — HTML + Plain-Text Tabs
|
||||
- `RecipientPicker` — Toggle: Liste ODER Segment
|
||||
- `SchedulePicker` — Sofort / Datum+Uhrzeit / Cron-Ausdruck
|
||||
- `CampaignAnalytics` — Zahlen aus ClickHouse via API
|
||||
|
||||
**State:** Server Components + React Server Actions — kein globaler Client-Store.
|
||||
|
||||
---
|
||||
|
||||
## Deliverability-Pflichten
|
||||
|
||||
- Suppression-Check ist PFLICHT vor jedem Versand
|
||||
- List-Unsubscribe-Header (RFC 8058) in jeder E-Mail
|
||||
- Bounce-Rate > 5% → Kampagne automatisch pausieren
|
||||
- Kein Klartext-Logging von E-Mail-Adressen
|
||||
|
||||
---
|
||||
|
||||
## DSGVO
|
||||
|
||||
- Double-Opt-In Pflicht (Consent-Timestamp + IP + User-Agent)
|
||||
- Subscriber-Löschung löscht auch ClickHouse-Events (via tenant_id)
|
||||
- Alle Daten auf Hetzner DE
|
||||
1920
docs/plans/2026-04-17-kampagnen-implementierung.md
Normal file
1920
docs/plans/2026-04-17-kampagnen-implementierung.md
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user