cms.c2sgmbh/CLAUDE.md
Martin Porwoll a923d3ecb4 docs: cleanup and update documentation for January 2026
- Delete redundant TODO.md (consolidated into PROJECT_STATUS.md)
- Simplify STAGING-DEPLOYMENT.md to quick reference format
- Update PROJECT_STATUS.md with January 2026 status:
  - Add Community Management System section
  - Document Payload 3.72.0 login bug (#15243)
  - Update roadmap and changelog
- Update framework-monitoring.md with current versions and bug warning
- Add YouTube/Meta collections to CLAUDE.md:
  - YouTubeChannels, YouTubeContent, YtSeries, YtNotifications
  - MetaAccounts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 15:29:34 +00:00

42 KiB

Payload CMS Multi-Tenant Project

Projektübersicht

Multi-Tenant CMS für 4 Websites unter einer Payload CMS 3.x Instanz:

  • porwoll.de
  • complexcaresolutions.de
  • gunshin.de
  • zweitmein.ng

Tech Stack

  • CMS: Payload CMS 3.69.0 (⚠️ 3.72.0 hat Login-Bug, siehe GitHub #15243)
  • Framework: Next.js 15.5.9
  • React: 19.2.3
  • Sprache: TypeScript
  • Runtime: Node.js 22.x
  • Datenbank: PostgreSQL 17.6 (separater Server)
  • Connection Pool: PgBouncer 1.24.1 (Transaction-Mode)
  • Reverse Proxy: Caddy 2.9.x mit Let's Encrypt (Dev) / Nginx (Prod)
  • Process Manager: PM2
  • Package Manager: pnpm
  • Cache: Redis 7.x (optional, mit In-Memory-Fallback)
  • Job Queue: BullMQ (Redis-basiert)
  • Analytics: Umami 3.x
  • AI Tools: Claude Code, Codex CLI, Gemini CLI

Architektur

Development (VLAN 181 - porwoll.tech)

Internet → Cloudflare → 37.24.237.181 → Caddy (sv-caddy) → Services
                                                               ↓
                                    ┌───────────────────────────┴───────────────────────────┐
                                    │                                                       │
                              sv-payload:3000                                        sv-frontend:3000-3008
                              (Payload CMS)                                          (9 Frontend-Projekte)
                                    │
                              PgBouncer (6432)
                                    │
                              PostgreSQL (sv-postgres:5432)
LXC Hostname IP Service Status
699 sv-caddy 10.10.181.99 Caddy Reverse Proxy Running
700 sv-payload 10.10.181.100 Payload CMS + Redis Running
701 sv-postgres 10.10.181.101 PostgreSQL 17 + Redis Commander Running
702 sv-dev-payload 10.10.181.102 Payload Experimental ⏸️ Stopped
703 sv-analytics 10.10.181.103 Umami Analytics Running
704 sv-frontend 10.10.181.104 Multi-Frontend Dev (9 Projekte) Running

Production (Hetzner 3)

Service URL Port
Payload CMS https://cms.c2sgmbh.de 3001
Umami Analytics https://analytics.c2sgmbh.de 3000
PostgreSQL 17 localhost 5432
Redis Cache localhost 6379

Wichtige Pfade

/home/payload/payload-cms/     # Projektroot
├── src/
│   ├── payload.config.ts      # Haupt-Konfiguration
│   ├── collections/           # Alle Collections
│   │   ├── Users.ts
│   │   ├── Media.ts
│   │   ├── Tenants.ts
│   │   ├── Posts.ts
│   │   ├── Categories.ts
│   │   ├── Portfolios.ts
│   │   ├── PortfolioCategories.ts
│   │   ├── FAQs.ts
│   │   ├── Team.ts
│   │   ├── ServiceCategories.ts
│   │   ├── Services.ts
│   │   ├── EmailLogs.ts
│   │   ├── AuditLogs.ts
│   │   └── ...
│   ├── app/(payload)/api/     # Custom API Routes
│   │   ├── users/login/route.ts    # Custom Login mit Audit
│   │   ├── send-email/route.ts
│   │   ├── email-logs/
│   │   │   ├── export/route.ts
│   │   │   └── stats/route.ts
│   │   └── test-email/route.ts
│   ├── lib/
│   │   ├── email/             # E-Mail-System
│   │   │   ├── tenant-email-service.ts
│   │   │   ├── payload-email-adapter.ts
│   │   │   ├── newsletter-service.ts    # Newsletter Double Opt-In
│   │   │   └── newsletter-templates.ts  # E-Mail-Templates
│   │   ├── security/          # Security-Module
│   │   │   ├── rate-limiter.ts
│   │   │   ├── csrf.ts
│   │   │   ├── ip-allowlist.ts
│   │   │   └── data-masking.ts
│   │   ├── queue/             # BullMQ Job Queue
│   │   │   ├── queue-service.ts
│   │   │   ├── jobs/email-job.ts
│   │   │   ├── jobs/pdf-job.ts
│   │   │   ├── workers/email-worker.ts
│   │   │   └── workers/pdf-worker.ts
│   │   ├── pdf/               # PDF-Generierung
│   │   │   └── pdf-service.ts
│   │   ├── search.ts          # Volltextsuche
│   │   └── redis.ts           # Redis Cache Client
│   └── hooks/                 # Collection Hooks
│       ├── sendFormNotification.ts
│       ├── sendNewsletterConfirmation.ts  # Newsletter Double Opt-In
│       ├── invalidateEmailCache.ts
│       └── auditLog.ts
├── tests/                     # Test Suite
│   ├── unit/security/         # Security Unit Tests
│   └── int/                   # Integration Tests
├── scripts/
│   ├── run-queue-worker.ts    # Queue Worker Starter
│   └── backup/                # Backup-System
│       ├── backup-db.sh       # PostgreSQL Backup-Skript
│       ├── restore-db.sh      # PostgreSQL Restore-Skript
│       ├── setup-backup.sh    # Interaktives Setup
│       └── README.md          # Backup-Dokumentation
├── .env                       # Umgebungsvariablen
├── ecosystem.config.cjs       # PM2 Config
└── .next/                     # Build Output

Umgebungsvariablen (.env)

Hinweis: Sensible Credentials (DB_PASSWORD, SMTP_PASS, etc.) werden aus ~/.pgpass und Umgebungsvariablen gelesen. Niemals Klartext-Passwörter in diese Datei schreiben!

# Datenbank (Passwort in ~/.pgpass oder als Umgebungsvariable)
DATABASE_URI=postgresql://payload:${DB_PASSWORD}@127.0.0.1:6432/payload_db
PAYLOAD_SECRET=a53b254070d3fffd2b5cfcc3
PAYLOAD_PUBLIC_SERVER_URL=https://pl.porwoll.tech
NEXT_PUBLIC_SERVER_URL=https://pl.porwoll.tech
NODE_ENV=production
PORT=3000

# E-Mail (Global Fallback)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=user@example.com
SMTP_PASS=secret
SMTP_FROM_ADDRESS=noreply@c2sgmbh.de
SMTP_FROM_NAME=Payload CMS

# Redis Cache
REDIS_URL=redis://localhost:6379

# Security
CSRF_SECRET=your-csrf-secret              # PFLICHT in Production (oder PAYLOAD_SECRET)
TRUST_PROXY=true                          # PFLICHT hinter Reverse-Proxy (Caddy/Nginx)
SEND_EMAIL_ALLOWED_IPS=                   # Optional: Komma-separierte IPs/CIDRs
ADMIN_ALLOWED_IPS=                        # Optional: IP-Beschränkung für Admin-Panel
BLOCKED_IPS=                              # Optional: Global geblockte IPs

# Meta (Facebook + Instagram) OAuth
META_APP_ID=your-app-id                   # Facebook App ID
META_APP_SECRET=your-app-secret           # Facebook App Secret
META_REDIRECT_URI=https://your-domain/api/auth/meta/callback

# YouTube OAuth (existing)
GOOGLE_CLIENT_ID=your-client-id           # Google Cloud Console
GOOGLE_CLIENT_SECRET=your-client-secret
YOUTUBE_REDIRECT_URI=https://your-domain/api/youtube/callback

# Cron Jobs
CRON_SECRET=your-64-char-hex              # Auth für Cron-Endpoints

Wichtig: TRUST_PROXY=true muss gesetzt sein wenn die App hinter einem Reverse-Proxy (wie Caddy) läuft. Ohne diese Einstellung funktionieren IP-basierte Sicherheitsfunktionen (Rate-Limiting, IP-Allowlists, Blocklists) nicht korrekt.

PgBouncer Connection Pooling

PgBouncer läuft auf dem App-Server und pooled Datenbankverbindungen:

# Konfiguration
/etc/pgbouncer/pgbouncer.ini
/etc/pgbouncer/userlist.txt   # chmod 600

# Service
sudo systemctl status pgbouncer
sudo systemctl restart pgbouncer

# Statistiken abfragen
# Pool-Statistiken (Passwort aus ~/.pgpass)
PGPASSWORD="$DB_PASSWORD" psql -h 127.0.0.1 -p 6432 -U payload -d pgbouncer -c "SHOW POOLS;"
PGPASSWORD="$DB_PASSWORD" psql -h 127.0.0.1 -p 6432 -U payload -d pgbouncer -c "SHOW STATS;"

Konfiguration:

Parameter Wert Beschreibung
pool_mode transaction Verbindung wird nach Transaktion freigegeben
default_pool_size 20 Standard-Pool-Größe pro DB/User
min_pool_size 5 Mindestens gehaltene Verbindungen
reserve_pool_size 5 Reserve für Lastspitzen
max_db_connections 50 Max. Verbindungen zu PostgreSQL
max_client_conn 200 Max. Clients zu PgBouncer

Verbindungen:

  • App → PgBouncer: 127.0.0.1:6432 (DATABASE_URI)
  • PgBouncer → PostgreSQL: 10.10.181.101:5432 (TLS 1.3, server_tls_sslmode = require)

Direkte Verbindung (für Migrationen/CLI):

PgBouncer im Transaction-Mode kann Probleme mit lang laufenden Migrationen verursachen. Für solche Fälle das db-direct.sh Skript verwenden:

# Migrationen direkt an PostgreSQL (umgeht PgBouncer)
./scripts/db-direct.sh migrate

# Interaktive psql-Session
./scripts/db-direct.sh psql

# Schema-Änderungen
./scripts/db-direct.sh migrate:create

Das Skript liest Credentials aus ~/.pgpass (chmod 600).

Multi-Tenant Plugin

Verwendet @payloadcms/plugin-multi-tenant für Mandantenfähigkeit.

Aktuelle Tenants:

ID Name Slug
1 porwoll.de porwoll
4 Complex Care Solutions GmbH c2s
5 Gunshin gunshin

User-Tenant-Zuweisung: Tabelle users_tenants

Wichtige Befehle

# Entwicklung
pnpm dev

# Production Build
pnpm build

# Migrationen
pnpm payload migrate:create
pnpm payload migrate

# Schema-Sync (wichtig nach Collection-Änderungen!)
./scripts/sync-schema.sh --dry-run   # Zeigt Änderungen
./scripts/sync-schema.sh             # Führt Sync aus

# ImportMap nach Plugin-Änderungen
pnpm payload generate:importmap

# PM2
pm2 status
pm2 logs payload
pm2 logs queue-worker
pm2 restart payload
pm2 restart queue-worker

# Tests
pnpm test              # Alle Tests
pnpm test:security     # Security Tests
pnpm test:access-control  # Access Control Tests
pnpm test:coverage     # Mit Coverage-Report

# Datenbank prüfen
# Verwende ./scripts/db-direct.sh psql oder:
PGPASSWORD="$DB_PASSWORD" psql -h 10.10.181.101 -U payload -d payload_db

# Backup
/home/payload/backups/postgres/backup-db.sh --verbose  # Manuelles Backup
scripts/backup/setup-backup.sh                          # Backup-System einrichten

Workflow nach Code-Änderungen

  1. Code ändern
  2. pnpm build
  3. pm2 restart payload
  4. Testen unter https://pl.porwoll.tech/admin

Bei Collection/Global-Änderungen (zusätzlich)

  1. pnpm payload migrate:create - Migration erstellen
  2. git add src/migrations/ - Migration committen
  3. Auf PROD nach Deployment: ./scripts/sync-schema.sh wird automatisch ausgeführt

⚠️ KRITISCH: Neue Collections hinzufügen

Beim Hinzufügen einer neuen Collection müssen System-Tabellen manuell aktualisiert werden!

Problem: Payload CMS verwendet payload_locked_documents_rels um Document-Locks über alle Collections zu tracken. Diese Tabelle benötigt eine {collection}_id Spalte für JEDE Collection. Ohne diese Spalte tritt nach dem Login ein RSC-Fehler auf:

Error: An error occurred in the Server Components render
caused by: column payload_locked_documents_rels.{collection}_id does not exist

Lösung: Die Migration für eine neue Collection MUSS folgendes enthalten:

-- 1. Collection-Tabellen erstellen (normal)
CREATE TABLE IF NOT EXISTS "{collection}" (...);
CREATE TABLE IF NOT EXISTS "{collection}_rels" (...);  -- falls benötigt
CREATE TABLE IF NOT EXISTS "{collection}_locales" (...);  -- falls lokalisiert

-- 2. KRITISCH: System-Tabelle aktualisieren!
ALTER TABLE "payload_locked_documents_rels"
  ADD COLUMN IF NOT EXISTS "{collection}_id" integer
  REFERENCES {collection}(id) ON DELETE CASCADE;

CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_{collection}_idx"
  ON "payload_locked_documents_rels" ("{collection}_id");

Beispiel-Migration: src/migrations/20260109_020000_add_blogwoman_collections.ts

Array-Felder: Collections mit Array-Feldern (z.B. hours_structured in Locations) benötigen zusätzliche Tabellen:

CREATE TABLE IF NOT EXISTS "{collection}_{array_field}" (
  "id" serial PRIMARY KEY NOT NULL,
  "_order" integer NOT NULL,
  "_parent_id" integer NOT NULL REFERENCES {collection}(id) ON DELETE CASCADE,
  -- Array-Feld-Spalten hier
);

Bekannte Besonderheiten

  • ES Modules: package.json hat "type": "module", daher PM2 Config als .cjs
  • Plugin ImportMap: Nach Plugin-Änderungen pnpm payload generate:importmap ausführen
  • User-Tenant-Zuweisung: Neue User müssen manuell Tenants zugewiesen bekommen
  • Admin Login: Custom Route mit Audit-Logging, unterstützt JSON und _payload FormData

Build-Konfiguration

Der Build ist für speichereffizientes Kompilieren optimiert:

  • package.json: --max-old-space-size=2048 (2GB Heap-Limit)
  • next.config.mjs: experimental.cpus: 1, workerThreads: false

WICHTIG: Der Server hat 8GB RAM ohne Swap. Bei laufendem VS Code Server kann der Build mit reduziertem Memory ausgeführt werden:

pm2 stop payload  # Speicher freigeben
NODE_OPTIONS="--no-deprecation --max-old-space-size=1024" ./node_modules/.bin/next build
pm2 start payload

Ohne PM2-Stop und mit VS Code wird der Build vom OOM Killer beendet (Exit code 137).

Mehrsprachigkeit (i18n)

Das System unterstützt Deutsch (default) und Englisch:

  • Admin UI: @payloadcms/translations für DE/EN
  • Content: Localization mit Fallback auf Deutsch
  • Datenbank: 36 _locales Tabellen für lokalisierte Felder
  • API: ?locale=de oder ?locale=en Parameter
  • Frontend: Routing über /[locale]/...
# Locales in der Datenbank prüfen
# Verwende ./scripts/db-direct.sh psql oder:
PGPASSWORD="$DB_PASSWORD" psql -h 10.10.181.101 -U payload -d payload_db -c "\dt *_locales"

URLs

Security-Features

Proxy-Vertrauen (TRUST_PROXY)

WICHTIG: Wenn die App hinter einem Reverse-Proxy läuft (Caddy, Nginx, etc.), muss TRUST_PROXY=true gesetzt sein:

TRUST_PROXY=true

Ohne diese Einstellung:

  • Werden X-Forwarded-For und X-Real-IP Header ignoriert
  • Können Angreifer IP-basierte Sicherheitsmaßnahmen nicht umgehen
  • Aber: Rate-Limiting und IP-Allowlists funktionieren nicht korrekt

Rate-Limiting

Zentraler Rate-Limiter mit vordefinierten Limits:

  • publicApi: 60 Requests/Minute
  • auth: 5 Requests/15 Minuten (Login)
  • email: 10 Requests/Minute
  • search: 30 Requests/Minute
  • form: 5 Requests/10 Minuten

Hinweis: Rate-Limiting basiert auf Client-IP. Bei TRUST_PROXY=false wird direct-connection als IP verwendet, was alle Clients zusammenfasst.

CSRF-Schutz

  • Double Submit Cookie Pattern
  • Origin-Header-Validierung
  • Token-Endpoint: GET /api/csrf-token
  • Admin-Panel hat eigenen CSRF-Schutz
  • CSRF_SECRET oder PAYLOAD_SECRET ist in Production PFLICHT
  • Server startet nicht ohne Secret in Production

IP-Allowlist

  • Konfigurierbar via SEND_EMAIL_ALLOWED_IPS, ADMIN_ALLOWED_IPS
  • Unterstützt IPs, CIDRs (192.168.1.0/24) und Wildcards (10.10.*.*)
  • Globale Blocklist via BLOCKED_IPS
  • Warnung: Leere Allowlists erlauben standardmäßig alle IPs

Data-Masking

  • Automatische Maskierung sensibler Daten in Logs
  • Erkennt Passwörter, Tokens, API-Keys, SMTP-Credentials
  • Safe-Logger-Factory für konsistentes Logging

E-Mail-System

Multi-Tenant E-Mail-System mit tenant-spezifischer SMTP-Konfiguration:

Architektur:

  • Globaler SMTP als Fallback (via .env)
  • Tenant-spezifische SMTP in Tenants Collection
  • Transporter-Caching mit automatischer Invalidierung
  • EmailLogs Collection für Audit-Trail

Tenant E-Mail-Konfiguration:

Tenants → email → fromAddress, fromName, replyTo
       → email → useCustomSmtp (Checkbox)
       → email → smtp → host, port, secure, user, pass

API-Endpoint /api/send-email:

curl -X POST https://pl.porwoll.tech/api/send-email \
  -H "Content-Type: application/json" \
  -H "Cookie: payload-token=..." \
  -d '{
    "to": "empfaenger@example.com",
    "subject": "Betreff",
    "html": "<p>Inhalt</p>",
    "tenantId": 1
  }'

Sicherheit:

  • Authentifizierung erforderlich
  • Tenant-Zugriffskontrolle (User muss Tenant-Mitglied sein)
  • Rate-Limiting: 10 E-Mails/Minute pro User
  • SMTP-Passwort nie in API-Responses

Newsletter Double Opt-In

DSGVO-konformes Newsletter-System mit Double Opt-In:

Flow:

  1. User meldet sich an → Status: pending, Token wird generiert
  2. Double Opt-In E-Mail wird automatisch gesendet
  3. User klickt Bestätigungs-Link → Status: confirmed
  4. Willkommens-E-Mail wird gesendet
  5. Abmeldung jederzeit über Link in E-Mails möglich

API-Endpoints:

# Newsletter-Anmeldung
curl -X POST https://pl.porwoll.tech/api/newsletter/subscribe \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "firstName": "Max",
    "tenantId": 1,
    "source": "footer"
  }'

