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
| Layer | Source | Scope |
|---|---|---|
| Static base | Flo.FE/public/configs/flo.configs.json + flo.theme.json | Title, meta tags, fonts, PWA defaults, 40+ CSS variables |
| Cached overlay | localStorage key flo.branding.overlay | Last visit's DB overlay, replayed before the network answers |
| DB overlay | BrandingConfiguration row served by GET /api/public/v1/branding | Only 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:
| Column | Content |
|---|---|
ThemeJson | flo.theme.json-shaped blob (theme, typography) |
BrandMetaJson | flo.configs.json-shaped blob (title, meta, fonts, pwa) plus brandKit |
LoginBrandingEnabled | Login branding toggle |
CustomLogoPath / CustomBackgroundPath / CustomBackgroundColor / CustomBackgroundType | Login assets (color, image, video, gif) |
RequirePhoneNumber / AdminOnlyLogin | Registration 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 byConfigEditorService(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 (nullotherwise) and strips thebrandKitsection — 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
brandKitand stores only the top-level sections that differ from the bundled base (sparse storage); the PWA save allowlistspwa(name,shortName,themeColor,backgroundColor,icons,iconPadding) andmeta(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.
| Endpoint | Who |
|---|---|
GET /api/v1/admin/branding | Admin/SuperAdmin, or Pro with organization.appExperience |
PUT /api/v1/admin/branding/theme, PUT /api/v1/admin/branding/brand-meta | SuperAdmin |
PUT /api/v1/admin/branding/pwa, PUT /api/v1/admin/branding/login | Admin |
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-assets | SuperAdmin / Admin |
GET /api/public/v1/branding | Anonymous (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):
| Kind | Types | Max size |
|---|---|---|
logo | PNG, JPEG, WebP, SVG, GIF | 2 MB |
background | images + MP4/WebM | 20 MB |
icon, splash | PNG, WebP | 1 MB / 6 MB |
font | TTF, OTF, WOFF, WOFF2 | 10 MB |
brand | images | 10 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:
| Surface | Route | Notes |
|---|---|---|
| Brand kit | /dashboard/amministrazione/identita-brand/kit | Colors, typography, logo/font slots, asset library, downloads |
| Brand assets | /dashboard/amministrazione/identita-brand/assets | Asset library with variants, collections and archiving |
| Theme | /dashboard/amministrazione/identita-brand/tema | SuperAdmin only; Monaco editor over ThemeJson |
| Brand meta | /dashboard/amministrazione/identita-brand/brand-meta | SuperAdmin only; Monaco editor over BrandMetaJson |
| Login | /dashboard/amministrazione/esperienza-app/login | Logo, background (color/image/video/gif), registration rules |
| App installation | /dashboard/amministrazione/esperienza-app/installazione-app | PWA 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):
| Output | Size |
|---|---|
| Manifest icons | 192, 256, 384, 512 |
apple-touch-icon | 180 |
| Favicon | 32 |
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.tson 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 whenenable_whitelabelis off or nothing has been customized. - The former Respirastudio model is gone:
FORCE_RESPIRASTUDIO_UI, hostname-based brand detection andrespirastudio.*.jsonfiles were removed.public/configs/now contains onlyflo.configs.jsonandflo.theme.json.
Key Files
| File | Purpose |
|---|---|
Flo.BE/Services/BrandingConfigService.cs | Effective/public config resolution and singleton upsert |
Flo.BE/Services/BrandingAssetService.cs | Whitelabel asset validation and storage |
Flo.BE/Services/LoginBrandingAssetService.cs | Atomic login asset replacement |
Flo.BE/Services/PwaManifestService.cs | Tenant-aware manifest build |
Flo.BE/Services/BrandKitService.cs | Brand kit, asset library and downloads |
Flo.BE/Controllers/BrandingController.cs | Admin branding API |
Flo.BE/Controllers/BrandKitController.cs | Brand kit API |
Flo.BE/Controllers/PublicApi/PublicBrandingController.cs | Anonymous branding bootstrap |
Flo.FE/src/app/app.component.ts | Base + 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.ts | Per-tenant manifest on the frontend host |