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:
- Tenant branding — a managed tenant's own logo and colors (set in their Settings).
- Reseller branding — the portal's brand; managed tenants inherit it automatically when they haven't set their own.
- 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.
| Env | Default | Used for |
|---|---|---|
BRAND_NAME | Hoppla | Email titles/footers, notification driver fallbacks, "powered by". |
BRAND_URL | https://hoppla.dev | Email footer link host. |
ALERT_PREFIX | Hoppla! | Subject prefix on alert mails + chat/webhook pushes. |
DASHBOARD_URL | https://app.hoppla.dev | Deep-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_AGENT | HopplaBot/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
- Set
domains.portalRootin your values (e.g.portal.example.com). - DNS: a wildcard record
*.portal.example.com→ your ingress address. - TLS: a wildcard certificate for
*.portal.example.com— with cert-manager, use a DNS-01 issuer (see Configuration → TLS); withprovisioner: manual, bring your own. - 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 (
/migrateon 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'supgrade_url, with?tenant=&plan=&feature=&suggested=context appended) ·contact(→support_url).checkoutis 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; inactiveposture a soft nudge appears once usage crosses this fraction of the limit (0.5–0.99, default 0.8). The upgrade link carriesusage+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:
- Add the domain on the status page (dashboard → status page → Custom domain).
- The instance shows the exact CNAME to create, e.g.
status.client.com CNAME cname.status.example.com. - DNS for
*.status.example.com(and the CNAME target) must resolve to your ingress address. - 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). Withprovisioner: manualit 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 teamlimits (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 yourhoppla-secrets). - Dashboard:
TURNSTILE_SITE_KEYviadashboard.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:
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 DSNChanges 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:
api:
env:
PROBE_LOCATION: "eu-yourcity"
PROBE_SECONDARY: "eu-yourcity-2" # same cluster, second attempthoppla: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 —
/migrateon 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'sportal_allow_registration) and requires Turnstile. Submitted competitor keys are stored encrypted and auto-deleted after one hour; hygiene jobs are pre-scheduled (migration:prune-previewshourly,migration:prune-credentialsdaily).
Onboarding a reseller — checklist
domains.portalRootset + wildcard DNS/TLS in place (once per instance).- Tenant promoted to the
resellerplan, managed-team limit set. - Reseller configures branding + (optionally) custom SMTP.
- Optional: custom portal domain added + CNAME verified.
- Optional:
portal_allow_registration+ Turnstile keys for self-signup. php artisan hoppla:doctor— all green.