# Bestätigung (via Link aus E-Mail)
GET https://pl.porwoll.tech/api/newsletter/confirm?token=<uuid>

# Abmeldung (via Link aus E-Mail)
GET https://pl.porwoll.tech/api/newsletter/unsubscribe?token=<uuid>

Features:

  • Automatischer E-Mail-Versand bei Anmeldung
  • Token-Ablauf nach 48 Stunden
  • Willkommens-E-Mail nach Bestätigung
  • Abmelde-Bestätigung per E-Mail
  • Rate-Limiting: 5 Anmeldungen/10 Minuten pro IP
  • Erneute Anmeldung nach Abmeldung möglich

BullMQ Job Queue

Das System verwendet BullMQ für asynchrone Job-Verarbeitung mit Redis als Backend.

PM2 Worker

Der Queue-Worker läuft als separater PM2-Prozess:

# Worker-Status prüfen
pm2 status
pm2 logs queue-worker

# Worker neustarten
pm2 restart queue-worker

Email Queue

E-Mails können asynchron über die Queue versendet werden:

import { queueEmail } from '@/lib/queue'

await queueEmail({
  tenantId: 1,
  to: 'empfaenger@example.com',
  subject: 'Betreff',
  html: '<p>Inhalt</p>',
  source: 'form-submission'
})

