# 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