Files
coding-starter/docs/plans/2026-04-17-kampagnen-design.md

4.2 KiB

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)

-- 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