PDF Queue

PDF-Generierung erfolgt über Playwright (HTML/URL zu PDF):

API-Endpoint /api/generate-pdf:

# PDF aus HTML generieren
curl -X POST https://pl.porwoll.tech/api/generate-pdf \
  -H "Content-Type: application/json" \
  -H "Cookie: payload-token=..." \
  -d '{
    "source": "html",
    "html": "<h1>Test</h1><p>Inhalt</p>",
    "filename": "test.pdf"
  }'

# Job-Status abfragen
curl "https://pl.porwoll.tech/api/generate-pdf?jobId=abc123" \
  -H "Cookie: payload-token=..."

Programmatisch:

import { queuePdfFromHtml, queuePdfFromUrl, getPdfJobStatus } from '@/lib/queue'

// HTML zu PDF
const job = await queuePdfFromHtml('<h1>Test</h1>', { filename: 'test.pdf' })

// URL zu PDF
const job2 = await queuePdfFromUrl('https://example.com', { format: 'A4' })

// Status abfragen
const status = await getPdfJobStatus(job.id)

Worker-Konfiguration

Über ecosystem.config.cjs:

  • QUEUE_EMAIL_CONCURRENCY: Parallele E-Mail-Jobs (default: 3)
  • QUEUE_PDF_CONCURRENCY: Parallele PDF-Jobs (default: 2)
  • QUEUE_RETENTION_CONCURRENCY: Parallele Retention-Jobs (default: 1)
  • QUEUE_DEFAULT_RETRY: Retry-Versuche (default: 3)
  • QUEUE_REDIS_DB: Redis-Datenbank für Queue (default: 1)

