Skip to content

White-Label & Resellers ​

Hoppla self-hosted ships the full reseller stack: branded client portals, managed tenants, per-reseller SMTP, white-label status pages, and white-label alert notifications. This page takes you from a vanilla install to running branded portals for your clients.

Branding resolves in three layers — most specific wins:

  1. Tenant branding — a managed tenant's own logo and colors (set in their Settings).
  2. Reseller branding — the portal's brand; managed tenants inherit it automatically when they haven't set their own.
  3. Instance brand — env-level defaults for everything else (system mails, fallback footer, alert prefix). Set these so nothing ever says "Hoppla".

Instance brand (env) ​

Set these on the API/worker pods via api.env in your values file. They are the last-resort fallbacks — per-tenant and per-reseller branding always wins where it is set.

EnvDefaultUsed for
BRAND_NAMEHopplaEmail titles/footers, notification driver fallbacks, "powered by".
BRAND_URLhttps://hoppla.devEmail footer link host.
ALERT_PREFIXHoppla!Subject prefix on alert mails + chat/webhook pushes.
DASHBOARD_URLhttps://app.hoppla.devDeep-links in notification emails — point at your domains.app.
SUPPORT_EMAIL[email protected]"If you did not make this change…" contact in security mails.
CRAWLER_USER_AGENTHopplaBot/1.0 (+https://hoppla.dev/bot)The UA your checks present — visible in monitored sites' logs.

Reseller portals ​

A reseller is a tenant on the reseller plan (your license's limits decide how many managed teams it may carry). Promote a tenant via the super-admin area (/dashboard/admin), or create your first one directly with hoppla:install.

Slug portals — <slug>.your-portal-root ​

  1. Set domains.portalRoot in your values (e.g. portal.example.com).
  2. DNS: a wildcard record *.portal.example.com → your ingress address.
  3. TLS: a wildcard certificate for *.portal.example.com — with cert-manager, use a DNS-01 issuer (see Configuration → TLS); with provisioner: manual, bring your own.
  4. Each reseller's portal is then live at <their-slug>.portal.example.com.

Custom portal domains — monitoring.client-agency.com ​

The reseller adds their domain in their dashboard (Reseller → Portal). What happens next depends on portalDomains.provisioner:

  • ingress (default): the API creates the Ingress + cert-manager Certificate itself. The reseller points a CNAME at your portal CNAME target and verifies — done.
  • manual: the API returns operator instructions (shown in the reseller's UI) and you create the route/TLS yourself.

Portal branding ​

The reseller configures everything under Settings → White-label: brand name, logo, favicon, primary/secondary colors, design tokens (radius/spacing/shadows), dashboard layout, footer, legal links. This brands:

  • the portal login/registration pages,
  • the dashboard chrome their clients see,
  • all notification emails + chat pushes (alert prefix, from-name, footer),
  • their clients' status pages (including "powered by"),
  • the public migration funnel (/migrate on their portal).

Upsell behavior ​

Resellers control whether and how their clients are nudged to upgrade for features not in their plan — and the experience never leaks vendor plan names or checkout. Set an account default (PATCH /api/v1/branding) inherited by all clients, and override per client (PATCH /api/v1/reseller/managed-teams/{id}/branding):

  • upsell_mode: hidden (default — clients never see what they didn't buy) · silent (shown, disabled, no CTA) · subtle (on-brand notice + one CTA) · active (+ nudges).
  • upsell_cta: none · link (→ the reseller's upgrade_url, with ?tenant=&plan=&feature=&suggested= context appended) · contact (→ support_url). checkout is rejected — it is vendor-internal only, so a client is never sent to the instance's billing.
  • plan_labels: maps internal tiers to the reseller's package names, e.g. {"starter":"Growth"}.
  • upsell_offers: a per-feature offer in the reseller's own product taxonomy, e.g. {"server_monitoring":{"label":"Infrastructure","url":"https://portal/buy/infra"}}. Shown when that feature is gated; keys are feature slugs (unknown ignored), URLs SSRF-checked. Per-team offers override the account default.
  • upsell_threshold: for numeric limits (monitors, status pages, channels), the offer fires at the cap; in active posture a soft nudge appears once usage crosses this fraction of the limit (0.5–0.99, default 0.8). The upgrade link carries usage + limit.

Included vs. upsold: what a client has = plan + feature_overrides (what the reseller provisions); what is sold on top = upsell_offers. Offers only surface for currently-gated features, so anything granted via overrides is never offered — they compose with no overlap.

Signed upgrade link. The redirect's identity context is signed with the same secret as the reseller's webhooks (HMAC-SHA256, like the X-Hoppla-Signature header). The link carries ts + sig; verify by recomputing hmac_sha256("tenant={t}&plan={p}&ts={ts}", secret). It is unsigned if the reseller has no webhook secret.

On self-hosted the default posture resolves to hidden (the license is fixed, so the operator is never nagged); a reseller opts in per client. The effective policy a client's dashboard applies is exposed in the upsell block of GET /api/v1/plan.

Per-reseller SMTP ​

Resellers can send from their own domain via Settings → White-label → Custom SMTP (API: GET/PATCH/DELETE /api/v1/smtp-config, POST /api/v1/smtp-config/test). Enter host/port/username/password/from, Send test to verify before saving, and save. Config is validated on save and stored encrypted; only allowlisted submission ports are accepted (25, 465, 587, 2525) and the host is SSRF-checked at send time.

Delivery falls back to the instance mailer if the reseller's SMTP breaks or the plan revokes the feature — so a bad config never silently drops alerts. This is independent of the instance-level MAIL_* and can be set any time.

Custom status-page domains — status.client.com ​

Beyond the portal, each status page can live on the tenant's own domain. Prerequisite: set domains.statusRoot (e.g. status.example.com) so the instance advertises a CNAME target it controls — without it, the page is served on the app domain's /status/<slug> path. Then:

  1. Add the domain on the status page (dashboard → status page → Custom domain).
  2. The instance shows the exact CNAME to create, e.g. status.client.com CNAME cname.status.example.com.
  3. DNS for *.status.example.com (and the CNAME target) must resolve to your ingress address.
  4. Verify in the UI. With customDomains.provisioner: ingress (default) the API then creates the Ingress + cert-manager Certificate itself and TLS issues automatically (HTTP-01). With provisioner: manual it returns operator instructions.

Managed tenants' custom status domains CNAME to the reseller's branded target, so the reseller's clients never see a Hoppla hostname.

Client (managed-tenant) signup ​

Two paths:

  • Reseller-created: the reseller adds teams from their dashboard. Counted against managed team limits (license → reseller plan → per-tenant override).
  • Self-signup on the portal: the reseller enables portal_allow_registration (Reseller → Portal settings). New accounts become managed tenants of that reseller automatically, with the reseller's registration defaults (plan — capped to their max managed plan — trial days, feature overrides).

Mapping to the reseller's customer database. Each managed tenant carries a tenant_id (UUID — the canonical key the plan hangs off), a public_id (HOP-xxxx, human-facing), and reseller-owned external_ref + metadata the reseller sets (POST/PATCH /api/v1/reseller/managed-teams). external_ref is unique per reseller, makes provisioning idempotent (retry-safe), supports reverse lookup (GET ?external_ref=), and — with metadata and public_id — is echoed in every reseller webhook under _managed_tenant, so a partner reconciles billing against their own customer IDs without a separate mapping table.

Registration & captcha ​

Self-signup (and the public migration funnel) is captcha-protected by Cloudflare Turnstile. Configure once per instance:

  • API: TURNSTILE_SECRET_KEY (in your hoppla-secrets).
  • Dashboard: TURNSTILE_SITE_KEY via dashboard.runtimeConfig (next section).

Without keys, captcha verification fails closed when required — set both before enabling any registration surface.

Dashboard runtime config ​

The dashboard image is prebuilt; per-install values are served at runtime via /config.js from the dashboard.runtimeConfig map in your values file:

yaml
dashboard:
  runtimeConfig:
    APP_DOMAIN: app.example.com        # usually derived from domains.app — set only to override
    PORTAL_DOMAIN: portal.example.com  # = domains.portalRoot
    STATUS_DOMAIN: status.example.com  # = domains.statusRoot
    DOCS_URL: ""                       # optional docs link in the UI
    # Live dashboard updates (websockets). MUST equal the REVERB_APP_KEY in
    # your hoppla-secrets Secret (install.sh generated it):
    #   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..."        # public site key (pairs with the API secret)
    SENTRY_DSN: ""                     # optional error reporting, your own DSN

Changes to this map roll the dashboard pod automatically (checksum annotation) — no rebuild needed.

Probe labels ​

Uptime checks record a probe location, and failures are re-verified from the secondary before alerting. On a single-cluster install both run from the same place — set honest labels so nobody mistakes it for true multi-region:

yaml
api:
  env:
    PROBE_LOCATION: "eu-yourcity"
    PROBE_SECONDARY: "eu-yourcity-2"   # same cluster, second attempt

hoppla:doctor warns when both labels are identical. True multi-region probing requires worker pools in a second cluster — see Scaling → Multi-probe locations.

Migration importers ​

The competitor-import tooling (Oh Dear, UptimeRobot — more coming) is included:

  • Authed wizard — every tenant (including managed tenants on reseller portals) has Settings → Import & migration and /dashboard/migrate. Paste a read-only competitor API key, preview, and import monitors + status pages + downtime history.
  • Public pre-signup funnel — /migrate on the main app and on reseller portals lets visitors preview an import before creating an account. It is gated behind registration being enabled (global flag, or the reseller's portal_allow_registration) and requires Turnstile. Submitted competitor keys are stored encrypted and auto-deleted after one hour; hygiene jobs are pre-scheduled (migration:prune-previews hourly, migration:prune-credentials daily).

Onboarding a reseller — checklist ​

  1. domains.portalRoot set + wildcard DNS/TLS in place (once per instance).
  2. Tenant promoted to the reseller plan, managed-team limit set.
  3. Reseller configures branding + (optionally) custom SMTP.
  4. Optional: custom portal domain added + CNAME verified.
  5. Optional: portal_allow_registration + Turnstile keys for self-signup.
  6. php artisan hoppla:doctor — all green.