Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/docs/deploy/customer-portal.md
T

38 KiB

Customer-Portal: Erst-Rollout und Betrieb

Dieses Runbook beschreibt den sicheren Erst-Rollout von VA-P26a sowie die vorbereiteten VA-P26b-/VA-P26c-Verträge. Es enthält nur Schlüsselnamen und Platzhalter. Ressourcen-IDs, URLs mit Zugangsdaten, Passwörter, Tokens und Live-Konfiguration gehören ausschließlich in die freigegebenen Secret- und Betriebswerkzeuge.

Stand 2026-07-22: VA-P26a, VA-P26b und VA-P26c sind technisch vollständig. Der aktuelle Produkt- und UX-Vertrag einschließlich Tenant-Logo, Endkundenreisen und Designgrenzen steht in docs/features/customer-portal.md. PR #212 wurde als Main-Revision 090e2558a6965e1c5804eb24d3eab5da8c8611a8 erfolgreich nach Staging ausgerollt. Core und Portal verwenden denselben Web-Digest; Health, Readiness, alle abhängigen Dienste und der revisionsgebundene Domain-Controller sind grün. Produktion wurde durch diesen UI-/Branding-Release nicht verändert.

Ticket Aktueller Stand Formales Abschluss-Gate
VA-P26a 8/8 DoD = 100 % technischer Handoff abgeschlossen; finaler Design-Polish bleibt VD-P26
VA-P26b 6/6 DoD = 100 % technischer Handoff abgeschlossen; finaler Design-Polish bleibt VD-P26
VA-P26c 8/8 DoD = 100 % technischer Handoff abgeschlossen; Produktion bleibt eine separate Freigabe

Abschlussnachweis des Basis-Deploys

  • Feature-Merge: PR #125; Deploy-Hotfixes: PR #130 und #132.
  • Finaler Lauf: #10300, exakte Revision f54b04162b2823b1d2f6db0318baf1a7431c5b66, vollständig grün für Staging und Produktion.
  • Forward-only Migration: prisma/migrations/20260716123000_va_p26_customer_portal/migration.sql, ausgeführt im einmaligen eingeschränkten Migrator-Container.
  • Migration-Chain-Parität, exakter Tenant-Policy-Vertrag, RLS-Release-Readiness und Live-RLS- Isolation waren grün. App, Worker und Portal verwenden getrennte Runtime-Rollen über PgBouncer.
  • hivegrid-web und hivegrid-portal liefen aus demselben gepinnten Image/Digest; beide bestanden /healthz und /readyz mit der erwarteten Revision. Hard-Caps und getrennte Pool-Limits waren Bestandteil des Deploy-Preflights.

Dieser Nachweis erfüllt das Merge-/RLS-Gate von VA-P26a/b. Er ersetzt nicht den UAT-Tenant, Browser-Klickpfade, Billing-Provider-Proof oder die VA-P26c-Controller-/Custom-Domain-/TLS-Gates.

Zielbild

hivegrid-web und hivegrid-portal sind zwei getrennte Coolify-Ressourcen aus demselben unveränderlichen hivegrid/web:<sha>-Image und demselben Registry-Digest. Die Core-Ressource läuft als hivegrid_app; die Portal-Ressource ausschließlich als hivegrid_portal. Der Deploy-Workflow provisioniert weder die Portal-Ressource noch einen DNS-Eintrag; die bereits bestehende Worker-Reconciliation bleibt davon unberührt. Für den Portal-Rollout akzeptiert er nur vorab angelegte, eindeutig allowlistete Ressourcen und bricht bei Abweichungen vor dem Rollout ab.

Standardhosts:

  • Staging: staging-portal.hivegrid.de
  • Produktion: portal.hivegrid.de
  • Kundeneigene Hosts wie portal.geerkens.de gehören zu VA-P26c. Claim, sichere Host-/Session- Auflösung und Admin-Self-Service sind lokal vorbereitet; eine Annahme im Live-Ingress erfolgt erst nach den unten beschriebenen Controller-, TLS-, Merge- und Staging-Gates.

Harte Vorbedingungen vor dem Main-Merge