Data Retention

Automatische Datenbereinigung für DSGVO-Compliance und Speicheroptimierung.

Retention Policies

Collection Retention Umgebungsvariable Beschreibung
email-logs 90 Tage RETENTION_EMAIL_LOGS_DAYS E-Mail-Protokolle
audit-logs 90 Tage RETENTION_AUDIT_LOGS_DAYS Audit-Trail
consent-logs 3 Jahre RETENTION_CONSENT_LOGS_DAYS DSGVO: expiresAt-basiert
media (orphans) 30 Tage RETENTION_MEDIA_ORPHAN_MIN_AGE_DAYS Unreferenzierte Medien

Automatischer Scheduler

Retention-Jobs laufen täglich um 03:00 Uhr (konfigurierbar via RETENTION_CRON_SCHEDULE).

# Umgebungsvariablen in .env
RETENTION_EMAIL_LOGS_DAYS=90
RETENTION_AUDIT_LOGS_DAYS=90
RETENTION_CONSENT_LOGS_DAYS=1095
RETENTION_MEDIA_ORPHAN_MIN_AGE_DAYS=30
RETENTION_CRON_SCHEDULE="0 3 * * *"

# Worker aktivieren/deaktivieren
QUEUE_ENABLE_RETENTION=true
QUEUE_ENABLE_RETENTION_SCHEDULER=true

