# 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.x - **Framework:** Next.js 15.4.7 - **Sprache:** TypeScript - **Datenbank:** PostgreSQL 17 (separater Server) - **Reverse Proxy:** Caddy 2.10.2 mit Let's Encrypt - **Process Manager:** PM2 - **Package Manager:** pnpm ## Architektur ``` Internet → 37.24.237.181 → Caddy (443) → Payload (3000) ↓ PostgreSQL (10.10.181.101:5432) ``` | Server | IP | Funktion | | --------------------- | ------------- | ---------- | | sv-payload (LXC 700) | 10.10.181.100 | App Server | | sv-postgres (LXC 701) | 10.10.181.101 | Datenbank | ## 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 │ │ ├── EmailLogs.ts │ │ └── ... │ ├── lib/ │ │ ├── email/ # E-Mail-System │ │ │ ├── tenant-email-service.ts │ │ │ └── payload-email-adapter.ts │ │ ├── search.ts # Volltextsuche │ │ └── redis.ts # Redis Cache Client │ └── hooks/ # Collection Hooks │ ├── sendFormNotification.ts │ └── invalidateEmailCache.ts ├── .env # Umgebungsvariablen ├── ecosystem.config.cjs # PM2 Config └── .next/ # Build Output ``` ## Umgebungsvariablen (.env) ```env DATABASE_URI=postgresql://payload:Finden55@10.10.181.101:5432/payload_db PAYLOAD_SECRET=a53b254070d3fffd2b5cfcc3 PAYLOAD_PUBLIC_SERVER_URL=https://pl.c2sgmbh.de NEXT_PUBLIC_SERVER_URL=https://pl.c2sgmbh.de 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 ``` ## 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 ```bash # Entwicklung pnpm dev # Production Build pnpm build # Migrationen pnpm payload migrate:create pnpm payload migrate # ImportMap nach Plugin-Änderungen pnpm payload generate:importmap # PM2 pm2 status pm2 logs payload pm2 restart payload # Datenbank prüfen PGPASSWORD=Finden55 psql -h 10.10.181.101 -U payload -d payload_db ``` ## Workflow nach Code-Änderungen 1. Code ändern 2. `pnpm build` 3. `pm2 restart payload` 4. Testen unter https://pl.c2sgmbh.de/admin ## 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 ## 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 nur 4GB RAM ohne Swap. Bei laufendem VS Code Server muss der Build mit reduziertem Memory ausgeführt werden: ```bash 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]/...` ```bash # Locales in der Datenbank prüfen PGPASSWORD=Finden55 psql -h 10.10.181.101 -U payload -d payload_db -c "\dt *_locales" ``` ## URLs - **Admin Panel:** https://pl.c2sgmbh.de/admin - **API:** https://pl.c2sgmbh.de/api - **E-Mail API:** https://pl.c2sgmbh.de/api/send-email (POST, Auth erforderlich) ## 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`:** ```bash curl -X POST https://pl.c2sgmbh.de/api/send-email \ -H "Content-Type: application/json" \ -H "Cookie: payload-token=..." \ -d '{ "to": "empfaenger@example.com", "subject": "Betreff", "html": "

Inhalt

", "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 ## Redis Caching Redis wird für API-Response-Caching und E-Mail-Transporter-Caching verwendet: ```typescript 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)) ``` ## Datenbank-Direktzugriff ```bash PGPASSWORD=Finden55 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; \dt -- Alle Tabellen ``` ## Collections Übersicht | 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 | | Posts | posts | Blog/News/Presse mit Kategorien | | Categories | categories | Kategorien für Posts | | Portfolios | portfolios | Portfolio-Galerien (Fotografie) | | PortfolioCategories | portfolio-categories | Kategorien für Portfolios | | Testimonials | testimonials | Kundenbewertungen | | NewsletterSubscribers | newsletter-subscribers | Newsletter mit Double Opt-In | | SocialLinks | social-links | Social Media Links | | Forms | forms | Formular-Builder | | FormSubmissions | form-submissions | Formular-Einsendungen | | EmailLogs | email-logs | E-Mail-Protokollierung | | CookieConfigurations | cookie-configurations | Cookie-Banner Konfiguration | | CookieInventory | cookie-inventory | Cookie-Inventar | | ConsentLogs | consent-logs | Consent-Protokollierung | ## Globals | Global | Slug | Beschreibung | |--------|------|--------------| | SiteSettings | site-settings | Allgemeine Website-Einstellungen | | Navigation | navigation | Navigationsmenü | | SEOSettings | seo-settings | SEO-Einstellungen | | PrivacyPolicySettings | privacy-policy-settings | Datenschutz-Einstellungen | *Letzte Aktualisierung: 07.12.2025*