Der Main-Workflow prüft das Coolify-Inventar und die Runtime-Grenzen vor der Migration und bricht bei Abweichungen ohne Schemaänderung ab. Trotzdem müssen alle folgenden Punkte bereits vor dem Merge erfüllt sein:

  1. Ein aktueller Backup-/Restore-Punkt ist bestätigt. Für Produktion ist zusätzlich ein dokumentierter Restore-Drill gemäß Estate-Standard erforderlich.
  2. pnpm run db:roles:provision wurde mit den fünf getrennten Passwort-Keys aus dem Secret-Store ausgeführt. hivegrid_portal ist danach LOGIN, NOINHERIT, NOBYPASSRLS, kein Superuser und hat keine Rollenmitgliedschaft.
  3. PgBouncer enthält einen eigenen SCRAM-Eintrag für hivegrid_portal, wurde neu geladen und akzeptiert den Portal-Login. Details stehen in docs/ops/pgbouncer.md.
  4. hivegrid-portal ist in Staging gestoppt beziehungsweise noch nicht öffentlich erreichbar angelegt, verwendet dasselbe Web-Image wie hivegrid-web und steht in der Staging-Ressourcen-Allowlist. Vor einer Produktion-Promotion gilt dasselbe für Produktion.
  5. Bestehende Runtime-Ressourcen enthalten weder DIRECT_URL, MIGRATE_ON_BOOT, SEED_ON_BOOT, SEED_* noch MIGRATOR_*. Diese Schlüssel gehören nur in den kurzlebigen Migrationsschritt.
  6. Core-Web und Portal haben in Coolify jeweils positive, explizite Hard-Caps in limits_cpus und limits_memory; Portal und Core haben getrennte PgBouncer-Zugangsdaten und bewusst gesetzte Pool-Limits. 0/unlimited, nur limits_cpu_shares oder nur eine Memory-Reservation zählen nicht als Hard-Cap. Für den ersten Produktionsrollout sind Core mit 2 vCPU/2 GB und DATABASE_POOL_MAX=10 sowie Portal mit 1 vCPU/1 GB und DATABASE_POOL_MAX=5 je Prozess/Instanz festgelegt. Spätere Anpassungen erfolgen nur anhand gemessener Last und bleiben als explizite Hard-Caps dokumentiert.
  7. worker-mail erhält denselben PORTAL_APP_HOST wie die Portal-Ressource, damit Status-Mails keine aus APP_BASE_DOMAIN abgeleitete falsche URL erzeugen.
  8. worker-mail erhält exakt denselben CRYPTO_SECRET wie hivegrid-web. Ohne diesen gemeinsamen Envelope-Key müssen Worker-Start, Healthcheck und Deploy-Preflight fail-closed abbrechen; der Wert darf nie geloggt werden.
  9. worker-general erhält LAGO_API_URL, LAGO_API_KEY, LAGO_TIMEOUT_MS, MOLLIE_API_KEY, MOLLIE_PROFILE_ID und MOLLIE_TIMEOUT_MS aus dem freigegebenen Coolify-Secret-/Env-Source. Fehlende Provider-Keys oder ungültige gesetzte Timeouts müssen im Deploy-Preflight beziehungsweise beim Required-RLS-Start fail-closed abbrechen. MOLLIE_PROFILE_ID bleibt optional.
  10. hivegrid-web, hivegrid-portal, worker-import und worker-general verwenden denselben privaten Bucket sowie dieselben normalisierten Werte für STORAGE_ENDPOINT, STORAGE_REGION, STORAGE_BUCKET und STORAGE_FORCE_PATH_STYLE. Ein vorhandenes gemeinsames STORAGE_ACCESS_KEY_ID-/STORAGE_SECRET_ACCESS_KEY-Paar ist für alle vier Ressourcen zulässig und kein Rollout-Blocker; getrennte Credentials bleiben optional. Nur Core, Portal und Import-Worker erhalten exakt denselben GDPR_EXPORT_SSE_C_KEY_BASE64; der reine Lösch-Worker benötigt und erhält diesen Customer-Key nicht. STORAGE_SESSION_TOKEN wird nur gesetzt, wenn das jeweilige Credential ihn tatsächlich benötigt. Fehlende Pflichtwerte, ein nicht-HTTPS-Endpoint oder ein nicht kanonisch Base64-kodierter 32-Byte-Key brechen den Produktionsstart fail-closed ab. Die lokale Staging-Compose-Referenz bildet dieselbe Konfiguration über die host-seitigen Quellen WEB_STORAGE_*, WORKER_IMPORT_STORAGE_* und WORKER_GENERAL_STORAGE_*; diese Quellen dürfen auf dasselbe Credential zeigen. Die Portal-Ressource bleibt eine separate Coolify-Resource mit vollständig gesetzten STORAGE_*-Werten. Der Deploy-Preflight prüft Vollständigkeit und gemeinsame Storage-Zielwerte, vergleicht oder protokolliert die Credential-Werte jedoch nicht. worker-import und worker-general müssen für die gemeinsame Tenant-Export-Barriere außerdem denselben normalisierten Redis-Endpunkt und dieselbe Redis-Datenbank verwenden. Dafür gilt effektiv ein nichtleeres REDIS_URL, sonst BULLMQ_REDIS_URL; Zugangsdaten dürfen getrennt sein. Fehlende, ungültige oder abweichende Ziele brechen den Deploy-Preflight ab, ohne Redis-URLs auszugeben.
  11. worker-general erhält zusätzlich MEILI_URL und den HiveGrid-scoped MEILI_API_KEY. In Production, required-RLS sowie Staging-/Production-Sentry muss der Worker vor Queue-Ownership abbrechen, wenn Wert oder Search-Client fehlen; ein Tenant-Hard-Delete darf sonst nicht erfolgreich abschließen.

Runtime-Matrix