API-Endpoint /api/retention

GET - Konfiguration abrufen:

curl https://pl.porwoll.tech/api/retention \
  -H "Cookie: payload-token=..."

GET - Job-Status abfragen:

curl "https://pl.porwoll.tech/api/retention?jobId=abc123" \
  -H "Cookie: payload-token=..."

POST - Manuellen Job auslösen:

# Vollständige Retention (alle Policies + Media-Orphans)
curl -X POST https://pl.porwoll.tech/api/retention \
  -H "Content-Type: application/json" \
  -H "Cookie: payload-token=..." \
  -d '{"type": "full"}'

# Einzelne Collection bereinigen
curl -X POST https://pl.porwoll.tech/api/retention \
  -H "Content-Type: application/json" \
  -H "Cookie: payload-token=..." \
  -d '{"type": "collection", "collection": "email-logs"}'

# Nur Media-Orphans bereinigen
curl -X POST https://pl.porwoll.tech/api/retention \
  -H "Content-Type: application/json" \
  -H "Cookie: payload-token=..." \
  -d '{"type": "media-orphans"}'

Architektur

Scheduler (Cron)
    ↓
Retention Queue (BullMQ)
    ↓
Retention Worker
    ↓
┌─────────────────┬─────────────────┬─────────────────┐
│  Email-Logs     │  Audit-Logs     │  Consent-Logs   │
│  (createdAt)    │  (createdAt)    │  (expiresAt)    │
└─────────────────┴─────────────────┴─────────────────┘
    ↓
Media-Orphan-Cleanup
    ↓
Cleanup-Ergebnis (Logs)

Dateien

  • src/lib/retention/retention-config.ts - Zentrale Konfiguration
  • src/lib/retention/cleanup-service.ts - Lösch-Logik
  • src/lib/queue/jobs/retention-job.ts - Job-Definition
  • src/lib/queue/workers/retention-worker.ts - Worker
  • src/app/(payload)/api/retention/route.ts - API-Endpoint

Redis Caching

Redis wird für API-Response-Caching und E-Mail-Transporter-Caching verwendet:

import { redis } from '@/lib/redis'

// Cache setzen (TTL in Sekunden)
await redis.set('key', JSON.stringify(data), 'EX', 60)

// Cache lesen
const cached = await redis.get('key')

// Pattern-basierte Invalidierung
await redis.keys('posts:*').then(keys => keys.length && redis.del(...keys))

Backup-System

Automatisches tägliches PostgreSQL-Backup mit lokalem Speicher und S3-Offsite-Backup.

Setup auf neuem Server:

cd /home/payload/payload-cms/scripts/backup
./setup-backup.sh

Konfigurationsdateien (nicht im Repo):

  • ~/.pgpass - PostgreSQL-Credentials (chmod 600)
  • ~/.s3cfg - S3-Credentials (chmod 600)

Backup-Speicherorte:

Ort Pfad Retention
Lokal /home/payload/backups/postgres/ 30 Tage
S3 s3://c2s/backups/postgres/ 30 Tage

Cron-Job: Täglich um 03:00 Uhr

Manuelles Backup:

/home/payload/backups/postgres/backup-db.sh --verbose

Restore aus Backup:

# Lokal
gunzip -c /home/payload/backups/postgres/payload_db_YYYY-MM-DD_HH-MM-SS.sql.gz | \
  psql -h 10.10.181.101 -U payload -d payload_db

# Aus S3
s3cmd get s3://c2s/backups/postgres/payload_db_YYYY-MM-DD_HH-MM-SS.sql.gz
gunzip -c payload_db_*.sql.gz | psql -h 10.10.181.101 -U payload -d payload_db

Dokumentation: scripts/backup/README.md

Datenbank-Direktzugriff

# Verwende ./scripts/db-direct.sh psql oder:
PGPASSWORD="$DB_PASSWORD" psql -h 10.10.181.101 -U payload -d payload_db

# Nützliche Queries
SELECT * FROM tenants;
SELECT * FROM users_tenants;
SELECT * FROM email_logs ORDER BY created_at DESC LIMIT 10;
SELECT * FROM audit_logs ORDER BY created_at DESC LIMIT 10;
\dt  -- Alle Tabellen

Blocks Übersicht (42 Blocks)

Core Blocks

Block Slug Beschreibung
HeroBlock hero-block Einzelner Hero mit Bild, Headline, CTA
HeroSliderBlock hero-slider-block Hero-Slider mit mehreren Slides
ImageSliderBlock image-slider-block Bildergalerie/Karussell
TextBlock text-block Textinhalt mit Rich-Text
ImageTextBlock image-text-block Bild + Text nebeneinander
CardGridBlock card-grid-block Karten-Raster
QuoteBlock quote-block Zitat
CTABlock cta-block Call-to-Action
ContactFormBlock contact-form-block Kontaktformular
TimelineBlock timeline-block Timeline-Darstellung
DividerBlock divider-block Trennlinie
VideoBlock video-block Video einbetten

Content Blocks

Block Slug Beschreibung
PostsListBlock posts-list-block Beitrags-Liste
TestimonialsBlock testimonials-block Kundenbewertungen
NewsletterBlock newsletter-block Newsletter-Anmeldung
ProcessStepsBlock process-steps-block Prozess-Schritte
FAQBlock faq-block FAQ-Akkordeon
TeamBlock team-block Team-Mitglieder
ServicesBlock services-block Leistungen

