Skip to content

Branding (White-Label)

ShinyHub ships a white-label mode that lets operators customize the front door without touching the core platform. All branding fields are optional. With no branding: block, / and /login serve the built-in catalog and login page unchanged.

YAML config

Add a branding: block to shinyhub.yaml (see shinyhub.yaml.example for the full commented example):

branding:
  site_title: "Example Shiny Platform"
  assets_dir: /etc/shinyhub/assets
  logo: logo.svg               # filename in assets_dir, or an absolute http(s):// URL
  favicon: favicon.ico         # filename in assets_dir, or an absolute http(s):// URL
  theme:
    primary_color: "#0a7d8c"   # CSS hex (#rgb or #rrggbb); sets --brand-primary
  landing_page: landing.html   # filename in assets_dir; replaces the stock catalog at /
  footer_links:
    - { label: "Support",   url: "mailto:support@example.com" }
    - { label: "Community", url: "https://example.com/community" }

Fields

Field Description
site_title Replaces the <title> tag in the SPA shell. On signed-out brand slots it replaces the ShinyHub wordmark when no logo is set; in signed-in navigation it appears as a subtitle beneath ShinyHub.
assets_dir Directory that backs all local asset references. Required when any field references a local file.
logo Brand logo: a filename inside assets_dir or an absolute http(s):// URL. Replaces the wordmark on the boot splash and login card; signed-in navigation keeps the ShinyHub lockup.
favicon Browser-tab icon: a filename inside assets_dir or an absolute http(s):// URL. Used by the dashboard, login, custom landing fallback, and ShinyHub-owned app status/access pages.
theme.primary_color CSS hex color (#rgb or #rrggbb). Injected as the --brand-primary CSS variable.
landing_page Filename inside assets_dir that replaces the stock app catalog at /. /login always serves the SPA shell.
footer_links List of { label, url } objects. URLs accept http, https, mailto, or an absolute /path.

assets_dir is validated at startup: the directory must exist and every referenced local file must resolve inside it (a symlink-aware containment check).

Browser-tab identity

ShinyHub-owned pages use the configured favicon consistently, including app starting, deploying, stopped, crashed, access-denied, and first-deploy pages. A custom landing_page may declare its own <link rel="icon">; when it does not, browsers fall back to the configured icon through /favicon.ico.

Running apps keep a favicon declared by their own HTML. When an app does not declare one, ShinyHub uses the app icon selected in its configuration (emoji or uploaded image), which keeps several open app tabs distinguishable. An app with no icon falls back to the platform favicon. Running apps also keep a title declared by their own HTML; when no title is present, ShinyHub uses App Name · ShinyHub. ShinyHub-owned lifecycle pages use that title format consistently. Access-denied pages remain generic so private app metadata is not exposed.

If no favicon is set, ShinyHub uses its stock favicon. This fallback remains active when site_title or logo is customized; setting favicon replaces the complete stock icon set.

Brand slots

A brand slot is anywhere ShinyHub shows product or instance identity: the sidebar, mobile top bar, boot splash, and login card. The two signed-out slots use the operator identity; the two signed-in slots remain ShinyHub-first so an external authentication proxy cannot bypass product identification.

On the boot splash and login card, the first configured value wins:

  1. logo - rendered as an image, with site_title (or ShinyHub) as its alt text.
  2. site_title - rendered as the signed-out identity text.
  3. The stock ShinyHub wordmark.

The login card matters most: signed out, it is the only chrome a visitor sees. A logo sized around 40px tall reads well there; wider lockups are clamped to the card width.

In the signed-in sidebar and mobile top bar, the ShinyHub wordmark remains the primary identity. When site_title is set, it appears directly beneath the wordmark as the hub subtitle. The compact desktop rail uses the standalone ShinyHub mark.

What branding does not replace

The About dialog, reached through About ShinyHub in the sidebar footer, names ShinyHub and its version, and branding never replaces it. It is the only place a signed-in operator can read which software and which release they are running, so it has to survive a full white-label: it is what makes a bug report actionable and what tells whoever inherits the server what it is. It also reports which app runtimes the host can start, which is the fastest answer to "why did my R app fail to deploy".

It sits behind the login and behind a click, where the audience is people who run the platform rather than people who visit it. The explicit trigger also keeps the product name discoverable for users who signed in at an external authentication proxy and never saw ShinyHub's login page.

Environment overrides

Each scalar field can be set or overridden via an environment variable. The footer_links list has no env override and must be set in YAML.

Env var Config field
SHINYHUB_BRANDING_SITE_TITLE branding.site_title
SHINYHUB_BRANDING_ASSETS_DIR branding.assets_dir
SHINYHUB_BRANDING_LOGO branding.logo
SHINYHUB_BRANDING_FAVICON branding.favicon
SHINYHUB_BRANDING_PRIMARY_COLOR branding.theme.primary_color
SHINYHUB_BRANDING_LANDING_PAGE branding.landing_page

Asset serving

Local files registered via logo and favicon are served from an explicit allow-list at /branding/<basename>. The asset handler accepts only a bare basename (no subdirectory segments) and looks it up in the map, so path traversal and symlink tricks are blocked at the handler level.

Operator landing pages should reference these assets with the full /branding/ prefix:

<img src="/branding/logo.svg" alt="Logo">

Relative paths in operator HTML resolve against /, not /branding/, so the prefix must be explicit (or add a <base href="/branding/"> element).

Remotely hosted images

When logo or favicon is an absolute http(s):// URL, its origin is added to the control-plane Content-Security-Policy img-src list automatically, so the browser will load it. Only img-src is widened, and only with the scheme, host and port of the images you configured; script, style and connect sources are untouched. Nothing is added for local assets, which are same-origin already.

The origin is allowed, not the exact path, because asset CDNs redirect and a CSP path source is matched against the pre-redirect URL only.

This applies to the SPA shell. An operator landing_page is served under a separate policy that does not widen img-src, so host landing-page images locally via assets_dir.

The landing_page file is served directly at / (replacing the stock catalog) and is NOT exposed under /branding/. It is served as trusted same-origin platform HTML. Only trusted operators should author it; it is not sandboxed.

Endpoints

Endpoint Auth Description
GET /.shinyhub/branding.json None (always public) Returns the active branding object, or {} when branding is not configured.
GET /.shinyhub/apps.json Optional Anonymous: public apps only. Admin/operator: all apps. Other authenticated users: apps visible to them (public, shared, owned, or member). Returns minimal {slug, name, visibility} objects. Identity is resolved from the browser session cookie only; callers presenting only an Authorization header are treated as anonymous.

Some reverse proxies block dot-prefixed paths. Ensure requests to /.shinyhub/ pass through to ShinyHub unmodified.

Environment identity

Mark a sandbox, development, or acceptance instance in its deployment configuration:

branding:
  environment:
    label: Acceptance
    color: "#f5b301"
    message: Test environment. Data may be incomplete or out of date.
    production_url: https://dashboards.example.com

The optional block creates a persistent, non-dismissible strip independent of announcements, prefixes browser titles with [Acceptance], and replaces ordinary tab favicons with the ShinyHub mark on an environment-colored tile. App identity stays in the title. The label is also returned by shinyhub whoami, including its JSON output, and shown as the target in fleet plan/apply output. Older servers and unmarked instances omit the label.

Omitting the block preserves existing presentation; absence is not proof that an instance is production. A supplied block requires a label of 1–32 characters, without control characters; repeated whitespace is normalized to a single space. color accepts a three- or six-digit CSS hex color and defaults to #f5b301. Text automatically uses black or white for contrast. message is optional plain text, at most 200 characters on a single line. No message is added when omitted. production_url is optional and must be an absolute HTTP(S) origin, without credentials, query, fragment, or subpath. Without it, the strip has no production link.

Deployment templates can use SHINYHUB_BRANDING_ENVIRONMENT_LABEL, SHINYHUB_BRANDING_ENVIRONMENT_COLOR, SHINYHUB_BRANDING_ENVIRONMENT_MESSAGE, and SHINYHUB_BRANDING_ENVIRONMENT_PRODUCTION_URL. Non-empty environment overrides require a label. Empty environment variables are ignored. Changes take effect after restart. There is no announcement record, admin token, reconcile task, or polling request.

Production navigation

“Open in production” uses a normal link and does not rely on a referrer:

  • Hosted app routes retain their path, app query parameters, and fragment. Reserved __shinyhub_* authentication/launch parameters are removed without re-encoding the remaining app query values. Query and fragment values must be suitable for use on the production app.
  • App-local /.shinyhub/ endpoints link to that app's root instead.
  • Recognized hub and console routes retain their path, with query and fragment removed.
  • Login, authentication callbacks, API routes, and unknown routes link to production home.

The link updates from the live URL when used, including app bookmark state changed through browser history APIs. It sends no referrer and preserves normal keyboard and middle-click behavior. Production authentication and app access rules still apply. It neither transfers the current session nor checks production's app inventory. A missing app shows a browser 404 with a catalog link rather than silently redirecting to home.

Coverage and limitations

The strip covers the ShinyHub hub and sign-in shell, platform-owned app lifecycle and access pages, and supported top-level hosted app HTML. A support-session safety rail stays above it; announcements remain independently visible beneath it. The strip wraps on narrow screens and stays in print. The environment favicon intentionally overrides configured and app-authored ordinary rel=icon entries when same-origin images are allowed; touch and mask icons are unchanged. Existing bookmarks and history entries are not retroactively renamed.

Hosted app injection retains the existing bounded HTML/CSP rules. Unsupported responses, oversized or force-compressed bodies, embedded iframes, and restrictive policies can omit some or all treatment. When the body can be rewritten but the script is refused, its initial title still receives the prefix. An app that forbids same-origin images keeps its own icons. Open app tabs need a reload after enabling or changing the configuration. App-authored fixed headers at top: 0 may sit beneath the strip; apps can use --shinyhub-environment-height to offset them.

External identity-provider pages and custom operator-authored landing HTML are not rewritten. Landing templates can read the public environment object at /.shinyhub/branding.json and render their own label, using DOM textContent for its plain-text values:

const { environment } = await fetch('/.shinyhub/branding.json').then(r => r.json());
if (environment) {
  const label = document.createElement('p');
  label.textContent = environment.label;
  document.body.prepend(label);
}

An environment label communicates identity; it does not restrict access or certify data freshness.