Files
craftvia/docs/craftvia/DEPLOY.md
T
msolarczekandClaude Opus 5 cadaedc6cc L10b Betrieb & Aufräumen: Deploy – craftvia-worker, CI-Testjob, DEPLOY.md, Certvia-Doku archiviert
- docker-compose.coolify(.prebuilt).yml: Service craftvia-worker (Target worker, Chromium,
  shm_size 1gb, gleiche Härtung), Craftvia-Variablen für app und worker.
- Dockerfile: worker-Stage mit HOME=/home/app (Chromium-Profil für non-root); lokaler
  docker build der Targets runner und worker erfolgreich, PDF-Erzeugung im Image geprüft.
- .env.example/.env.prod.example/.env.coolify.example: alle Craftvia-Variablen inkl. RLS,
  KI-Provider, PDF_CHROMIUM_PATH, OFFLINE_MAX_DAYS, API_RATE_LIMIT_*, AI_GENERATION_RETENTION_DAYS,
  AI_MONTHLY_TOKEN_LIMIT.
- CI (.github, .gitea): Job gate mit Postgres (pgvector) und Redis als Service: migrate deploy,
  seed, Passwort für craftvia_app, tsc, lint, build, npm run test.
- docs/craftvia/DEPLOY.md (aus den Certvia-Deploy-Docs abgeleitet): Architektur, Domains, Secrets,
  Worker, Migrationen, RLS-Aktivierung, Backup/Restore, KI, Rate Limits, Aufbewahrung, Smoke,
  Update/Rollback. build-and-push-images.sh baut craftvia-worker.
- Certvia-/ISMS-Dokumente aus docs/ nach docs/_certvia-archiv/ (mit README); Verweise in README.md
  und Skript-Kommentaren angepasst.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:19:19 +02:00

23 KiB
Raw Blame History

Craftvia – Betrieb & Deployment

Maßgeblich für Test- und Produktivbetrieb. Abgeleitet aus den Fundament-Runbooks (archiviert unter docs/_certvia-archiv/) und auf Craftvia umgeschrieben. Domains in diesem Dokument sind Platzhalter (app.craftvia.example). Deploy-Dateien: docker-compose.coolify.yml (Build auf dem Host) bzw. docker-compose.coolify.prebuilt.yml (fertige Images aus der Registry). Env-Referenzen: .env.coolify.example (Testserver), .env.prod.example (Produktion), .env.example (lokal).

1. Architekturüberblick

                Internet ──► Coolify-Proxy (Traefik, TLS) ──► app:3000
                                                               │
 ┌─────────────── Netz "backend" (internal: true, kein Egress) ─┼──────────────────────────┐
 │  postgres (pgvector/pg16, RLS)   redis (requirepass)   garage (S3 :3900, Admin :3903)   │
 │     ▲          ▲        ▲            ▲    ▲     ▲         ▲                             │
 │  migrate    app  craftvia-worker  worker  backup-worker  garage-provision              │
 └──────────────────────────────────────────────────────────────────────────────────────────┘
      app, craftvia-worker, worker, backup-worker zusätzlich im Netz "default" (Egress/Proxy)