Blogging Blocks

Block Slug Beschreibung
AuthorBioBlock author-bio-block Autoren-Biografie
RelatedPostsBlock related-posts-block Verwandte Beiträge
ShareButtonsBlock share-buttons-block Social Share Buttons
TableOfContentsBlock table-of-contents-block Inhaltsverzeichnis

Team Blocks

Block Slug Beschreibung
TeamFilterBlock team-filter-block Team mit Filter-Funktion
OrgChartBlock org-chart-block Organigramm

Feature Blocks

Block Slug Beschreibung
LocationsBlock locations-block Standorte/Filialen
LogoGridBlock logo-grid-block Partner/Kunden-Logos
StatsBlock stats-block Statistiken/Zahlen
JobsBlock jobs-block Stellenangebote
DownloadsBlock downloads-block Download-Bereich
MapBlock map-block Karten-Einbindung

Interactive Blocks

Block Slug Beschreibung
EventsBlock events-block Veranstaltungen
PricingBlock pricing-block Preistabellen
TabsBlock tabs-block Tab-Navigation
AccordionBlock accordion-block Akkordeon/Aufklappbar
ComparisonBlock comparison-block Vergleichstabelle

Tenant-specific Blocks

Block Slug Beschreibung
BeforeAfterBlock before-after-block Vorher/Nachher Bildvergleich (porwoll.de)

BlogWoman Blocks

Block Slug Beschreibung
FavoritesBlock favorites-block Affiliate-Produkte Grid/Liste/Karussell
SeriesBlock series-block YouTube-Serien Übersicht
SeriesDetailBlock series-detail-block Serien-Einzelseite mit Hero
VideoEmbedBlock video-embed-block YouTube/Vimeo Embed mit Privacy Mode
FeaturedContentBlock featured-content-block Kuratierte Mixed-Content Sammlung

HeroSliderBlock Features

Vollwertiger Hero-Slider mit:

Slides (Array, 1-10):

  • Hintergrundbild (Desktop + optional Mobil)
  • Headline + Subline (lokalisiert)
  • Text-Ausrichtung (links/zentriert/rechts)
  • Vertikale Position (oben/mitte/unten)
  • Overlay (Farbe, Deckkraft, Gradient)
  • Primärer + Sekundärer CTA-Button
  • Textfarbe (weiß/dunkel/primär)

Animationen:

  • Typen: fade, slide, zoom, flip, none
  • Konfigurierbare Dauer (300-1200ms)

Autoplay:

  • Intervall (3-10 Sekunden)
  • Pause bei Hover
  • Pause bei Interaktion

Navigation:

  • Pfeile (verschiedene Stile, Positionen)
  • Dots (Punkte, Striche, Nummern, Thumbnails, Fortschritt)
  • Touch-Swipe
  • Tastaturnavigation

Layout:

  • Höhe (Vollbild bis kompakt)
  • Separate Mobile-Höhe
  • Content-Breite

Collections Übersicht (40+ Collections)

Core Collections

Collection Slug Beschreibung
Users users Benutzer mit isSuperAdmin Flag
Tenants tenants Mandanten mit E-Mail-Konfiguration
Media media Medien mit 11 responsive Image Sizes
Pages pages Seiten mit Blocks

Content Collections

Collection Slug Beschreibung
Posts posts Blog/News/Presse mit Kategorien
Categories categories Kategorien für Posts
Tags tags Tags für Posts (Blogging)
Authors authors Autoren für Posts
Testimonials testimonials Kundenbewertungen
FAQs faqs Häufig gestellte Fragen (FAQ)
SocialLinks social-links Social Media Links

Team & Services

Collection Slug Beschreibung
Team team Team-Mitglieder und Mitarbeiter
ServiceCategories service-categories Kategorien für Leistungen
Services services Leistungen und Dienstleistungen
Jobs jobs Stellenangebote

Portfolio & Media

Collection Slug Beschreibung
Portfolios portfolios Portfolio-Galerien (Fotografie)
PortfolioCategories portfolio-categories Kategorien für Portfolios
Videos videos Video-Bibliothek mit YouTube/Vimeo/Uploads
VideoCategories video-categories Kategorien für Videos

Products (E-Commerce)

Collection Slug Beschreibung
Products products Produkte
ProductCategories product-categories Produkt-Kategorien

Feature Collections

Collection Slug Beschreibung
Locations locations Standorte/Filialen
Partners partners Partner/Kunden
Downloads downloads Download-Dateien
Events events Veranstaltungen
Timelines timelines Chronologische Events (Geschichte, Meilensteine)
Workflows workflows Komplexe Prozesse mit Phasen und Schritten

Formulare & Newsletter

Collection Slug Beschreibung
Forms forms Formular-Builder (Plugin)
FormSubmissions form-submissions Formular-Einsendungen mit CRM-Workflow
NewsletterSubscribers newsletter-subscribers Newsletter mit Double Opt-In
Collection Slug Beschreibung
CookieConfigurations cookie-configurations Cookie-Banner Konfiguration
CookieInventory cookie-inventory Cookie-Inventar
ConsentLogs consent-logs Consent-Protokollierung (WORM)
PrivacyPolicySettings privacy-policy-settings Datenschutz-Einstellungen

Tenant-specific Collections

Collection Slug Beschreibung
Bookings bookings Fotografie-Buchungen (porwoll.de)
Certifications certifications Zertifizierungen (C2S)
Projects projects Game-Development-Projekte (gunshin.de)

BlogWoman Collections

Collection Slug Beschreibung
Favorites favorites Affiliate-Produkte mit Kategorien und Badges
Series series YouTube-Serien mit Branding (Logo, Farben)

YouTube & Community Management

Collection Slug Beschreibung
YouTubeChannels youtube-channels Multi-Kanal-Verwaltung mit OAuth
YouTubeContent youtube-content Videos + Shorts mit Kommentaren
YtSeries yt-series Serien mit Branding (Logo, Farben, Playlist)
YtNotifications yt-notifications Handlungsbedarf-System
MetaAccounts meta-accounts Facebook/Instagram OAuth-Verbindungen