Bereich hivegrid-web hivegrid-portal worker-mail
Surface WEB_RUNTIME_SURFACE=core WEB_RUNTIME_SURFACE=portal nicht setzen
DB-Rolle DATABASE_RUNTIME_ROLE=hivegrid_app DATABASE_RUNTIME_ROLE=hivegrid_portal DATABASE_RUNTIME_ROLE=hivegrid_worker
DB-Modus RLS_RUNTIME_MODE=required RLS_RUNTIME_MODE=required RLS_RUNTIME_MODE=required
Produktions-Pool je Instanz DATABASE_POOL_MAX=10 DATABASE_POOL_MAX=5 nach Worker-Vertrag
Produktions-Hard-Cap 2 vCPU / 2 GB RAM 1 vCPU / 1 GB RAM nach Worker-Vertrag
Session SESSION_SECRET PORTAL_SESSION_SECRET keine Web-Session
Tenant-Envelope-Key CRYPTO_SECRET nicht erforderlich exakt derselbe CRYPTO_SECRET wie hivegrid-web
Portalhost PORTAL_APP_HOST PORTAL_APP_HOST PORTAL_APP_HOST
Custom-Domain-Runtime in Staging exakt PORTAL_CUSTOM_DOMAIN_RUNTIME_ENABLED=true; Produktion bis zum freigegebenen Controller unset/false identischer expliziter Staging-Opt-in; Produktion bis zur Freigabe unset/false nicht setzen
Redis nach Core-Funktion gültiges REDIS_URL; optional gültiges BULLMQ_REDIS_URL Queue-Verbindung nach Worker-Vertrag
GDPR-Export-Storage gemeinsamer Endpoint/Region/Bucket, vollständiges Storage-Credential; gemeinsames Paar zulässig gemeinsamer Endpoint/Region/Bucket, vollständiges Storage-Credential; gemeinsames Paar zulässig nicht setzen
GDPR-Export-SSE-C GDPR_EXPORT_SSE_C_KEY_BASE64, exakt wie im Import-Worker exakt derselbe Wert wie hivegrid-web und Import-Worker nicht setzen
CAPTCHA Core-Konfiguration CAPTCHA_PROVIDER=cap oder mcaptcha plus passende Login-Keys und Instanz-URL nicht erforderlich
Auth-Mail Core-Mailkonfiguration nur Queue-Verbindung; keine Mailjet-Provider-Credentials Mailjet-Absender-/Provider-Keys für Auth- und Status-Mails plus Queue-Verbindung
Admin SSO/OAuth/Billing nur dort, wo der Core-Vertrag es erlaubt verboten nach eigenem Worker-Vertrag, nie Portal-Credentials

Auf hivegrid-portal sind zusätzlich alle Core-Session-, Authentik-, OAuth-, Billing-, Coolify-, SSH-, Traefik-, Ingress-, Cloudflare-, Domain-Controller- und veralteten PORTAL_SERVER_*-Keys verboten. APP_REVISION wird aus dem Image gelesen und darf nicht als Coolify-Override existieren.

GDPR-Export-Storage: Schlüssel und effektive Rechte

Der dedizierte Import-Worker schreibt Portal- und Privacy-Exporte mit SSE-C. Beide Web-Surfaces lesen sie ausschließlich serverseitig mit demselben Customer-Key; es gibt keinen Presigned-URL- oder Klartext-Fallback. Die S3-Zugangsdaten dürfen von den vier Runtimes geteilt werden; getrennte Principals sind optional und werden nicht vom Deploy-Preflight erzwungen. Für die Anwendungspfade gilt diese Funktionsmatrix:

Runtime Erlaubte Object-Actions Erlaubte Object-ARNs/Key-Prefixe Explizit nicht benötigt
hivegrid-web s3:GetObject arn:aws:s3:::<bucket>/gdpr-exports/privacy/tenants/*/*/* Portal-GDPR-Prefix, s3:PutObject, s3:DeleteObject, s3:ListBucket
hivegrid-portal s3:GetObject arn:aws:s3:::<bucket>/gdpr-exports/portal/tenants/*/*/* Core-GDPR-Prefix, s3:PutObject, s3:DeleteObject, s3:ListBucket
worker-import s3:PutObject; zusätzlich s3:DeleteObject nur nach einem eindeutig fehlgeschlagenen Attempt, niemals bei unklarem Commit-Ausgang oder Owner-CAS-Verlust beide oben genannten Object-Prefixe s3:GetObject, s3:ListBucket
worker-general s3:DeleteObject; bucket-level s3:ListBucket nur mit s3:prefix-Condition auf tenants/*, gdpr-exports/privacy/tenants/* und gdpr-exports/portal/tenants/* beide oben genannten Object-Prefixe sowie der bestehende Hard-Delete-Prefix arn:aws:s3:::<bucket>/tenants/* Customer-Key, s3:GetObject, s3:PutObject, unbeschränktes s3:ListBucket

Bei einem gemeinsamen Credential benötigt dessen Bucket-Policy die Vereinigungsmenge der in der Tabelle aufgeführten Actions und Prefixe. Die Trennung der App-Pfade bleibt im Code erhalten, ist dann aber keine Principal-Isolation des Providers. worker-general erhält unabhängig davon nie den SSE-C-Customer-Key und kann die Exportinhalte deshalb nicht entschlüsseln. Falls das Credential weitere, bereits freigegebene Storage-Funktionen bedient, erhalten diese eigene, explizite Statements auf deren Prefixe; daraus darf kein s3:* auf dem gesamten Bucket entstehen. Alle Artefakte liegen im gemeinsamen, export-first Keyspace gdpr-exports/; jeder Versuch erhält einen eigenen attemptId, sodass ein später oder parallel verlorener Versuch nicht das veröffentlichte Artefakt überschreibt. Der private Export-Bucket verwendet für diese Objekte weder Versioning noch Object Lock, weil der aktuelle DeleteObject-Pfad sonst nur einen Delete-Marker setzen beziehungsweise an einer Retention scheitern könnte. Die Bucket-Policy ist default-deny für andere Principals, Actions und Prefixe.

Als Defense-in-depth liegt auf exakt dem Prefix gdpr-exports/ zusätzlich eine Provider-Lifecycle-Regel mit Expiration.Days=3. Sie schützt auch dann vor dauerhaft liegen gebliebenen Attempts, wenn App-Kompensation und beide Sweeper ausfallen. Sie ist kein Ersatz für den 15-Minuten-Sweep und keine Zusage einer minutengenauen Löschung: Hetzner verarbeitet Lifecycle-Expiration asynchron. Die Regel darf weder den übrigen Bucket noch andere Prefixe erfassen. Hetzner dokumentiert action-spezifische Bucket-Policies, SSE-C und Lifecycle-Regeln; das Repository kann ihre tatsächlich angewandte Provider-Konfiguration jedoch nicht allein beweisen (S3-Credentials, SSE-C, Lifecycle, unterstützte Actions).

Privacy-Exporte sind nach resultExpiresAt höchstens 72 Stunden, Portal-Exporte höchstens 24 Stunden über die authentifizierte App-Route abrufbar. Die Route sperrt den Zugriff ab diesem Zeitpunkt sofort. Der erforderliche Lösch-Scheduler läuft im 15-Minuten-Takt; physisch gelöscht wird im ersten erfolgreichen Sweep nach Ablauf. Die planmäßige Löschlatenz beträgt damit bis zu 15 Minuten zuzüglich Laufzeit, ist bei Provider-/Queue-Störung aber ausdrücklich nicht garantiert. Fehler müssen den Job fehlschlagen lassen, erneut versucht werden und bis zur erfolgreichen Löschung als Alarm offen bleiben. Eindeutig fehlgeschlagene Attempts werden unmittelbar kompensiert. Bei unklarem PUT- oder Ready-CAS-Ausgang bleiben Claim und Pointer erhalten, damit der Retry denselben Key übernimmt; der Sweep erfasst zusätzlich deterministisch adressierbare verwaiste Artefakte.

Bei einer Rotation werden Import-Worker, Core und Portal gemeinsam umgestellt, nachdem alle mit dem alten Customer-Key verschlüsselten Exportobjekte erfolgreich gelöscht wurden. Im Dedicated-Modus setzt der Import-Worker PORTAL_GDPR_EXPORT_QUEUE_ENABLED=true und hält IMPORT_QUEUE_ENABLED=false sowie IMPORT_QUEUE_RUNTIME_READY=false; eine Mischkonfiguration bricht Runtime- und Deploy-Preflight fail-closed ab. Der alternative Full-Modus setzt beide IMPORT_QUEUE_*-Gates auf true und hält PORTAL_GDPR_EXPORT_QUEUE_ENABLED unset oder false. Auch dann bleiben der Storage-/SSE-C-Vertrag sowie Portal-, Privacy- und generische Import-/Export-Service-Wirings verpflichtend. Im Idle-Modus bleiben alle drei Gates unset oder false; dann darf der Import-Worker seinen Storage-/SSE-C-Vertrag weglassen und konsumiert keine Import-/Export-Queue. Core und Portal behalten ihren verschlüsselten Downloadvertrag unabhängig davon. worker-general setzt in jedem Staging-/Produktions-Rollout EXPORT_RETENTION_QUEUE_ENABLED=true und startet nicht bereit, wenn Scheduler, Redis oder sein Delete-only-Storage-Vertrag fehlen. Ein gesetzter GDPR_EXPORT_SSE_C_KEY_BASE64 ist auf diesem Lösch-Worker ausdrücklich verboten und wird von Runtime- und Deploy-Preflight zurückgewiesen.

Operator-Checkpoint: Provider-Vertrag

Unit-Tests und der Coolify-Preflight beweisen nur Key-Format, Env-Vollständigkeit, gemeinsamen Bucket/Endpoint und die Runtime-Verdrahtung. Sie beweisen weder die aktive Hetzner-Bucket-Policy noch SSE-C- oder Hard-Delete-Semantik. Vor jedem neuen Environment und nach jeder Policy-/Credential-Rotation führt ein Operator deshalb mit ausschließlich wegwerfbaren Testobjekten den folgenden redigierten Check aus:

  1. Privaten Bucket, default-deny Policy sowie deaktiviertes Versioning/Object Lock bestätigen. Die aktive Lifecycle-Konfiguration muss genau ein Expiration.Days=3-Statement für gdpr-exports/ enthalten und darf keinen breiteren Prefix erfassen. Policy-/Lifecycle-Hash und Prüfzeitpunkt dokumentieren, nie Schlüsselwerte. Vor dem ersten SSE-C-Rollout zusätzlich die Legacy-Keyspaces tenants/*/gdpr/*.zip und tenants/*/portal-gdpr/*/*.json mit einem separat verwahrten Operator-Credential inventarisieren, vollständig löschen und die dazugehörigen DB-Jobs auf expired setzen beziehungsweise ihre alte Objektadresse entfernen. Nur redigierte Counts, Policy-Hash und Prüfzeitpunkt dokumentieren. Der neue Reader darf diese Klartextobjekte nie als Fallback akzeptieren.
  2. Mit dem konfigurierten Storage-Credential je ein Lese- und ein Kompensations-Testobjekt mit SSE-C in beide GDPR-Prefixe schreiben und die Kompensationsobjekte wieder löschen. Für den GDPR-Keyspace müssen Actions und Prefixe außerhalb der oben beschriebenen Export-/Hard-Delete-Matrix scheitern; außerhalb dieses Keyspaces gilt die vollständig inventarisierte Vereinigungsmenge aller für das gemeinsame Credential explizit freigegebenen Actions und Prefixe.
  3. Über die authentifizierten App-Routen bestätigen, dass Core nur das Privacy-Objekt und Portal nur das Portal-Objekt mit Customer-Key ausliefert. Kreuz-Prefix sowie Abruf ohne oder mit falschem Customer-Key müssen scheitern. Ein gemeinsamer Provider-Principal wird hierbei bewusst nicht als Runtime-Isolation gewertet.
  4. worker-general muss die drei für Tenant-Hard-Delete benötigten Prefixe (tenants/{tenantId}/, Privacy-Export, Portal-Export) listen und darin löschen können. Der Worker darf keinen GDPR_EXPORT_SSE_C_KEY_BASE64 erhalten und bietet ausschließlich den Delete-/Retention-Pfad an. Bei einem gemeinsamen Storage-Credential ist eine providerseitige Get-/Put-Sperre für diesen einzelnen Prozess nicht nachweisbar; verpflichtend bleiben die Prefix-Grenzen der Bucket-Policy. Danach wird bestätigt, dass die Objekte fehlen.
  5. Je ein isoliertes, kurz vor Ablauf datiertes UAT-Fixture für Privacy und Portal beobachten: App-Download ab Expiry abgewiesen, Artefakt im ersten erfolgreichen 15-Minuten-Sweep gelöscht, Alarmweg bei einem absichtlich fehlgeschlagenen Delete anschließend wieder resolved. Das Fenster im Produktvertrag bleibt 72 beziehungsweise 24 Stunden; nur dieser Live- Checkpoint zählt als Provider-Proof.
  6. Ein ausschließlich für den Backstop erzeugtes Attempt unter gdpr-exports/ liegen lassen und nach Ablauf der dreitägigen Provider-Regel mit dem separaten Operator-Credential seine Entfernung bestätigen. Das Ergebnis ist ein zeitversetzter Provider-Nachweis und blockiert nicht den primären App-Sweep- Checkpoint; bis zur Bestätigung bleibt es als offener Betriebsnachweis markiert.

VA-P26c: privater Domain-Controller-Vertrag

Die vorbereitete private Ressource heißt hivegrid-portal-domain-controller und verwendet das gleichnamige immutable Image/Target mit derselben expliziten Revision wie der freigegebene Portal-Release. Sie ist nicht öffentlich erreichbar und besitzt ausdrücklich keine Datenbank-, Coolify-, SSH-, Root-, Cloudflare-, allgemeine Traefik-/Ingress- oder Docker-Socket-Credentials. Benötigt werden ausschließlich:

  • eine eigene Redis-Verbindung für die dedizierten signierten Command-/Result-Queues;
  • ein getrenntes Command-Secret sowie ein getrenntes Bearer-Token für /metrics;
  • DNS-/HTTPS-Outbound für Ownership-, CNAME-, SNI- und Health-Prüfungen;
  • Schreibrecht ausschließlich auf /data/coolify/proxy/dynamic/hivegrid-portal-domains/, nicht auf das übergeordnete Dynamic-Config-Verzeichnis;
  • das environment-spezifische, provider-qualifizierte Coolify-Ziel https-0-<hivegrid-portal-uuid>@docker; ein frei wählbares oder provider-loses Backend ist verboten.

Commands tragen claimId, tenantId, generation, hostname und action sowie Signatur und Content-Hash. Der Controller verwirft falsche Signaturen, alte Generationen und nicht exakte Hosts. Traefik-Dateien werden über Temp-Datei, fsync und atomaren Rename geschrieben; jede Regel matcht exakt Host() und verwendet kein offenes On-Demand-Wildcard-TLS. active darf erst nach realem SNI-/Portal-Health-Ack gesetzt werden. Dafür läuft ausschließlich der schmale hivegrid_portal_resolve_custom_hostname_health-Resolver vor dem normalen Host-Dispatch: Er akzeptiert nur DNS-verifizierte Claims mit aktivem Entitlement/Payment, persistiertem Controller-Command und Domainstatus provisioning, active oder degraded; pending, disabled, Kill-Switch und Deprovisioning liefern 404. Das ist ein Controller-Probe, keine Freigabe für pre-active Portalinhalte. /healthz bleibt ohne Secrets nutzbar, /metrics ist Bearer-geschützt.

Das Repository enthält Image-, CI-/SBOM-, Health-, Queue- und Renderer-Verträge. Das ist kein Beleg, dass die Ressource in Coolify angelegt, gestartet oder mit dem Estate-Traefik verbunden wurde. Bis dieser Beleg je Environment vorliegt, bleibt PORTAL_CUSTOM_DOMAIN_RUNTIME_ENABLED dort unset oder false. Nur Staging setzt den Wert aktuell auf exakt true, und zwar auf hivegrid-web sowie hivegrid-portal; der Coolify-Helper prüft beide Werte vor jeder Controller-Mutation. Das Gate blendet den Tenant-Admin-Link aus und liefert für Domain-Admin-Route sowie Custom-Host-Routing 404, ohne den Standardhost des Kundenportals abzuschalten.

Merge-Migration

Auf dem Feature-Branch blieb verify:migrations-parity gemäß AD-21 absichtlich rot. Für den Merge wurde daraus genau eine reviewte forward-only P26-Migration für den ausgewählten gemeinsamen Portal-Vertragsstand aus VA-P26a/b/c erzeugt: prisma/migrations/20260716123000_va_p26_customer_portal/migration.sql. Der rohe Prisma-Diff wurde nicht ungeprüft übernommen; auf dem Feature-Branch gab es ausdrücklich keine eigene Migration.

Bestandsdaten-Preflight

Die Migration muss vor jeder destruktiven Änderung fail-closed prüfen:

  • Portal.tenantId ist lückenlos und verweist auf den richtigen Tenant.
  • Pro Tenant existiert höchstens ein Portal, bevor der neue Unique-Index entsteht.
  • PortalUser.tenantId ist lückenlos und entspricht immer dem Tenant seines Portals.
  • Portal-E-Mails sind nach lower(trim(email)) weder leer noch innerhalb eines Portals doppelt.
  • Die später benötigten zusammengesetzten IDs/FKs zwischen Portal, PortalUser, Projekt, Stage und File sind tenant-konsistent.
  • Das freigegebene Legacy-Mapping ist anwendbar: tenant-konsistente PortalUser mit einem nichtleeren, vom aktuellen Passwort-Hasher akzeptierten passwordHash werden active; alle übrigen übernahmefähigen PortalUser werden invited. Bestehende Hashes werden byte-identisch erhalten und niemals neu gehasht oder durch ein Default-Passwort ersetzt. emailVerifiedAt wird nur aus einem vor dem Cutover dokumentierten, belastbaren Verifikationsbeleg übernommen. Ein fehlender Legacy-verifyToken allein zählt nicht als Beleg und führt deshalb zu NULL.

Fehlerhafte oder mehrdeutige Zeilen werden nicht geraten, automatisch gelöscht oder unter einem beliebigen Tenant einsortiert. Sie müssen vor dem Cutover bewusst bereinigt beziehungsweise zugeordnet werden.

Token- und Re-Invite-Entscheidung

Die alten Felder PortalUser.verifyToken und PortalUserRegistration.token enthalten keinen mit dem neuen PortalToken-Vertrag kompatiblen Hash. Sie werden nicht in gültige neue Login-Fähigkeiten umgewandelt. Offene Registrierungen beziehungsweise alte Verify-Tokens werden invalidiert; betroffene Personen erhalten nach dem Cutover einen neuen Setup-/Magic-Link über den geprüften Flow. Dieser Re-Invite muss vor dem Drop fachlich freigegeben und kommuniziert sein.

Die Migration bricht vor jeder Mutation ab, sobald Tenant-/Portal-Zuordnung, normalisierte E-Mail oder Portal-Owner nicht eindeutig sind. Sie archiviert oder verschiebt keine Konfliktzeile automatisch. Legacy-Registrierungen und Tokens werden vollständig invalidiert; weder Klartext- noch Legacy-Tokenwerte dürfen in die neuen PortalToken-Zeilen gelangen.

DDL- und Policy-Reihenfolge

Die Migration führt Preflight, Backfill, Constraints, Tabellenänderungen und RLS in einer atomaren Migration aus:

  1. Fehlende tenantId-/emailNormalized-/Locale-Werte nach dem freigegebenen Mapping backfillen und die Preflight-Prüfungen erneut ausführen.
  2. Die sieben geprüften Legacy-Commerce-Policies sowie die Policy von PortalUserRegistration explizit entfernen.
  3. Acht Legacy-Tabellen FK-sicher child-first entfernen: PortalUserSubscriptionProductPrice, PortalUserSubscriptionProduct, PortalSubscriptionFeature, PortalSubscriptionPrice, PortalUserSubscription, PortalSubscriptionProduct, PortalCheckoutSessionStatus und PortalUserRegistration.
  4. Den vollständigen aktuellen Portal-Vertragsstand anlegen: Auth/Session/Token, ProjectAccess/Stage-/Dokumentfreigabe/Status-Mail, Upload/Reklamation, Subunternehmerzugang, Tenant-Konfiguration/Auto-Release/Notification-Präferenzen, Service-Inbox/Routing, DSGVO-Export/Löschung/Lifecycle sowie Domain-Claim und aktuelle Custom-Domain-State-Machine. Dazu gehören alle Enums, Indizes, zusammengesetzten Tenant-FKs und die sicheren Änderungen an Portal/PortalUser; eine fest verdrahtete Annahme von nur „sechs Portal-Tabellen" ist veraltet.
  5. Nur den noch nicht migrierten Portal-Delta für die normalen hivegrid_app-/ hivegrid_worker-Tenant-Policies einbetten. Bereits migrierte Telephony-Objekte dürfen nicht aus einem breiten Generator-Delta dupliziert werden.
  6. Die schmalen hivegrid_portal-Policies, Spalten-Grants und SECURITY DEFINER-Resolver einbetten. Die Actor-Invarianten erzwingen für File, FileUpload, FileEntityLink und portal_form-FormSubmission genau den zulässigen internen oder Portal-Aktor. Die schmalen Domain-Controller-Capabilities bleiben vom Portal-Login getrennt. SQL-Dateien mit eigenem BEGIN/COMMIT werden nicht blind verkettet; beim Einbetten bleibt genau eine äußere Transaktion.

Der Inverse-SQL-Kommentar rekonstruiert Struktur, Indizes, FKs, Grants und Policies parent-first. Er weist ausdrücklich darauf hin, dass gelöschte Registrierungs- und Commerce-Zeilen nur aus dem Backup, nicht durch Inverse-DDL, wiederhergestellt werden.

Pflicht-Proof der Migration

Vor dem Merge beziehungsweise Rollout müssen grün sein:

pnpm exec prisma validate
pnpm run verify:migrations-parity
HIVEGRID_RLS_REQUIRE_MIGRATION_CHAIN_PARITY=1 pnpm run verify:rls-live
pnpm run verify:tenant-policy-contract
pnpm run verify:rls-release-readiness

Der strikte RLS-Lauf muss eine frische leere Datenbank ausschließlich mit prisma migrate deploy aufbauen. prisma db push, der Feature-Branch-Recut-Helper und manuell nachgeladene Policy-Dateien sind in diesem Proof verboten.

Lokaler Merge-Kandidaten-Proof vom 2026-07-16

Ein bewusst ungetrackter lokaler Merge-Kandidaten-Proof wurde gegen zwei wegwerfbare PostgreSQL-16-Datenbanken ausgeführt. Die zugehörigen Repository-Verträge liegen in 18dfd4825; der temporäre Migrationskandidat selbst wurde gemäß AD-21 nicht in der Feature-Branch committed:

  1. leere Datenbank nach vollständigem prisma migrate deploy der bestehenden Chain;
  2. Datenklon mit den acht Legacy-Commerce-/Registrierungstabellen und repräsentativen PortalUsern.

Beide Läufe verwendeten den eingeschränkten hivegrid_migrator und endeten schema-paritätisch zum aktuellen Prisma-Schema. Der Legacy-Klon bestätigt active für akzeptierte Hashes, invited für nicht akzeptierte/fehlende Hashes, byte-identische Hash-Erhaltung, emailNormalized-/Tenant-/Locale- Backfill und Reset nicht belastbar belegter Verifikation sowie alter Registrierung/Token-Fähigkeiten. Eine normalisierte Duplicate-E-Mail-Kollision bricht fail-closed vor Mutation ab. Vier Actor-Checks sind validiert; 22/22 eingeschränkte Portal-Live-Tests (P26a 14/14 + P26b 7/7 + schmaler Custom-Host-Resolver 1/1) belegen Customer/Subunternehmer, PortalUser/Tenant A/B, active/revoked, Frozen read/write und fail-closed Host-Auflösung. Zusammen mit App-/Worker- und Tenant-Bootstrap- Proofs ist der frische Feature-Overlay-Lauf 68/68 grün.

Dieser Probe-Lauf erlaubt weder verify:migrations-parity noch den migration-only Strict-Lauf grün zu markieren: Der Kandidat liegt bewusst nur außerhalb des Repositories. Vor Merge muss daraus die eine reviewte forward-only Migration mit Inverse-SQL entstehen; danach folgen frische Chain und Staging-RLS-Proof.

Ausgeführter Merge-/RLS-Proof vom 2026-07-16

Die reviewte Migration wurde mit PR #125 in main übernommen und im Deploy-Lauf #10300 als einmaliger Migrationsschritt ausgeführt. Der Lauf prüfte die exakte Revision f54b04162b2823b1d2f6db0318baf1a7431c5b66, Migration-Chain-Parität, Tenant-Policy-Vertrag, RLS-Release-Readiness und Live-RLS-Isolation. App, Worker und Portal liefen mit getrennten Rollen über PgBouncer; Core und Portal verwendeten denselben gepinnten Web-Digest und bestanden /healthz//readyz. Damit sind die oben beschriebenen Merge-/RLS-Gates erfüllt.

Staging-Rollout

  1. Portal-Ressource und Core-Web zunächst ohne öffentlichen Portal-DNS vollständig nach der Runtime-Matrix konfigurieren. Alle Ressourcen-Allowlisten aktualisieren.
  2. Merge erst freigeben, wenn Migration, Rollen, PgBouncer und Coolify-Inventar vorbereitet sind.
  3. Der Main-Workflow migriert einmalig, pinnt Web und Portal auf denselben SHA/Digest, deployt beide getrennt und prüft /healthz sowie /readyz auf Service, Surface, Revision und echte DB-Rolle/RLS-Bereitschaft.
  4. Erst nach grüner interner Bereitschaft den DNS-Eintrag anlegen. Nach Estate- Standard bleibt Cloudflare zunächst proxied=false, bis das LE-Zertifikat gültig ist; anschließend kann proxied=true gesetzt werden.
  5. Die schreibenden Portal-RLS-Integrationstests laufen nur gegen eine wegwerfbare PostgreSQL-Instanz. Shared Staging erhält einen nicht destruktiven Rollen-/Policy-/ Grant-/readyz-Proof und bewusst angelegte UAT-Fixtures.

Staging-UAT

VA-P26b besitzt den vollständigen Admin-/Interaktionsflow; Merge-Migration und eingeschränkter Staging-RLS-Proof sind mit Lauf #10300 grün. Für den formalen Handoff fehlt jetzt ein bewusst angelegter isolierter UAT-Tenant mit PortalUser, ProjectAccess, sichtbarer Stage, freigegebenem Testdokument sowie begrenztem Customer- und Subunternehmerzugang. Es werden keine SEED_*-Runtime-Schalter verwendet.

Zu prüfen sind mindestens:

  • Magic-Link, optionales Passwort, Setup, Verifikation, Reset und Logout;
  • keine User-Enumeration bei unbekannter E-Mail;
  • PortalUser A/B, Tenant A/B und nicht freigegebene Projekte/Dokumente liefern 404;
  • Core-Host weist Portalpfade ab und Portalhost weist Core-/Admin-/API-/Rootpfade ab;
  • Frozen-Tenant verhält sich read-only;
  • Status-Mail enthält den richtigen Portalhost, respektiert Sichtbarkeit/Präferenz, wird debounced und tatsächlich zugestellt;
  • Multi-Upload/Reklamation schreibt erst nach AV-/Storage-Erfolg File-/Link-/Activity-/Inbox- Datensätze und benachrichtigt über notify(); Subunternehmer sehen weder Endkunden noch fremde Projekt-/Dokumentdaten;
  • Admin-Konfiguration, Rechtslink-Aktivierungsgate, Inbox-Routing, Export, 24-Stunden-Lösch-Widerruf und Lifecycle-Jobs verhalten sich unter Frozen read-only;
  • /healthz und /readyz melden den erwarteten Service, die erwartete Surface und die gebackene Revision, ohne DB-Rolle oder Secrets öffentlich auszugeben.

VA-P26c-UAT folgt separat erst nach echtem Billing-Provider-Proof und angelegter Controller-Ressource: TXT/CNAME-Verifikation, exakte Host-Auflösung, SNI/TLS-/Health-Ack, Retry/Revalidation, Remove/Switch, Kill-Switch und Standardhost-Fallback. Localhost-, Mock- oder manuell eingespielte Traefik-Dateien ersetzen diesen Proof nicht.

Produktion und Rollback

Die VA-P26a-Grundlage darf vorab auf Staging verifiziert werden. Eine öffentliche Buchung beziehungsweise Produktfreischaltung erfolgt jedoch erst nach VA-P26b, wenn Workspace-Admin-Verwaltung, Upload/Reklamation und DSGVO-Flows vollständig verfügbar sind. VA-P26c schaltet danach Paket-Gates und Custom-Domains frei.

Produktion wird nur über den bestehenden promote-Workflow mit der exakt in Staging geprüften Live-Staging-SHA und einem erst nach Backup-Bereitschaft manuell gestarteten, input-freien Promote ausgerollt. Ressourcen-, Rollen-, PgBouncer-, Env-, DNS- und Zertifikatsvorbereitung werden vorher nochmals geprüft.

Vor der DB-Migration kann auf den vorherigen Web-Digest zurückgerollt und die Portal- Ressource beziehungsweise ihr DNS deaktiviert werden. Nach der Migration gilt der im Migration-File dokumentierte Inverse-SQL-Pfad; gelöschte Legacy-Zeilen benötigen das Backup. Core und Portal werden immer als Digest-Paar zurückgerollt, damit keine zwei unterschiedlichen Surface-Verträge gleichzeitig laufen.

Für VA-P26c bleibt der Standardhost der verpflichtende Fallback. Bei Controller-, DNS-, TLS- oder Entitlement-Störung wird zuerst der Domain-Kill-Switch gesetzt, die aktuelle Generation deprovisioniert und ausschließlich die exakt zu diesem Claim gehörende generierte Traefik-Datei entfernt. Danach wird die Controller-Ressource auf den zuvor geprüften immutable Digest zurückgesetzt oder gestoppt; Core-/Portal-Digests und fremde Host-Dateien bleiben unberührt. Die 30-Tage-Reclaim- Sperre wird durch einen Rollback nicht umgangen. Ohne erfolgreiches Deprovisioning-/Fallback-UAT wird kein Custom-Host öffentlich aktiviert.

Folge-Tickets

  • VA-P26b ergänzt Admin-Einladungen, Access-Matrix, Stage-/Dokumentfreigaben, Upload/Reklamation und DSGVO-Flows.
  • VA-P26c ergänzt Paket-Gates und den Self-Service-Custom-Domain-Flow inklusive DNS-Verifikation, Zertifikatsstatus und Deaktivierung.