White-Label & Reseller
Hoppla self-hosted bringt den vollständigen Reseller-Stack mit: gebrandete Kundenportale, verwaltete Tenants, Per-Reseller-SMTP, White-Label-Status-Pages und -Benachrichtigungen. Diese Anleitung bringt dich von einer normalen Installation zu gebrandeten Portalen für deine Kunden.
Branding löst sich in drei Schichten auf -- die spezifischste gewinnt:
- Tenant-Branding -- das eigene Logo/die Farben eines verwalteten Tenants (in dessen Einstellungen).
- Reseller-Branding -- das Brand des Portals; verwaltete Tenants erben es automatisch, solange sie kein eigenes gesetzt haben.
- Instanz-Brand -- Env-Defaults für alles Übrige (System-Mails, Fallback-Footer, Alert-Präfix). Setze diese so, dass nirgends „Hoppla" steht.
1. Instanz-Brand (Env)
Setze diese auf den API-/Worker-Pods (api.env in deiner Werte-Datei):
| Env | Default | Wofür |
|---|---|---|
BRAND_NAME | Hoppla | E-Mail-Titel/-Footer, Fallbacks der Notification-Driver, „powered by" |
BRAND_URL | https://hoppla.dev | Host für den E-Mail-Footer-Link |
ALERT_PREFIX | Hoppla! | Betreff-Präfix auf Alarm-Mails + Chat-/Webhook-Pushes |
DASHBOARD_URL | https://app.hoppla.dev | Deep-Links in Benachrichtigungs-Mails -- zeig auf deine domains.app |
SUPPORT_EMAIL | [email protected] | „Falls du diese Änderung nicht vorgenommen hast…"-Kontakt in Security-Mails |
CRAWLER_USER_AGENT | HopplaBot/1.0 (+https://hoppla.dev/bot) | UA, den deine Checks präsentieren -- sichtbar in den Logs überwachter Seiten |
Das sind die Last-Resort-Fallbacks; Per-Tenant-/Reseller-Branding gewinnt immer dort, wo es gesetzt ist.
2. Reseller-Portale
Ein Reseller ist ein Tenant auf dem reseller-Plan (die limits deiner Lizenz entscheiden, wie viele verwaltete Teams er tragen darf). Befördere einen Tenant über den Super-Admin-Bereich (/dashboard/admin) oder lege deinen ersten direkt per hoppla:install an.
Slug-Portale -- <slug>.deine-portal-root
- Setze
domains.portalRootin deinen Werten (z. B.portal.example.com). - DNS: Wildcard-Eintrag
*.portal.example.com→ deine Ingress-IP. - TLS: ein Wildcard-Zertifikat für
*.portal.example.com-- mit cert-manager nutzt du einen DNS-01-Issuer; mitprovisioner: manualbringst du dein eigenes mit. - Das Portal jedes Resellers ist dann unter
<sein-slug>.portal.example.comlive.
Eigene Portal-Domains -- monitoring.client-agency.com
Der Reseller fügt seine Domain im Dashboard hinzu (Reseller → Portal). Was als Nächstes passiert, hängt von customDomains.provisioner ab:
ingress(Default): Die API erstellt den Ingress + das cert-manager-Certificate selbst. Der Reseller zeigt ein CNAME auf dein Portal-CNAME-Ziel und verifiziert -- fertig.manual: Die API gibt Betreiber-Anweisungen zurück (in der UI des Resellers angezeigt) und du legst Route/TLS selbst an.
Portal-Branding
Der Reseller konfiguriert alles unter Einstellungen → White-Label in seinem Dashboard: Brand-Name, Logo, Favicon, Primär-/Sekundärfarben, Design-Tokens (Radius/Spacing/Schatten), Dashboard-Layout, Footer, Rechtslinks. Das brandet:
- die Portal-Login-/Registrierungsseiten,
- die Dashboard-Hülle, die ihre Kunden sehen,
- alle Benachrichtigungs-Mails + Chat-Pushes (Alert-Präfix, Absendername, Footer),
- die Status Pages ihrer Kunden (inklusive „powered by"),
- den öffentlichen Migrations-Funnel (
/migrateauf ihrem Portal).
Upsell-Verhalten
Reseller steuern, ob/wie ihre Kunden zu Upgrades für Features außerhalb ihres Plans angestupst werden -- und entscheidend: Die Erfahrung leakt nie Vendor-Plannamen oder -Checkout. Setze einen Account-Default (PATCH /api/v1/branding), den alle Kunden erben, und überschreibe pro Kunde (PATCH /api/v1/reseller/managed-teams/{id}/branding). Felder:
upsell_mode:hidden(Default -- Kunden sehen nie, was sie nicht gekauft haben) ·silent(gezeigt, deaktiviert, kein CTA) ·subtle(markenkonformer Hinweis + ein CTA) ·active(+ Nudges).upsell_cta:none·link(→ dieupgrade_urldes Resellers, mit angehängtem?tenant=&plan=&feature=&suggested=-Kontext) ·contact(→support_url).checkoutwird abgelehnt -- es ist Vendor-intern, ein Kunde wird also nie zur Abrechnung der Instanz geschickt.plan_labels: bildet interne Stufen auf die Paketnamen des Resellers ab (z. B.{"starter":"Growth"}).upsell_offers: Per-Feature-Angebot in der eigenen Produkt-Taxonomie des Resellers --{"server_monitoring":{"label":"Infrastructure","url":"https://portal/buy/infra"}}. Gezeigt (Name + URL), wenn das Feature gesperrt ist; Keys sind Feature-Slugs (Unbekannte ignoriert), URLs SSRF-geprüft. Per-Team-Angebote überschreiben den Account-Default.upsell_threshold: Für numerische Limits (Monitors, Status Pages, Kanäle) sind das aufgelöste Limit/die Nutzung live (Plan + Overrides). Das Angebot feuert am Cap; inactive-Haltung erscheint zusätzlich ein sanfter Nudge, sobald die Nutzung diesen Bruchteil des Limits überschreitet (0,5–0,99, Default 0,8). Der Upgrade-Link trägtusage+limit, damit der Reseller zur richtigen Stufe routet.
Inkludiert vs. upgesellt: Was ein Kunde hat = Plan + feature_overrides (was der Reseller bereitstellt); was obendrauf verkauft wird = upsell_offers. Angebote tauchen nur für aktuell gesperrte Features auf, etwas über Overrides Gewährtes wird also nie angeboten -- sie komponieren ohne Überlappung.
Signierter Upgrade-Link. Der Identitätskontext der Weiterleitung wird mit demselben Secret signiert wie die Webhooks des Resellers (das secret eines Reseller-Webhook-Kanals; HMAC-SHA256, wie der X-Hoppla-Signature-Header). Der Link trägt ts + sig; verifizieren durch Neuberechnen von hmac_sha256("tenant={t}&plan={p}&ts={ts}", secret). Unsigniert, wenn der Reseller kein Webhook-Secret hat.
Self-hosted löst die Default-Haltung auf hidden auf (die Lizenz ist fix, der Betreiber wird also nie genervt); ein Reseller opted pro Kunde ein. Die effektive Policy, die das Dashboard eines Kunden anwendet, steht im upsell-Block von GET /api/v1/plan.
Reseller-SMTP
Reseller können aus ihrer eigenen Domain senden: Einstellungen → White-Label → Custom SMTP (API: GET/PATCH/DELETE /api/v1/smtp-config, POST /api/v1/smtp-config/test). Schon einen Provider (z. B. Mailgun)? Host/Port/Username/Passwort/From eintragen, Test senden zum Verifizieren vor dem Speichern, und speichern. Die Konfiguration wird beim Speichern validiert und verschlüsselt abgelegt; nur Allowlist-Submission-Ports werden akzeptiert (25, 465, 587, 2525), und der Host wird beim Senden SSRF-geprüft. Die Zustellung fällt auf den Instanz-Mailer zurück, wenn das SMTP des Resellers kaputtgeht oder der Plan das Feature entzieht -- eine schlechte Konfiguration verschluckt also nie still Alarme. Das ist unabhängig vom Instanz-MAIL_* (siehe Installation → E-Mail) und jederzeit setzbar.
Eigene Status-Page-Domains -- status.client.com
Über das Portal hinaus kann jede Status Page auf der eigenen Domain des Tenants leben. Voraussetzung: Setze domains.statusRoot (z. B. status.example.com), damit die Instanz ein CNAME-Ziel bewirbt, das sie tatsächlich kontrolliert -- ohne das wird die Seite auf dem /status/<slug>-Pfad der App-Domain ausgeliefert. Dann:
- Domain auf der Status Page hinzufügen (Dashboard → Status Page → Custom Domain).
- Die Instanz zeigt das exakte zu erstellende CNAME, z. B.
status.client.com CNAME cname.status.example.com. - DNS für
*.status.example.com(und das CNAME-Ziel) muss auf deine Ingress-IP auflösen. - In der UI verifizieren. Mit
customDomains.provisioner: ingress(Default) erstellt die API dann den Ingress + das cert-manager-Certificate selbst, und TLS wird automatisch (HTTP-01) ausgestellt. Mitprovisioner: manualgibt sie Betreiber-Anweisungen zurück, um Route/TLS selbst zu verdrahten.
Eigene Status-Domains verwalteter Tenants zeigen per CNAME auf das gebrandete Ziel des Resellers, dessen Kunden sehen also nie einen Hoppla-Hostnamen.
3. Kunden-Anmeldung (verwalteter Tenant)
Zwei Wege:
- Vom Reseller angelegt: Der Reseller fügt Teams aus seinem Dashboard hinzu. Wird gegen die
managed team-Limits gezählt (Lizenz → Reseller-Plan → Per-Tenant-Override). - Self-Signup auf dem Portal: Der Reseller aktiviert
portal_allow_registration(Reseller → Portal-Einstellungen). Neue Konten werden automatisch zu verwalteten Tenants dieses Resellers, mit dessen Registrierungs-Defaults (Plan -- gedeckelt auf seinen maximalen verwalteten Plan -- Trial-Tage, Feature-Overrides).
Mapping auf die Kundendatenbank des Resellers. Jeder verwaltete Tenant trägt eine tenant_id (UUID -- der kanonische Key, an dem der Plan hängt), eine public_id (HOP-xxxx, menschenlesbar) und reseller-eigene external_ref + metadata, die der Reseller setzt (POST/PATCH /api/v1/reseller/managed-teams). external_ref ist pro Reseller eindeutig, macht das Provisioning idempotent (retry-sicher), unterstützt Reverse-Lookup (GET ?external_ref=) und wird -- mit metadata und public_id -- in jedem Reseller-Webhook unter _managed_tenant mitgeschickt, sodass ein Partner die Abrechnung gegen seine eigenen Kunden-IDs abgleicht, ohne eine separate Mapping-Tabelle.
Self-Signup (und der öffentliche Migrations-Funnel) ist per Cloudflare Turnstile captcha-geschützt. Einmal pro Instanz konfigurieren:
- API:
TURNSTILE_SECRET_KEY(in deinemhoppla-secrets) - Dashboard:
TURNSTILE_SITE_KEYviadashboard.runtimeConfig(nächster Abschnitt)
Ohne Schlüssel schlägt die Captcha-Verifikation fehl-geschlossen, wo sie erforderlich ist -- setze beide, bevor du irgendeine Registrierungsfläche aktivierst.
4. Dashboard-Runtime-Config
Das Dashboard-Image ist vorgebaut; Per-Installations-Werte werden zur Laufzeit über /config.js aus der dashboard.runtimeConfig-Map in deiner Werte-Datei ausgeliefert:
dashboard:
runtimeConfig:
APP_DOMAIN: app.example.com # meist aus domains.app abgeleitet — nur zum Überschreiben setzen
PORTAL_DOMAIN: portal.example.com # = domains.portalRoot
STATUS_DOMAIN: status.example.com # = domains.statusRoot
DOCS_URL: "" # optionaler Docs-Link in der UI
# Live-Dashboard-Updates (WebSockets). MUSS dem REVERB_APP_KEY in
# deinem hoppla-secrets Secret entsprechen (install.sh hat ihn generiert):
# kubectl -n hoppla get secret hoppla-secrets -o jsonpath='{.data.REVERB_APP_KEY}' | base64 -d
REVERB_APP_KEY: "<your reverb app key>"
TURNSTILE_SITE_KEY: "0x..." # öffentlicher Site-Key (passt zum API-Secret)
SENTRY_DSN: "" # optionales Error-Reporting, deine eigene DSNÄnderungen an dieser Map rollen den Dashboard-Pod automatisch (Checksum-Annotation) -- kein Rebuild nötig.
5. Probe-Standorte
Uptime-Checks zeichnen einen Probe-Standort auf, und Fehler werden vor dem Alarmieren vom sekundären verifiziert (Single-Location-Aussetzer pagen nie jemanden). Bei einer Single-Cluster-Installation laufen beide vom selben Ort -- setze ehrliche Labels:
api:
env:
PROBE_LOCATION: "eu-yourcity"
PROBE_SECONDARY: "eu-yourcity-2" # selber Cluster, zweiter Versuchhoppla:doctor warnt, wenn beide Labels identisch sind. Echtes Multi-Region-Probing braucht Worker-Pools in einem zweiten Cluster -- siehe Skalierung & Betreiber → Multi-Probe-Standorte.
6. Migrations-Importer
Das Mitbewerber-Import-Tooling (Oh Dear, UptimeRobot -- mehr kommt) ist enthalten:
- Authentifizierter Wizard -- jeder Tenant (auch verwaltete Tenants auf Reseller-Portalen) hat Einstellungen → Import & Migration und
/dashboard/migrate. Funktioniert ab Werk: einen Read-Only-Mitbewerber-API-Key einfügen, Vorschau ansehen, Monitors + Status Pages + Downtime-Historie importieren. - Öffentlicher Pre-Signup-Funnel --
/migrateauf der Haupt-App und auf Reseller-Portalen lässt Besucher einen Import in der Vorschau ansehen, bevor sie ein Konto anlegen. Er ist davon abhängig, dass Registrierung aktiviert ist (globales Flag, oder dasportal_allow_registrationdes Resellers) und braucht Turnstile (Abschnitt 3). Eingereichte Mitbewerber-Keys werden verschlüsselt gespeichert und nach einer Stunde automatisch gelöscht; Hygiene-Jobs sind vorgeplant (migration:prune-previewsstündlich,migration:prune-credentialstäglich).
Kurz-Checkliste zum Onboarding eines Resellers
domains.portalRootgesetzt + Wildcard-DNS/TLS in Stellung (einmal pro Instanz)- Tenant auf den
reseller-Plan befördert, Managed-Team-Limit gesetzt - Reseller konfiguriert Branding + (optional) Custom SMTP
- Optional: eigene Portal-Domain hinzugefügt + CNAME verifiziert
- Optional:
portal_allow_registration+ Turnstile-Keys für Self-Signup php artisan hoppla:doctor-- alles grün