System Collections

Collection Slug Beschreibung
EmailLogs email-logs E-Mail-Protokollierung
AuditLogs audit-logs Security Audit Trail
SiteSettings site-settings Website-Einstellungen (pro Tenant)
Navigations navigations Navigationsmenüs (pro Tenant)
Redirects redirects URL-Weiterleitungen (Plugin)

Timeline Collection

Dedizierte Collection für komplexe chronologische Darstellungen:

Timeline-Typen:

  • history - Unternehmensgeschichte
  • milestones - Projektmeilensteine
  • releases - Produkt-Releases
  • career - Karriere/Lebenslauf
  • events - Ereignisse
  • process - Prozess/Ablauf

Event-Features:

  • Flexible Datumsformate (Jahr, Monat+Jahr, vollständig, Zeitraum, Freitext)
  • Kategorien (Meilenstein, Gründung, Produkt, Team, Auszeichnung, etc.)
  • Wichtigkeitsstufen (Highlight, Normal, Minor)
  • Bilder und Galerien
  • Links und Metadaten
  • Rich-Text-Beschreibungen

Display-Optionen:

  • Layouts: vertikal, alternierend, horizontal, kompakt
  • Sortierung: aufsteigend/absteigend
  • Gruppierung nach Jahr
  • Verschiedene Marker-Stile

Prozess-spezifische Felder (type=process):

  • stepNumber - Explizite Schritt-Nummerierung
  • duration - Dauer (z.B. "2-3 Tage")
  • responsible - Verantwortliche Person/Rolle
  • actionRequired - Wer muss aktiv werden (customer, internal, both, automatic)
  • deliverables - Ergebnisse/Dokumente des Schritts

API-Endpoint:

# Liste aller Timelines eines Tenants
curl "https://pl.porwoll.tech/api/timelines?tenant=1"

# Nach Typ filtern
curl "https://pl.porwoll.tech/api/timelines?tenant=1&type=history"

# Einzelne Timeline
curl "https://pl.porwoll.tech/api/timelines?tenant=1&slug=company-history"

# Mit Sprache
curl "https://pl.porwoll.tech/api/timelines?tenant=1&locale=en"

Workflows Collection

Komplexe Prozess- und Workflow-Darstellungen mit Phasen, Abhängigkeiten und Status-Tracking.

Workflow-Typen:

  • project - Projektabläufe
  • business - Geschäftsprozesse
  • approval - Genehmigungs-Workflows
  • onboarding - Mitarbeiter-/Kundeneinführung
  • support - Support/Service-Prozesse
  • development - Entwicklungsprozesse
  • marketing - Marketing-Workflows
  • other - Sonstige

Eigenschaften:

  • estimatedDuration - Geschätzte Gesamtdauer
  • complexity - simple, medium, complex, very_complex
  • isIterative - Kann wiederholt werden
  • allowParallelPhases - Parallele Phasen möglich

Display-Optionen:

  • Layouts: vertical, horizontal, flowchart, kanban, gantt
  • Farbschema: phase, status, priority, brand
  • Optionale Anzeige: Nummern, Zeiten, Verantwortliche, Fortschritt

Phasen-Struktur:

Workflow
└── Phasen (Array)
    ├── name, description, icon, color
    ├── estimatedDuration, responsible
    ├── deliverables (Array)
    └── Schritte (Array)
        ├── name, description, stepType, priority
        ├── estimatedDuration, responsible
        ├── dependencies (dependsOnSteps, canRunParallel, isBlocking)
        ├── conditions (für Entscheidungen)
        ├── checklist (Array)
        ├── resources (Dokumente, Links, Tools)
        └── outputs (Ergebnisse)

API-Endpoint:

# Liste aller Workflows eines Tenants
curl "https://pl.porwoll.tech/api/workflows?tenant=1"

# Nach Typ filtern
curl "https://pl.porwoll.tech/api/workflows?tenant=1&type=project"

# Nach Komplexität filtern
curl "https://pl.porwoll.tech/api/workflows?tenant=1&complexity=medium"

# Einzelner Workflow
curl "https://pl.porwoll.tech/api/workflows?tenant=1&slug=web-project"

# Mit Sprache
curl "https://pl.porwoll.tech/api/workflows?tenant=1&locale=en"

Validierung:

  • tenant - Pflichtparameter (Tenant-Isolation)
  • type - Validiert gegen erlaubte Typen (400 bei Fehler)
  • complexity - Validiert gegen erlaubte Werte (400 bei Fehler)
  • locale - de (default) oder en

FormSubmissions CRM-Workflow

Die FormSubmissions Collection wurde zu einem leichtgewichtigen CRM erweitert:

Status-Workflow:

  • 🆕 Neu → 👀 Gelesen → 🔄 In Bearbeitung → Warten → Erledigt → 🗑️ Archiviert

Features:

  • Priorität (Hoch/Normal/Niedrig)
  • Zuständigkeits-Zuweisung an User
  • Interne Notizen mit Auto-Autor und Zeitstempel
  • Antwort-Tracking (Methode, Zeitstempel, Zusammenfassung)
  • Tags zur Kategorisierung
  • Auto-Markierung als gelesen beim ersten Öffnen

Dateien:

  • src/collections/FormSubmissionsOverrides.ts - Feld-Definitionen
  • src/hooks/formSubmissionHooks.ts - Automatisierungen

Globals

Hinweis: SiteSettings, Navigations und PrivacyPolicySettings wurden zu tenant-spezifischen Collections umgewandelt (siehe Collections Übersicht).

Global Slug Beschreibung
SEOSettings seo-settings Globale SEO-Einstellungen (systemweit)

Test Suite

# Alle Tests ausführen
pnpm test

# Security Tests
pnpm test:security

# Coverage Report
pnpm test:coverage

