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#130und#132. - Finaler Lauf:
#10300, exakte Revisionf54b04162b2823b1d2f6db0318baf1a7431c5b66, 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-webundhivegrid-portalliefen aus demselben gepinnten Image/Digest; beide bestanden/healthzund/readyzmit 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.degehö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:
- Ein aktueller Backup-/Restore-Punkt ist bestätigt. Für Produktion ist zusätzlich ein dokumentierter Restore-Drill gemäß Estate-Standard erforderlich.
pnpm run db:roles:provisionwurde mit den fünf getrennten Passwort-Keys aus dem Secret-Store ausgeführt.hivegrid_portalist danachLOGIN,NOINHERIT,NOBYPASSRLS, kein Superuser und hat keine Rollenmitgliedschaft.- PgBouncer enthält einen eigenen SCRAM-Eintrag für
hivegrid_portal, wurde neu geladen und akzeptiert den Portal-Login. Details stehen indocs/ops/pgbouncer.md. hivegrid-portalist in Staging gestoppt beziehungsweise noch nicht öffentlich erreichbar angelegt, verwendet dasselbe Web-Image wiehivegrid-webund steht in der Staging-Ressourcen-Allowlist. Vor einer Produktion-Promotion gilt dasselbe für Produktion.- Bestehende Runtime-Ressourcen enthalten weder
DIRECT_URL,MIGRATE_ON_BOOT,SEED_ON_BOOT,SEED_*nochMIGRATOR_*. Diese Schlüssel gehören nur in den kurzlebigen Migrationsschritt. - Core-Web und Portal haben in Coolify jeweils positive, explizite Hard-Caps in
limits_cpusundlimits_memory; Portal und Core haben getrennte PgBouncer-Zugangsdaten und bewusst gesetzte Pool-Limits.0/unlimited, nurlimits_cpu_sharesoder nur eine Memory-Reservation zählen nicht als Hard-Cap. Für den ersten Produktionsrollout sind Core mit 2 vCPU/2 GB undDATABASE_POOL_MAX=10sowie Portal mit 1 vCPU/1 GB undDATABASE_POOL_MAX=5je Prozess/Instanz festgelegt. Spätere Anpassungen erfolgen nur anhand gemessener Last und bleiben als explizite Hard-Caps dokumentiert. worker-mailerhält denselbenPORTAL_APP_HOSTwie die Portal-Ressource, damit Status-Mails keine ausAPP_BASE_DOMAINabgeleitete falsche URL erzeugen.worker-mailerhält exakt denselbenCRYPTO_SECRETwiehivegrid-web. Ohne diesen gemeinsamen Envelope-Key müssen Worker-Start, Healthcheck und Deploy-Preflight fail-closed abbrechen; der Wert darf nie geloggt werden.worker-generalerhältLAGO_API_URL,LAGO_API_KEY,LAGO_TIMEOUT_MS,MOLLIE_API_KEY,MOLLIE_PROFILE_IDundMOLLIE_TIMEOUT_MSaus 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_IDbleibt optional.hivegrid-web,hivegrid-portal,worker-importundworker-generalverwenden denselben privaten Bucket sowie dieselben normalisierten Werte fürSTORAGE_ENDPOINT,STORAGE_REGION,STORAGE_BUCKETundSTORAGE_FORCE_PATH_STYLE. Ein vorhandenes gemeinsamesSTORAGE_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 denselbenGDPR_EXPORT_SSE_C_KEY_BASE64; der reine Lösch-Worker benötigt und erhält diesen Customer-Key nicht.STORAGE_SESSION_TOKENwird 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 QuellenWEB_STORAGE_*,WORKER_IMPORT_STORAGE_*undWORKER_GENERAL_STORAGE_*; diese Quellen dürfen auf dasselbe Credential zeigen. Die Portal-Ressource bleibt eine separate Coolify-Resource mit vollständig gesetztenSTORAGE_*-Werten. Der Deploy-Preflight prüft Vollständigkeit und gemeinsame Storage-Zielwerte, vergleicht oder protokolliert die Credential-Werte jedoch nicht.worker-importundworker-generalmüssen für die gemeinsame Tenant-Export-Barriere außerdem denselben normalisierten Redis-Endpunkt und dieselbe Redis-Datenbank verwenden. Dafür gilt effektiv ein nichtleeresREDIS_URL, sonstBULLMQ_REDIS_URL; Zugangsdaten dürfen getrennt sein. Fehlende, ungültige oder abweichende Ziele brechen den Deploy-Preflight ab, ohne Redis-URLs auszugeben.worker-generalerhält zusätzlichMEILI_URLund den HiveGrid-scopedMEILI_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:
- Privaten Bucket, default-deny Policy sowie deaktiviertes Versioning/Object Lock
bestätigen. Die aktive Lifecycle-Konfiguration muss genau ein
Expiration.Days=3-Statement fürgdpr-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-Keyspacestenants/*/gdpr/*.zipundtenants/*/portal-gdpr/*/*.jsonmit einem separat verwahrten Operator-Credential inventarisieren, vollständig löschen und die dazugehörigen DB-Jobs aufexpiredsetzen beziehungsweise ihre alte Objektadresse entfernen. Nur redigierte Counts, Policy-Hash und Prüfzeitpunkt dokumentieren. Der neue Reader darf diese Klartextobjekte nie als Fallback akzeptieren. - 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.
- Ü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.
worker-generalmuss 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 keinenGDPR_EXPORT_SSE_C_KEY_BASE64erhalten 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.- 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.
- 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.tenantIdist lückenlos und verweist auf den richtigen Tenant.- Pro Tenant existiert höchstens ein Portal, bevor der neue Unique-Index entsteht.
PortalUser.tenantIdist 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
passwordHashwerdenactive; alle übrigen übernahmefähigen PortalUser werdeninvited. Bestehende Hashes werden byte-identisch erhalten und niemals neu gehasht oder durch ein Default-Passwort ersetzt.emailVerifiedAtwird nur aus einem vor dem Cutover dokumentierten, belastbaren Verifikationsbeleg übernommen. Ein fehlender Legacy-verifyTokenallein zählt nicht als Beleg und führt deshalb zuNULL.
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:
- Fehlende
tenantId-/emailNormalized-/Locale-Werte nach dem freigegebenen Mapping backfillen und die Preflight-Prüfungen erneut ausführen. - Die sieben geprüften Legacy-Commerce-Policies sowie die Policy von
PortalUserRegistrationexplizit entfernen. - Acht Legacy-Tabellen FK-sicher child-first entfernen:
PortalUserSubscriptionProductPrice,PortalUserSubscriptionProduct,PortalSubscriptionFeature,PortalSubscriptionPrice,PortalUserSubscription,PortalSubscriptionProduct,PortalCheckoutSessionStatusundPortalUserRegistration. - 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. - 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. - Die schmalen
hivegrid_portal-Policies, Spalten-Grants undSECURITY DEFINER-Resolver einbetten. Die Actor-Invarianten erzwingen fürFile,FileUpload,FileEntityLinkundportal_form-FormSubmissiongenau den zulässigen internen oder Portal-Aktor. Die schmalen Domain-Controller-Capabilities bleiben vom Portal-Login getrennt. SQL-Dateien mit eigenemBEGIN/COMMITwerden 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:
- leere Datenbank nach vollständigem
prisma migrate deployder bestehenden Chain; - 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
- Portal-Ressource und Core-Web zunächst ohne öffentlichen Portal-DNS vollständig nach der Runtime-Matrix konfigurieren. Alle Ressourcen-Allowlisten aktualisieren.
- Merge erst freigeben, wenn Migration, Rollen, PgBouncer und Coolify-Inventar vorbereitet sind.
- Der Main-Workflow migriert einmalig, pinnt Web und Portal auf denselben SHA/Digest,
deployt beide getrennt und prüft
/healthzsowie/readyzauf Service, Surface, Revision und echte DB-Rolle/RLS-Bereitschaft. - 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 kannproxied=truegesetzt werden. - 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;
/healthzund/readyzmelden 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.