Skip to main content

Multi-Brand Theming

Flo ships a single UI brand (flo) and customizes it per tenant at runtime. Branding is not a build-time fork: a bundled static base is always applied first, and an optional database-backed overlay — the whitelabel — replaces only the sections an operator actually customized.

Layers​

LayerSourceScope
Static baseFlo.FE/public/configs/flo.configs.json + flo.theme.jsonTitle, meta tags, fonts, PWA defaults, 40+ CSS variables
Cached overlaylocalStorage key flo.branding.overlayLast visit's DB overlay, replayed before the network answers
DB overlayBrandingConfiguration row served by GET /api/public/v1/brandingOnly the sections the operator customized

At startup AppComponent.loadCustomerConfigs() applies the static base first (first paint never blocks on the API), replays the cached overlay, then fetches the public branding endpoint and merges the non-null sections over the base. Theme variables are injected into :root and referenced by Tailwind — see Design Tokens & Typography.

Backend​

Storage​

Flo.BE/Models/BrandingConfiguration.cs is a per-tenant singleton row (Id = 1), created on first save (no migration seeding). Nullable columns are the "not configured yet" signal, evaluated per section:

ColumnContent
ThemeJsonflo.theme.json-shaped blob (theme, typography)
BrandMetaJsonflo.configs.json-shaped blob (title, meta, fonts, pwa) plus brandKit
LoginBrandingEnabledLogin branding toggle
CustomLogoPath / CustomBackgroundPath / CustomBackgroundColor / CustomBackgroundTypeLogin assets (color, image, video, gif)
RequirePhoneNumber / AdminOnlyLoginRegistration rules shown on the login and registration screens

Resolution (BrandingConfigService)​

BrandingConfigService is the only place with branding logic:

  • Admin read (GetEffectiveAsync): the DB row merged over the bundled files, top level only. Bundled file keys are resolved by ConfigEditorService (theme → flo.theme.json, brand-config → flo.configs.json). Login and registration settings fall back to {env}.config.json.
  • Public read (GetPublicAsync): returns only the sections the operator customized (null otherwise) and strips the brandKit section — bundled defaults are never served to a tenant's visitors.
  • Writes: upsert the singleton row (created on first save). Brand-meta, PWA, login and asset-reference writes take a table-lock transaction; a brand-meta save keeps brandKit and stores only the top-level sections that differ from the bundled base (sparse storage); the PWA save allowlists pwa (name, shortName, themeColor, backgroundColor, icons, iconPadding) and meta (appleIcon, favicon, appleStartupImage).

API​

BrandingController (/api/v1/admin/branding) and BrandKitController (/api/v1/brand-kit) are gated by [RequireWhitelabelEnabled]: when the enable_whitelabel flag is off, the whole surface answers 404, as if the feature were never deployed. The flag is listed in Feature Flags.

EndpointWho
GET /api/v1/admin/brandingAdmin/SuperAdmin, or Pro with organization.appExperience
PUT /api/v1/admin/branding/theme, PUT /api/v1/admin/branding/brand-metaSuperAdmin
PUT /api/v1/admin/branding/pwa, PUT /api/v1/admin/branding/loginAdmin
POST /api/v1/admin/branding/assets?kind=...icon/splash: Admin; other kinds: SuperAdmin
POST /api/v1/admin/branding/login-assets, DELETE /api/v1/admin/branding/login-assets/{kind}Admin
DELETE /api/v1/admin/branding/assets / DELETE /api/v1/admin/branding/pwa-assetsSuperAdmin / Admin
GET /api/public/v1/brandingAnonymous (no-store)

Assets​

BrandingAssetService validates uploads by kind (magic-byte sniff, not just content type) and stores them under the whitelabeling/ folder of the configured storage provider (R2 or local):

KindTypesMax size
logoPNG, JPEG, WebP, SVG, GIF2 MB
backgroundimages + MP4/WebM20 MB
icon, splashPNG, WebP1 MB / 6 MB
fontTTF, OTF, WOFF, WOFF210 MB
brandimages10 MB

Deletes are scoped to the whitelabeling/ folder. LoginBrandingAssetService replaces login assets atomically: the new object is uploaded, the reference is saved, then the superseded object is deleted best-effort.

Admin Surfaces​

The admin UI lives under Brand e identità (described in the client-facing manual) and App Experience:

SurfaceRouteNotes
Brand kit/dashboard/amministrazione/identita-brand/kitColors, typography, logo/font slots, asset library, downloads
Brand assets/dashboard/amministrazione/identita-brand/assetsAsset library with variants, collections and archiving
Theme/dashboard/amministrazione/identita-brand/temaSuperAdmin only; Monaco editor over ThemeJson
Brand meta/dashboard/amministrazione/identita-brand/brand-metaSuperAdmin only; Monaco editor over BrandMetaJson
Login/dashboard/amministrazione/esperienza-app/loginLogo, background (color/image/video/gif), registration rules
App installation/dashboard/amministrazione/esperienza-app/installazione-appPWA identity and derived icons/splash

Routes are gated by FeatureFlagGuard with ff: 'enable_whitelabel' plus AdminOrProGuard / AccessPolicyGuard (organization.brandKit, organization.appExperience); the theme and brand-meta tabs add IsSuperAdminGuard.

The brand kit is an authoring/export surface: its brandKit section (colors, typography, asset slots, library) is stored in BrandMetaJson but stripped from public responses, so it does not reach the live UI.

Login Branding​

Login and registration screens read the login branding through the anonymous bootstrap GET /api/v1/admin/configs (LoginBrandingSettings). The same endpoint returns the registration rules (RequirePhoneNumber, AdminOnlyLogin). Supported backgrounds are a solid color, an image, a video, or a GIF; logo and background are rendered through the brandingAsset pipe.

PWA Branding​

Icon derivation​

The backend has no image library, so the whitelabel editor derives the whole PWA icon set on a canvas from a single square master (minimum 512×512):

OutputSize
Manifest icons192, 256, 384, 512
apple-touch-icon180
Favicon32
iOS splash (apple-touch-startup-image)2048×2732

Seven renders (six icons + one splash) are uploaded via POST /api/v1/admin/branding/assets?kind=icon|splash and patched into the brand-meta blob (pwa.icons, pwa.iconPadding, meta.appleIcon, meta.favicon, meta.appleStartupImage) with PUT /api/v1/admin/branding/pwa. The manifest purpose is declared any maskable only when the safe-zone padding is at least 10% (maximum 25%); the favicon stays edge-to-edge. Superseded objects are deleted best-effort once the new blob is saved.

Manifest serving​

GET /manifest.webmanifest is served per tenant by both:

  • the backend (PwaManifestController, anonymous, no-store), used by the bundled frontend and the local dev proxy;
  • the Cloudflare Pages Function Flo.FE/functions/manifest.webmanifest.ts on the frontend host.

Both start from the bundled manifest-flo.json and overlay name, short_name, theme_color, background_color and the sanitized icons[] (max 12 entries, NxN sizes, HTTPS URLs; loopback URLs are rewritten to the configured API origin). A failed branding read serves the untouched base. The frontend always links /manifest.webmanifest.

Legacy Note​

  • The file-based theme (public/configs/flo.theme.json) remains the base layer and the fallback when enable_whitelabel is off or nothing has been customized.
  • The former Respirastudio model is gone: FORCE_RESPIRASTUDIO_UI, hostname-based brand detection and respirastudio.*.json files were removed. public/configs/ now contains only flo.configs.json and flo.theme.json.

Key Files​

FilePurpose
Flo.BE/Services/BrandingConfigService.csEffective/public config resolution and singleton upsert
Flo.BE/Services/BrandingAssetService.csWhitelabel asset validation and storage
Flo.BE/Services/LoginBrandingAssetService.csAtomic login asset replacement
Flo.BE/Services/PwaManifestService.csTenant-aware manifest build
Flo.BE/Services/BrandKitService.csBrand kit, asset library and downloads
Flo.BE/Controllers/BrandingController.csAdmin branding API
Flo.BE/Controllers/BrandKitController.csBrand kit API
Flo.BE/Controllers/PublicApi/PublicBrandingController.csAnonymous branding bootstrap
Flo.FE/src/app/app.component.tsBase + overlay application, CSS variable injection
Flo.FE/src/app/features/dashboard/amministrazione/brand-identity/**Brand e identità tabs
Flo.FE/src/app/features/dashboard/amministrazione/whitelabel-editor/**Theme/brand-meta/login/PWA editor
Flo.FE/functions/manifest.webmanifest.tsPer-tenant manifest on the frontend host