Skip to content

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:

  1. Tenant-Branding -- das eigene Logo/die Farben eines verwalteten Tenants (in dessen Einstellungen).
  2. Reseller-Branding -- das Brand des Portals; verwaltete Tenants erben es automatisch, solange sie kein eigenes gesetzt haben.
  3. 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):

EnvDefaultWofür
BRAND_NAMEHopplaE-Mail-Titel/-Footer, Fallbacks der Notification-Driver, „powered by"
BRAND_URLhttps://hoppla.devHost für den E-Mail-Footer-Link
ALERT_PREFIXHoppla!Betreff-Präfix auf Alarm-Mails + Chat-/Webhook-Pushes
DASHBOARD_URLhttps://app.hoppla.devDeep-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_AGENTHopplaBot/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 ​

  1. Setze domains.portalRoot in deinen Werten (z. B. portal.example.com).
  2. DNS: Wildcard-Eintrag *.portal.example.com → deine Ingress-IP.
  3. TLS: ein Wildcard-Zertifikat für *.portal.example.com -- mit cert-manager nutzt du einen DNS-01-Issuer; mit provisioner: manual bringst du dein eigenes mit.
  4. Das Portal jedes Resellers ist dann unter <sein-slug>.portal.example.com live.

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 (/migrate auf 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 (→ die upgrade_url des Resellers, mit angehängtem ?tenant=&plan=&feature=&suggested=-Kontext) · contact (→ support_url). checkout wird 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; in active-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ägt usage + 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:

  1. Domain auf der Status Page hinzufügen (Dashboard → Status Page → Custom Domain).
  2. Die Instanz zeigt das exakte zu erstellende CNAME, z. B. status.client.com CNAME cname.status.example.com.
  3. DNS für *.status.example.com (und das CNAME-Ziel) muss auf deine Ingress-IP auflösen.
  4. 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. Mit provisioner: manual gibt 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 deinem hoppla-secrets)
  • Dashboard: TURNSTILE_SITE_KEY via dashboard.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:

yaml
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:

yaml
api:
  env:
    PROBE_LOCATION: "eu-yourcity"
    PROBE_SECONDARY: "eu-yourcity-2"   # selber Cluster, zweiter Versuch

hoppla: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 -- /migrate auf 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 das portal_allow_registration des 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-previews stündlich, migration:prune-credentials täglich).

Kurz-Checkliste zum Onboarding eines Resellers ​

  1. domains.portalRoot gesetzt + Wildcard-DNS/TLS in Stellung (einmal pro Instanz)
  2. Tenant auf den reseller-Plan befördert, Managed-Team-Limit gesetzt
  3. Reseller konfiguriert Branding + (optional) Custom SMTP
  4. Optional: eigene Portal-Domain hinzugefügt + CNAME verifiziert
  5. Optional: portal_allow_registration + Turnstile-Keys für Self-Signup
  6. php artisan hoppla:doctor -- alles grün