Dienst Dockerfile-Target / Image Aufgabe Lebensdauer
migrate migrate / craftvia-migrate prisma migrate deploy → Rollen-Rechte-Sync (scripts/sync-role-permissions.ts) → optional Demo-Seed (RUN_DEMO_SEED) bzw. Erst-Admin (BOOTSTRAP_ADMIN) Init-Job, restart: "no"
app runner / craftvia-app Next.js standalone (Backoffice, PWA /m, REST /api/v1/**, Betreiber-Portal /admin) dauerhaft, Healthcheck auf /
craftvia-worker worker / craftvia-worker BullMQ-Queues import-extraction, transcription, report-pdf, image-derivatives (scripts/craftvia-worker.ts); enthält Chromium + Schriften dauerhaft
worker migrate / craftvia-migrate Mail-Worker (scripts/mail-worker.ts): Zustellung mit Retry/DLQ, täglicher Erinnerungslauf dauerhaft
backup-worker migrate / craftvia-migrate Queue backup-ops (scripts/backup-worker.ts): Mandanten-Export/-Restore, DSGVO-Export, seriell dauerhaft
postgres pgvector/pgvector:0.8.0-pg16 Primärdatenbank, RLS-Policies tenant_isolation Volume pgdata
redis redis:7.4.2-alpine Queues (BullMQ) Volume redisdata
garage garage / craftvia-garage Objektspeicher (Dokumente, Fotos, PDFs, Backups), Config eingebacken aus deploy/garage.toml Volumes garage_meta, garage_data
garage-provision migrate / craftvia-migrate Layout, Bucket, Access-Key, Rechte über die Admin-API (idempotent) Init-Job

Startreihenfolge: postgres (healthy) → migrate; garage (healthy) → garage-provision; danach app, craftvia-worker, worker, backup-worker.

Ohne laufende Worker reiht die App bei gesetztem REDIS_URL Jobs nur ein: Import-Extraktion, Transkription, Berichts-PDFs und Bild-Derivate bleiben dann liegen (craftvia-worker), Mails bleiben pending (worker), Restore/Export bleiben queued (backup-worker). Ohne Redis laufen die Craftvia-Processors inline in der App. Das ist nur für Dev/Demo gedacht: der PDF-Processor ist absichtlich nicht im App-Bundle, Freigaben bleiben gültig, „PDF erzeugen" auf /reports/[id] stößt den Job erneut an.

Härtung (alle Dienste): no-new-privileges, cap_drop: ALL (gezielte cap_add nur für die Entrypoints von postgres/redis), CPU-/RAM-Limits, gepinnte Image-Tags, non-root-User app (UID 1001) in allen Node-Images, Redis mit Passwort.

2. Domains, Proxy, TLS

  • In Coolify beim Service app die Domain setzen, z. B. https://app.craftvia.example:3000 (Port 3000 = Container-Port, Schema https). Coolify/Traefik stellt das Let's-Encrypt-Zertifikat aus. Kein Host-Port wird exponiert. garage, postgres, redis und die Worker erhalten keine Domain.
  • AUTH_URL = exakt die öffentliche App-URL (https://app.craftvia.example), AUTH_TRUST_HOST=true (im Compose Default). APP_BASE_URL für absolute Links in Mails/PDFs (leer = AUTH_URL).
  • Passkeys: WEBAUTHN_ORIGIN/WEBAUTHN_RP_ID nur setzen, wenn sie von AUTH_URL abweichen.
  • Uploads bis 25 MB laufen über den Proxy (experimental.proxyClientMaxBodySize = 26mb). Vorgelagerte Proxies dürfen kein niedrigeres Body-Limit haben.
  • DNS: A/AAAA-Record app.craftvia.example → Server-IP; Firewall nur 80/443 + SSH.

3. Ersteinrichtung (Coolify)

  1. Ressource: Git-Repo (Deploy-Key, nur lesend), Build Pack Docker Compose, Compose Location docker-compose.coolify.yml (oder …prebuilt.yml, siehe §9).
  2. Environment-Variablen aus .env.prod.example bzw. .env.coolify.example eintragen (§4). Secrets literal setzen, nicht über ${…} referenzieren (Coolify-Interpolation).
  3. Persistent Storage prüfen: pgdata, garage_meta, garage_data, redisdata, backups.
  4. Erster Deploy Produktion: RUN_DEMO_SEED=false, BOOTSTRAP_ADMIN=true + BOOTSTRAP_ADMIN_* + BOOTSTRAP_TENANT_*. Der migrate-Job legt über scripts/bootstrap-admin.ts den ersten Mandanten mit Mandanten-Admin (/login) und einen Plattform-Admin (/platform/login, MFA-Einrichtung beim ersten Login) an. Das Skript ist idempotent und überschreibt kein Passwort. Log im migrate-Container prüfen, danach Passwort ändern und BOOTSTRAP_ADMIN=false.
  5. Testserver: RUN_DEMO_SEED=true legt die Demo-Mandanten an (Logins siehe AGENTS.md). Nie in Produktion.
  6. RLS scharfschalten (§6), Smoke (§10).

4. Konfiguration & Secrets

4.1 Variablen

Variable Dienste Pflicht Bedeutung
POSTGRES_USER/PASSWORD/DB, DATABASE_URL postgres, alle Node-Dienste ja Owner-Verbindung (Superuser/BYPASSRLS), Host postgres
RLS_ENFORCED, RLS_DATABASE_URL app, craftvia-worker Prod ja scharfe RLS über Rolle craftvia_app (§6)
REDIS_PASSWORD redis + alle Queue-Nutzer ja REDIS_URL wird im Compose daraus gebildet, nicht separat setzen
S3_ENDPOINT/ACCESS_KEY/SECRET_KEY/BUCKET/REGION app, craftvia-worker, backup-worker, garage-provision ja Garage: http://garage:3900, Key GK+24 Hex, Secret 64 Hex, Region us-east-1. Ohne S3 speichert der Adapter nur Metadaten (Stub), in Prod also unbrauchbar
GARAGE_RPC_SECRET, GARAGE_ADMIN_TOKEN garage, garage-provision ja je openssl rand -hex 32
AUTH_SECRET app, Worker ja ≥ 32 Zeichen, sonst Fail-Secure-Abbruch (src/server/env.ts)
PASSWORD_PEPPER app, migrate, backup-worker ja 64 Hex, nicht rotierbar
MFA_ENC_KEY app, craftvia-worker empfohlen TOTP-Secrets at-rest (leer = aus AUTH_SECRET), nach dem Setzen nicht ändern
BACKUP_ENC_KEY backup-worker empfohlen AES-256-GCM der Backup-Artefakte (leer = AUTH_SECRET)
AUTH_URL, APP_BASE_URL app, Worker ja öffentliche URL
SMTP_HOST/PORT/SECURE/USER/PASSWORD/FROM, MAIL_FROM_NAME, MAIL_REPLY_TO app, worker, craftvia-worker für Mailversand ohne vollständige Konfiguration bleiben Mails pending mit Begründung
AI_EXTRACTION_PROVIDER, ANTHROPIC_API_KEY, ANTHROPIC_MODEL app, craftvia-worker optional §7
TRANSCRIPTION_PROVIDER/API_URL/API_KEY/MODEL app, craftvia-worker optional §7
AI_MONTHLY_TOKEN_LIMIT app, craftvia-worker optional §7.2, Default 0 = unbegrenzt
AI_GENERATION_RETENTION_DAYS craftvia-worker optional §7.3, Default 180
API_RATE_LIMIT_PER_MINUTE, API_FIELD_RATE_LIMIT_PER_MINUTE app optional §8, Default 300 / 1200
OFFLINE_MAX_DAYS app optional Offline-Bundle gilt nach N Tagen als veraltet (1–365, Default 7)
CLAMAV_HOST, CLAMAV_PORT app optional zusätzlicher Malware-Scan per clamd INSTREAM (sonst Allowlist + Magic Bytes)
PDF_CHROMIUM_PATH craftvia-worker – im Image/Compose fest /usr/bin/chromium
BACKUP_LOCAL_DIR, BACKUP_S3_BUCKET app, backup-worker, garage-provision optional §11
RUN_DEMO_SEED, BOOTSTRAP_ADMIN*, BOOTSTRAP_TENANT_* migrate – §3

4.2 Secrets-Register (Grundregeln)

Werte liegen ausschließlich im Passwortmanager (je Umgebung eigener Ordner) plus versiegelter Offline-Kopie und werden nur als Coolify-Env verteilt, nie im Repo, Image oder Backup-Bucket. Test, Staging und Prod haben unterschiedliche Werte.

Secret Rotierbar? Folge einer Rotation
AUTH_SECRET ja alle Sessions ungültig, Nutzer loggen neu ein
PASSWORD_PEPPER nein erzwungener Passwort-Reset aller Konten
MFA_ENC_KEY nein alle Nutzer müssen MFA neu einrichten
BACKUP_ENC_KEY bedingt gilt nur für neue Artefakte, Altschlüssel bis Retention-Ende aufbewahren
craftvia_app-Passwort (RLS_DATABASE_URL) ja ALTER ROLE … PASSWORD, danach Env setzen und app + craftvia-worker neu starten
POSTGRES_PASSWORD, REDIS_PASSWORD ja koordiniert mit allen Diensten neu deployen
GARAGE_*, S3_ACCESS_KEY/SECRET_KEY ja neuen Key provisionieren (garage-provision), Env tauschen, alten Key entfernen
ANTHROPIC_API_KEY, TRANSCRIPTION_API_KEY, SMTP_PASSWORD ja Env tauschen, Dienste neu starten

5. Migrationen

  • Der migrate-Job führt bei jedem Deploy npx prisma migrate deploy aus. Das ist idempotent: nur neue Migrationen werden angewandt. app und Worker starten erst nach erfolgreichem Abschluss (service_completed_successfully). Der App-Container migriert nie selbst.
  • Danach läuft scripts/sync-role-permissions.ts: additiv, zieht neu eingeführte Rechte für bestehende Mandanten nach. Betroffene Nutzer sehen neue Rechte nach erneutem Login (JWT).
  • Regeln für neue Migrationen (RLS für Tenant-Tabellen usw.): MIGRATIONS.md.
  • Manuell (Coolify-Terminal des migrate-Containers oder docker exec): npx prisma migrate status / npx prisma migrate deploy.
  • Vor Migrationen mit Datenumbau: Cluster-Backup ziehen (§11.1).

6. Row Level Security aktivieren

Die Baseline-Migration legt die Rolle craftvia_app NOLOGIN NOBYPASSRLS an, vergibt die Tabellenrechte und aktiviert je Tenant-Tabelle ENABLE + FORCE ROW LEVEL SECURITY mit Policy tenant_isolation (USING + WITH CHECK auf current_setting('app.tenant_id', true)). Mit RLS_ENFORCED=true verbinden sich app und craftvia-worker über RLS_DATABASE_URL als craftvia_app und setzen app.tenant_id transaktionslokal (src/server/db.ts, dbForTenant, tenantTransaction). Migrationen, Seed/Bootstrap, Login-Lookup, Mail- und Backup-Worker laufen weiter über die Owner-DATABASE_URL.

  1. Passwort für die App-Rolle setzen (einmalig je Umgebung, Wert in den Passwortmanager):
    docker exec -it <postgres-container> psql -U craftvia -d craftvia \
      -c "ALTER ROLE craftvia_app WITH LOGIN PASSWORD '<STARKES_PASSWORT>';"
    
  2. Env setzen: RLS_ENFORCED=true, RLS_DATABASE_URL=postgresql://craftvia_app:<STARKES_PASSWORT>@postgres:5432/craftvia?schema=public
  3. Neu deployen. Fehlt RLS_DATABASE_URL bei aktivem Flag, bricht der Prozess beim Start ab (fail secure).
  4. Nachweis: npx tsx scripts/test-rls-enforcement.ts. Der Test prüft Owner-Sicht, Isolation A/B, 0 Zeilen ohne Kontext, WITH CHECK und dbForTenant end-to-end. Er gehört auch zum CI-Gate.

Warnungen:

  • Die Owner-Rolle in DATABASE_URL muss Superuser oder BYPASSRLS sein. Sonst sieht der Login keine Nutzer, und Restore (session_replication_role) scheitert.
  • Mehrschritt-Schreibvorgänge nur über inTransaction(ctx, fn), direktes ctx.db.$transaction ist unter RLS_ENFORCED=true nicht atomar (ARCHITEKTUR §4.8).
  • Zeilen mit tenant_id = NULL (Plattform-Audit, Mail-Logs, Auth-Tokens) sind für craftvia_app unsichtbar. Sie werden nur über den Owner-Client geschrieben.

7. Worker, Chromium und KI-Provider

7.1 craftvia-worker und Chromium

  • Image-Stage worker (Dockerfile): node:22.14.0-slim + Debian-Pakete chromium, fonts-dejavu-core, fonts-liberation; node_modules inkl. tsx, generierter Prisma-Client, scripts/, src/, messages/, prisma/. Start npx tsx scripts/craftvia-worker.ts.
  • PDF-Rendering (src/server/pdf/render.ts): playwright-core startet PDF_CHROMIUM_PATH mit --no-sandbox --disable-dev-shm-usage. Der Container braucht deshalb keine zusätzlichen Capabilities. shm_size: 1gb ist als Reserve für fotoreiche Berichte gesetzt. Seiten laden keine Netzressourcen, alle Assets sind als data: eingebettet.
  • Concurrency: report-pdf 2, übrige Queues 4 je Worker-Prozess. Horizontal skalieren = weitere craftvia-worker-Replicas (BullMQ verteilt). RAM-Limit im Compose 1,5 GB.
  • Diagnose im Container: chromium --version; Logs zeigen [worker] listening on <queue> bzw. [worker] <queue> job <id> failed: ….

7.2 KI-Provider

Zweck Env Verhalten ohne Konfiguration
Auftragsimport-Extraktion (PDF/Bild) AI_EXTRACTION_PROVIDER=anthropic, ANTHROPIC_API_KEY, ANTHROPIC_MODEL (leer = claude-opus-5) Import bleibt manuell erfassbar
Lotse (Berichtsentwurf, Vollständigkeitsprüfung) ANTHROPIC_API_KEY, ANTHROPIC_MODEL kein Entwurf, UI funktioniert weiter
Transkription von Sprachnotizen TRANSCRIPTION_PROVIDER=openai-compatible, TRANSCRIPTION_API_URL (Default OpenAI /v1/audio/transcriptions), TRANSCRIPTION_API_KEY, TRANSCRIPTION_MODEL (Default whisper-1) Status disabled
  • Jede KI-Nutzung wird in AiGeneration protokolliert (Art, Provider, Modell, Bezug, Tokens ein/aus, auslösender Nutzer, Ein-/Ausgabe).
  • Kostenbremse: AI_MONTHLY_TOKEN_LIMIT ist die Plattform-Vorgabe für Tokens (ein + aus) je Mandant je Kalendermonat (UTC), 0 = unbegrenzt. Mandantenadministratoren können unter /settings/lotse einen eigenen Wert setzen (TenantSettings.aiMonthlyTokenLimit; leer = Plattform-Vorgabe, 0 = unbegrenzt). Ist das Kontingent aufgebraucht, lehnt der Lotse neue Entwürfe/Zusammenfassungen ab („Kontingent aufgebraucht“), die Import-Extraktion fällt auf manuelle Erfassung zurück. Geprüft wird vor jedem Aufruf – ein laufender Aufruf kann das Limit einmalig überschreiten. Transkription (Audio) liefert keine Tokens und wird nicht gezählt.
  • Datenschutz: Anbieter (Anthropic, Transkriptions-API) sind Auftragsverarbeiter, daher AVV und Drittlandbewertung vor Aktivierung klären. Die Worker brauchen Egress (Netz default).

7.3 Aufbewahrung KI-Protokoll

AI_GENERATION_RETENTION_DAYS (Default 180): Der Job ai-retention (Queue gleichen Namens) läuft täglich im craftvia-worker (BullMQ-Job-Scheduler ai-retention-daily, beim Worker-Start registriert, idempotent auch bei mehreren Replikas). Er leert Ein- und Ausgaben (input/output) von AiGeneration-Einträgen, die älter als die Frist sind, und entfernt den Personenbezug (createdById). Metadaten (Art, Modell, Tokens, Zeitpunkt, Bezug) bleiben für Kosten- und Nachvollziehbarkeit erhalten; je Mandant wird ein Audit-Eintrag ai_generation_retention geschrieben. Die Frist mit dem DSB abstimmen. Die Variable muss im craftvia-worker gesetzt sein.

8. Rate Limits

Bereich Env Default Zählung
REST-API /api/v1/** API_RATE_LIMIT_PER_MINUTE 300 je Nutzer pro Minute
Einsatz/Sync: /api/v1/sync, /api/v1/uploads, /api/v1/field/** API_FIELD_RATE_LIMIT_PER_MINUTE 1200 je Nutzer pro Minute
Passwort-Reset, Alt-Passwort-Prüfung, E-Mail-Änderung fest (src/server/rate-limit.ts) 5–10 je Fenster je IP und je Konto

Das Field-Limit ist höher, weil die PWA nach Offline-Phasen Outbox-Batches (≤ 50 Ops) und Fotos in Schüben nachsendet. Wird es zu knapp gewählt, laufen die Clients in Retry/Backoff, und die Sync-Seite zeigt Fehler. Limits je Prozess gelten pro App-Instanz. Bei mehreren Replicas multipliziert sich das effektive Limit.

9. Prebuilt-Images (Registry)

Wenn der Host-Build in Coolify zu lange dauert, die Images auf einem Build-Host (amd64) bauen, in die Registry pushen und in Coolify docker-compose.coolify.prebuilt.yml verwenden.

Image Target Dienste
${REGISTRY}/craftvia-app:${IMAGE_TAG} runner app
${REGISTRY}/craftvia-migrate:${IMAGE_TAG} migrate migrate, worker, backup-worker, garage-provision
${REGISTRY}/craftvia-worker:${IMAGE_TAG} worker craftvia-worker
${REGISTRY}/craftvia-garage:${IMAGE_TAG} garage garage
docker login <registry-host>
REGISTRY=registry.example.com/craftvia ALSO_MAIN=true ./scripts/build-and-push-images.sh
# worker-Image (bis das Skript es mitbaut):
docker build --platform linux/amd64 --target worker -t registry.example.com/craftvia/craftvia-worker:main .
docker push registry.example.com/craftvia/craftvia-worker:main

Gotchas: Coolify reicht IMAGE_TAG nicht zuverlässig in die Compose-Interpolation, daher immer auch :main pushen. Coolify entfernt alte Container vor dem Pull: erst alle Images pushen, dann Redeploy, sonst ist die Umgebung unten. Registry-Token mit Minimalrechten (read/write:package) verwenden und nach Klartext-Nutzung widerrufen.

10. Smoke nach Deploy

  1. migrate und garage-provision mit Exit 0 beendet. app ist healthy, craftvia-worker, worker und backup-worker laufen, im Log steht [worker] listening on report-pdf usw.
  2. https://app.craftvia.example/login lädt mit gültigem Zertifikat. /sw.js und /site.webmanifest sind ohne Session erreichbar (PWA).
  3. Login Backoffice → /dashboard, /work-orders, /customers, /reports. Login Monteur → /m.
  4. Datei-Upload an einem Auftrag und Download über /files/<documentId> (prüft Garage + S3-Keys).
  5. Bericht freigeben → PDF erscheint am Auftrag (prüft Queue, craftvia-worker, Chromium).
  6. Optional: Import-PDF hochladen → Extraktion (bei gesetztem API-Key). Sprachnotiz → Transkription.
  7. Test-Mail (z. B. Passwort-Reset) kommt an bzw. steht nachvollziehbar auf pending.
  8. Plattform-Login /platform/login → /admin, /admin/backup zeigt das Backup-Ziel.
  9. Bei aktiver RLS: Login + Auftragsliste funktionieren (sonst prüfen: RLS_DATABASE_URL, Rolle hat LOGIN).

Automatisierter HTTP-Smoke mit Session-Cookie (ohne Passworteingabe, braucht DB-Zugriff und AUTH_SECRET): BASE=https://app.craftvia.example npx tsx scripts/smoke-auth.ts (z. B. im migrate-Container oder von einem Admin-Host mit Tunnel zur DB).

11. Backup & Restore

Zwei Ebenen, beide sind nötig: DB und Objektspeicher.

11.1 Ebene A: Cluster (gesamte Datenbank + Volumes)

  • Postgres: mindestens täglich pg_dump -Fc (Coolify Scheduled Task) in einen separaten, verschlüsselten Speicher. Für PITR pgBackRest/wal-g mit WAL-Archiving und repo-cipher-type=aes-256-cbc. Deckt auch die globalen Tabellen (Identity, Plattform-Admins, Kataloge) ab.
    docker exec <postgres-container> pg_dump -U craftvia -d craftvia -Fc > craftvia-$(date +%F).dump
    # Restore in leere DB (App + Worker gestoppt):
    docker exec -i <postgres-container> pg_restore -U craftvia -d craftvia --clean --if-exists < craftvia-YYYY-MM-DD.dump
    
  • Garage: garage_meta (Bucket-/Key-/Layout-Definitionen, kritisch) und garage_data sichern, z. B. mit restic (eigenes Repo, eigenes Passwort) oder als Volume-Snapshot bei gestopptem garage. Ohne garage_meta sind die Objektdaten nicht adressierbar.
  • Volume backups: enthält lokale App-Backup-Artefakte (Ebene B), mitsichern.
  • Host-Encryption: Daten-Volumes auf LUKS bzw. provider-verschlüsseltem Block-Storage. Das Boot-Unlock-Verfahren dokumentieren.
  • Restore-Test mindestens quartalsweise in eine Wegwerf-Umgebung, Ergebnis protokollieren.

11.2 Ebene B: Mandanten-Export/-Restore und DSGVO (Betreiber-Portal)

  • Ziel der Artefakte in /admin/backup wählbar (Lokal = Volume /app/.backups oder S3). Die Konfiguration liegt verschlüsselt in der DB. Präzedenz: DB-Config → S3_*/BACKUP_LOCAL_DIR → lokaler Default.
  • Export, Restore und DSGVO-Export je Mandant unter /admin/[id] (Plattform-Full-Admin + MFA-Step-up), ausgeführt vom backup-worker. Die Artefakte sind mit BACKUP_ENC_KEY (AES-256-GCM) verschlüsselt, der Restore arbeitet nur innerhalb von tenant_id und betrifft keine anderen Mandanten. TENANT_MODELS in src/server/db.ts und src/server/backup/topology.ts müssen jede Tenant-Tabelle enthalten.

11.3 Restore-Kohärenz (Vorbedingung)

PASSWORD_PEPPER, MFA_ENC_KEY und BACKUP_ENC_KEY stehen nicht im Backup. Ein Restore in eine Umgebung mit anderen Werten macht Logins (Pepper), MFA (MFA_ENC_KEY) bzw. das Entschlüsseln der Artefakte (BACKUP_ENC_KEY) unmöglich. Vor jedem Restore die Secrets der Quellumgebung bereitstellen oder einen Passwort-/MFA-Reset einplanen. Cross-Environment-Restores (prod → staging) sind nur so lauffähig.

12. Update & Rollback

Update (Standard):

  1. CI grün (Gate-Job: migrate, seed, tsc, lint, build, Tests).
  2. Migrationen der Release sichten. Bei destruktiven Änderungen vorher pg_dump (§11.1).
  3. Coolify-Redeploy (bzw. Images pushen, dann Redeploy). migrate läuft vor app und Workern.
  4. Smoke (§10). Neue Env-Variablen aus den .env.*.example-Dateien vorher eintragen.

Rollback:

  • Ohne Schemaänderung: vorheriges Image-Tag als :main retaggen und pushen (Prebuilt) bzw. vorherigen Commit deployen. Die Worker ziehen dasselbe Tag mit.
  • Mit Schemaänderung: Prisma-Migrationen haben kein automatisches Down. Entweder Vorwärts-Fix (neue Migration), oder App + Worker stoppen, DB aus dem Pre-Deploy-Dump wiederherstellen (§11.1), dann den alten Stand deployen. prisma migrate deploy toleriert in der DB angewandte Migrationen, die im alten Code fehlen.
  • Queues: Beim Rollback können Jobs eines neueren Payload-Formats in Redis liegen. Vor dem Rollback Worker-Logs prüfen, fehlgeschlagene Jobs nach dem Fix erneut anstoßen (z. B. „PDF erzeugen").

13. Go-Live-Checkliste

  • Frische, starke Secrets je Umgebung, im Passwortmanager + versiegelte Offline-Kopie
  • RUN_DEMO_SEED=false, Bootstrap-Admin-Passwort geändert, BOOTSTRAP_ADMIN=false
  • HTTPS aktiv, AUTH_URL/APP_BASE_URL korrekt
  • RLS_ENFORCED=true + RLS_DATABASE_URL, Smoke mit aktiver RLS bestanden
  • craftvia-worker läuft, Test-PDF erzeugt
  • SMTP mit SPF/DKIM/DMARC der Absenderdomain
  • KI: AVV geklärt, AI_MONTHLY_TOKEN_LIMIT und AI_GENERATION_RETENTION_DAYS festgelegt
  • Backups Ebene A (Postgres + garage_meta/garage_data + backups) eingerichtet, Restore-Test dokumentiert
  • Monitoring: Uptime-Check auf die App-URL, Log-Aggregation, Alarm bei Worker-Neustarts
  • Firewall (80/443/SSH), SSH-Key-Login, unattended-upgrades