Test Coverage Thresholds:

  • Lines: 35%
  • Functions: 50%
  • Branches: 65%

Scheduled Cron Jobs (Vercel)

Automatische Hintergrund-Jobs via Vercel Cron (vercel.json):

Endpoint Schedule Beschreibung
/api/cron/community-sync */15 * * * * (alle 15 Min) Synchronisiert Kommentare von YouTube, Facebook, Instagram
/api/cron/token-refresh 0 6,18 * * * (6:00 + 18:00 UTC) Erneuert ablaufende OAuth-Tokens automatisch
/api/cron/send-reports 0 * * * * (stündlich) Versendet fällige Community-Reports per E-Mail

Authentifizierung: Alle Cron-Endpoints erfordern Authorization: Bearer $CRON_SECRET Header.

Manueller Trigger:

# Community Sync manuell auslösen
curl -X POST "https://your-domain/api/cron/community-sync" \
  -H "Authorization: Bearer $CRON_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"platforms": ["youtube", "facebook"]}'

# Token Refresh manuell auslösen (Dry-Run)
curl "https://your-domain/api/cron/token-refresh?dryRun=true" \
  -H "Authorization: Bearer $CRON_SECRET"

# Token Refresh Status prüfen
curl -I "https://your-domain/api/cron/token-refresh" \
  -H "Authorization: Bearer $CRON_SECRET"

Monitoring:

  • HEAD-Requests geben Status-Header zurück (X-Sync-Running, X-Last-Run, etc.)
  • Bei laufendem Job: HTTP 423 (Locked)
  • Fehlerhafte Tokens werden als Benachrichtigungen in yt-notifications gespeichert

CI/CD Pipeline

GitHub Actions Workflows in .github/workflows/:

ci.yml (Main CI Pipeline)

Läuft bei Push/PR auf main und develop:

Job Beschreibung
lint ESLint + Prettier Check
typecheck TypeScript Compiler (tsc --noEmit)
test Unit & Integration Tests (Vitest)
build Next.js Production Build
e2e Playwright E2E Tests

Lokale Ausführung:

pnpm lint              # ESLint
pnpm typecheck         # TypeScript Check
pnpm format:check      # Prettier Check
pnpm format            # Prettier Auto-Fix
pnpm test              # Alle Tests
pnpm build             # Production Build

security.yml (Security Scanning)

  • Gitleaks: Secret Scanning
  • pnpm audit: Dependency Vulnerabilities
  • CodeQL: Static Analysis (SAST)
  • Security Tests: Unit & Integration Tests für Security-Module

deploy-staging.yml (Staging Deployment)

Automatisches Deployment auf Staging-Server bei Push auf develop:

Trigger Aktion
Push auf develop Automatisches Deployment
workflow_dispatch Manuelles Deployment (optional: skip_tests)

Deployment-Ziel:

Ablauf:

  1. Pre-deployment Checks (Lint, Tests)
  2. SSH-Verbindung zum Staging-Server
  3. Git Pull + Dependencies installieren
  4. Migrations ausführen
  5. Build + PM2 Restart
  6. Health Check

Manuelles Staging-Deployment:

# Auf dem Staging-Server (pl.porwoll.tech)
./scripts/deploy-staging.sh

# Mit Optionen
./scripts/deploy-staging.sh --skip-build      # Nur Code-Update
./scripts/deploy-staging.sh --skip-migrations # Ohne Migrationen

GitHub Secret erforderlich:

  • STAGING_SSH_KEY - SSH Private Key für payload@37.24.237.181

deploy-production.yml (Production Deployment)

Manuelles Deployment auf Production-Server (Hetzner 3) bei workflow_dispatch:

Feature Beschreibung
Pre-flight Checks Überprüft Branch-Status und Migrationen
Pre-deployment Tests Optional, Lint + Unit Tests
Database Backup Automatisches Backup vor Deployment
Health Check Verifiziert erfolgreiche Deployment
Auto-Rollback Bei fehlgeschlagenem Health Check

Manuelles Production-Deployment:

# Via GitHub Actions (empfohlen)
gh workflow run deploy-production.yml

# Auf dem Production-Server (Hetzner 3)
ssh payload@162.55.85.18
./scripts/deploy-production.sh

# Mit Optionen
./scripts/deploy-production.sh -y              # Ohne Bestätigung
./scripts/deploy-production.sh --skip-backup   # Ohne Backup
./scripts/deploy-production.sh --rollback      # Rollback zur vorherigen Version
./scripts/deploy-production.sh --dry-run       # Zeigt was passieren würde

GitHub Secrets erforderlich:

  • STAGING_SSH_KEY - SSH Private Key für sv-payload
  • PRODUCTION_SSH_KEY - SSH Private Key für Hetzner 3

Dokumentation

Hauptdokumentation

  • CLAUDE.md - Diese Datei (Projekt-Übersicht für AI-Assistenten)
  • docs/INFRASTRUCTURE.md - Infrastruktur-Übersicht (Dev + Prod)
  • docs/DEPLOYMENT.md - Deployment-Prozess & Checklisten
  • docs/DEPLOYMENT_STRATEGY.md - Vollständige Deployment-Strategie (Dev → Prod)
  • docs/PROJECT_STATUS.md - Aktueller Projektstatus & Roadmap
  • docs/STAGING-DEPLOYMENT.md - Staging Deployment Workflow

Anleitungen

  • docs/anleitungen/TODO.md - Task-Liste & Changelog
  • docs/anleitungen/SECURITY.md - Sicherheitsrichtlinien
  • docs/anleitungen/FRONTEND.md - Frontend-Entwicklung (sv-frontend)
  • docs/anleitungen/API_ANLEITUNG.md - API-Dokumentation
  • docs/anleitungen/framework-monitoring.md - Framework-Updates beobachten

Scripts & Backup

  • scripts/backup/README.md - Backup-System Dokumentation

Letzte Aktualisierung: 17.01.2026