Table of Contents

Tenant visual identity

The tenant visual identity lets a platform administrator override the look of a cluster for one tenant, at runtime, without rebuilding or redeploying anything. The administrator defines a style in Tenant Management, made of a theme built in the theme editor, of a CSS written by hand, or of both, and gives it to a tenant. The users of that tenant see it applied on top of the cluster theme. A style is not tied to a tenant: it is created once and used by as many tenants as needed, as their default style, which applies to all their clusters and all their versions, or for a cluster version (see Use a style for a tenant).

Overview

A style is a named group of:

  • A light slot, which is required. It holds a light theme, a light CSS, or both.
  • A dark slot, which is optional. It holds a dark theme, a dark CSS, or both. When it is empty, the content of the light slot is also used when a dark theme is active (see Light and dark slots).
  • Images, which belong to its CSS. The CSS references them with a relative path such as url(assets/logo.png) (see Images).

A tenant uses a style in two ways, which can be combined:

  • As its default style, chosen on the tenant. It applies to every cluster the tenant is assigned to and to every version of these clusters.
  • By an assignment of a style to a cluster version, which gives the tenant another style from that version of the cluster.

A theme is a named, reusable set of color and component attribute overrides on a standard Neos theme, edited without writing any CSS. See Themes. A CSS is a named, reusable text of CSS that the administrator types or loads from a file once and chooses in any style. See CSS. In this article, the CSS of a slot is this text, as opposed to the CSS that Tenant Management generates from a theme.

The feature is managed in Tenant Management. A cluster does not need any extra configuration to receive the style of its tenants.

Note

The tenant visual identity only applies to clusters running in multitenant mode, and only to users who are signed in to a tenant. See Multitenant.

Note

The screens of Tenant Management call a style a visual identity (identité visuelle in French). This article keeps the word style.

Permission

Only the platform administrators who hold the Tenant visual identity function (Identité visuelle du tenant in French) can manage the styles, the themes and the CSS of the tenants. The function is a standard CRUD function. Without it, the Default visual identity group of the tenant and the Visual identity column of its clusters are not shown.

Tenant Management grants it automatically to every role whose type is Tenant manager administrator, after each tenant database migration of Tenant Management. An existing role of another type does not receive it: grant it from the role permissions of Tenant Management if that role must manage styles.

Create a style

Integrator steps:

  1. Open Tenant Management and choose Visual identity (Identité visuelle in French) in the menu. The screen has three tabs: Visual identity (Identité visuelle), which lists the styles, Theme (Thème) and CSS. In the Visual identity tab, the list shows the name, the themes and the CSS of each style.

  2. Click Add. Type a Name, which is required and unique. It has at most 100 characters and no leading or trailing space.

  3. In the Light tab, choose a Light theme (see Themes), a Light CSS (see CSS), or both. The light slot needs a light theme, a light CSS, or both. The lists only propose the themes and the CSS of the Light color scheme.

  4. Optionally, do the same in the Dark tab, with a Dark theme and a Dark CSS. Leave both empty only for a style that is meant for tenants that do not use the dark mode: the light slot is then reused in dark themes (see Light and dark slots).

  5. A style has no images of its own: its images are the ones of its light CSS and of its dark CSS. To add an image, open the CSS in the CSS tab and use its Images tab (see Images).

  6. Reference each image from the text of the CSS with the path that its Images tab shows, for example:

    :root {
      --primary-500: #b3002d;
    }
    
    .brand-logo {
      background-image: url(assets/logo.png);
    }
    
  7. Save the style, then give it to a tenant (see Use a style for a tenant) and check the result: reload the application as a user of that tenant, in a cluster whose version is covered by the style (see Which style applies). The tenant stylesheet is loaded after the cluster theme, so a rule of the style wins over a cluster theme rule of the same specificity.

Changing a style changes it for every tenant that uses it. To remove a style, open it from the list and use the delete button of its toolbar. Deleting a style keeps its CSS and their images. A style that a tenant uses, as its default style or by an assignment, cannot be deleted: the database refuses it, with a message that says so. Give those tenants another style first.

Tip

The theme values are CSS custom properties on the root element, such as --primary-500. Overriding those properties is usually simpler than overriding the rules of each component, and a theme does it without any CSS. See How to create your own theme for the theme concepts.

Light and dark slots

Each slot of a style, light or dark, can hold a theme, a CSS, or both. Tenant Management publishes one stylesheet per slot, built in this order:

  1. The CSS generated from the theme of the slot, when the slot has a theme.
  2. A line break.
  3. The text of the CSS of the slot, when the slot has one.

The browser still loads one file per color scheme. The CSS comes last, so it wins over the theme for the same variable or the same rule.

A dark slot that has a dark theme but no dark CSS takes the CSS of the light slot as its own, so that the rules of the light CSS still apply in the dark mode, after the dark theme. A dark slot that has a dark CSS keeps it alone, and the CSS of the light slot is not added to it.

The slots follow these rules:

  • The light slot is required. It needs a light theme or a light CSS, and either one alone is enough. The light slot is the one that users see when the dark mode is not chosen, so a style that has a dark theme or a dark CSS also needs a light one.
  • The dark slot is optional.
  • A theme or a CSS only fits the slot of its color scheme: the Light color scheme in the light slot, the Dark color scheme in the dark slot. The fields of the style screen only offer the matching themes and CSS, and a save is refused otherwise.
Warning

When the dark slot is empty, which means no dark theme and no dark CSS, the whole content of the light slot, its theme and its CSS, is also used in the dark mode. The light colors then override the colors of the dark theme of the cluster. Give a dark theme or a dark CSS to every tenant whose users can switch to the dark mode.

Images

The images belong to a CSS, not to a style. A style serves the images of its light CSS and of its dark CSS. To add an image:

  1. Open the CSS in the CSS tab. Its Images tab appears once the CSS is saved.
  2. Click Upload an image. The name of an image is the name of the selected file, with spaces replaced by underscores. The Path in the CSS column shows how to reference it, and the copy button of a row copies that path to the clipboard.
  3. Save the CSS.

An image is referenced with url(assets/<name>). The path is relative to the URL of the stylesheet, and the framework serves it from the same origin as the application, so no absolute URL is needed. The images of a style are the ones of the CSS of the style that the stylesheet comes from.

The two CSS of a style are served under the same path, so they cannot have an image with the same name. A style is refused when its light CSS and its dark CSS have an image of the same name, and an image is refused when the other CSS of a style that uses its CSS already has an image of that name. Deleting a CSS deletes its images. Saving the images of a CSS republishes every tenant and cluster that uses a style that has the CSS.

Only images are accepted. A font file cannot be uploaded, so a tenant CSS cannot bring its own web fonts.

Themes

A theme is a named set of overrides on top of a base theme, which is a theme of a cluster version or one of the standard Neos themes. It lets an administrator restyle a cluster for a tenant without writing any CSS. A theme is global: it is not tied to a tenant, so any style can use it, and several styles can share the same theme.

A theme holds:

  • A Name, which is required and unique. It has at most 100 characters and no leading or trailing space.
  • An optional Description of at most 500 characters.
  • A Color scheme, Light or Dark. It decides in which slot of a style the theme can be used, and which base themes can be chosen.
  • A Base cluster version, which is optional. When it is chosen, the base themes proposed are the themes that this cluster version publishes. When it is empty, they are the standard themes.
  • A Base theme, which provides the values that the theme does not override. It must have the same color scheme as the theme.
  • The overrides, which are only the values that differ from the base theme. A value equal to the value of the base theme is not stored.
Color scheme Standard themes
Light NeosV1, NeosV2, NeosV3
Dark NeosV3Dark

The standard themes are the ones that Neos Studio offers. See How to create your own theme for the theme concepts.

Base themes

A cluster publishes the themes that its modules define in the state store, under the key {ClusterName}_{ClusterVersion}_Themes, each time it starts. A published theme holds its name, its color scheme, its standard theme and only the colors and the component attributes that it defines itself. Tenant Management completes it with the values of its standard theme, which gives the same values as the CSS that the cluster builds for this theme.

The cluster versions that can be chosen are the ones that Tenant Management knows (the Clusters screen). A cluster version that has not started since this feature exists, or that never started, publishes no theme: only the standard themes are then proposed.

The themes of a cluster version are listed first, then the standard themes that the cluster version does not redefine. A cluster version that uses a standard theme directly has no theme of its own, so its base theme is a standard theme.

Create a theme

A theme is created in two steps, because the colors and the component attributes that the screen shows come from the base theme.

  1. Open Tenant Management, choose Visual identity in the menu, then the Theme tab. The list shows the name, the color scheme, the base theme, the base cluster and the number of overrides of each theme.
  2. Click Add. Type a Name, choose the Color scheme, optionally choose a Base cluster version, choose the Base theme and optionally type a description. The base themes offered are the ones of the chosen color scheme, and one of them is proposed by default: the first theme of the cluster version when there is one, otherwise the most recent standard theme.
  3. Save the theme. The Colors and Components tabs, hidden until then, now appear next to the General tab. A theme that is already saved opens on its Colors tab.
  4. Edit the colors and the components, then save again.

The color scheme is read-only once the theme is saved, in the screen and in the API: the styles that use the theme have a slot for its color scheme, and they check it only when they are saved. To change it, create another theme.

The base cluster version and the base theme can still be changed after the first save. Save the theme to refresh the values of the base theme that the Colors and Components tabs show. The overrides are kept, and the save is refused when a component attribute names a color that the new base theme does not have.

The General tab also shows, read-only, the CSS that was generated by the last save, in a Generated CSS group that is closed when the screen opens.

To remove a theme, open it from the list and use the delete button of its toolbar. A theme that a style uses cannot be deleted (see What happens when a theme changes).

Edit the colors

The Colors tab lists the colors of the base theme. Each row shows the standard value with a swatch, and a color input that holds the current value.

  • To override a color, change the value of its color input. An asterisk marks an overridden color.
  • To go back to the standard value, click the reset button of the row. Clearing a value has the same effect.
  • To add a color that the base theme does not define, click Add a custom color, then type its name and choose its value. A custom color has no standard value. It is used by name, in the Components tab. The remove button of a custom color deletes it.

Edit the components

The Components tab edits the attributes of the components. The standard catalog holds 757 attributes in total, so the tab shows one component at a time: choose it in the Component filter, Button by default. The attributes of the chosen component are grouped by category. Each row shows the name of the attribute, its type, the standard value, and an editor for the value:

  • A Color attribute takes the name of a color of the theme, picked in a list. The list holds the colors of the base theme and the custom colors of the theme.
  • A Layout attribute takes a CSS value, such as a size, typed in a text box.

An asterisk marks an overridden attribute, and the reset button of a row puts back the standard value.

The preview, on the right of the list, follows the component that is chosen in the filter: it only shows the samples of this component. Every component of the catalog has a sample. A few components share one, for example the frames container, the frame tab and the nested frame tab are shown together, and the components that style the whole page or an overlay, such as the application or the shadows, are shown on the sample that displays them.

Values that are accepted

The values are checked when the theme is saved, because they are written as they are in a generated CSS block. The rules are strict: a value that is not recognized is refused, and the message names the color or the attribute concerned.

Value Accepted
Color value A hexadecimal color with 3, 4, 6 or 8 digits, rgb(), rgba(), hsl() or hsla() with plain numbers and no nested function, var(--name), transparent, inherit or a CSS named color such as red
Layout value Letters, digits, spaces and the characters . , % # _ ( ) + * / -, with balanced parentheses
Functions in a layout Only calc, min, max, clamp, var, rgb, rgba, hsl and hsla. var() takes a variable name only, without a fallback value. A # value must be a valid hexadecimal color
Name of a custom color Starts with a letter, then letters, digits, dashes and underscores, 100 characters at most

Whatever its type, a value is refused when it:

  • Is longer than 200 characters.
  • Contains a line break or another control character.
  • Contains one of ;, {, }, <, >, \, url(, @import, expression(, javascript:, /* or */.

An empty value is not an override: the standard value applies. A color name cannot be defined twice in a theme, and the color that an attribute names must exist in the base theme or among the custom colors.

What happens when a theme changes

When a theme is saved, Tenant Management generates its CSS and stores it in the theme. The CSS is one :root block of CSS variables, written in two layers: the values of the base theme that are not overridden, then the overrides, which therefore win. Here is a shortened example:

:root {
  --background: #ffffff;
  --neutral-100: #f3f4f6;
  --avatar-size: 2rem;
  --datagrid-background: var(--background);
  --primary-500: #b3002d;
}

The generated CSS is self-contained: it declares every color and every component attribute of the base theme, not only the overrides. It therefore replaces the values of the theme of the cluster, whatever that theme is.

The following rules apply to a theme that styles use:

  • Republication. Saving a theme republishes every tenant and cluster that uses a style that has the theme, in the light slot or in the dark slot. The users see the change as described in When a change becomes visible.
  • Size. The CSS that is published for a slot, the theme CSS plus the CSS, cannot exceed the limit of Limits and validation. A save of a theme that would make a style exceed it is refused, and the message names a style concerned.
  • Deletion. A theme that is used by at least one style cannot be deleted: the database refuses it, with a message saying that the theme is used by at least one tenant style. Choose another theme in those styles, or remove them, then delete the theme.

When a new version of Neos is deployed

A new version of Neos can change how the CSS of a theme is generated, for example the names of the CSS variables or the attributes of a component. The theme stores the version of Neos that generated its CSS, next to the CSS. When Tenant Management starts, it generates again the CSS of every theme whose CSS was generated by another version, from the colors and the attributes stored in the theme, with the base theme and the generator of the running version. Each theme is saved on its own, which republishes the styles that use it, then the published styles are rebuilt, which also publishes a theme whose own republication failed. The replicas generate the CSS one at a time: a replica that starts while another one is generating it waits until it gets the lock of the regeneration, then generates again the themes that are still outdated. During a rolling update, a theme saved by a replica of the previous version is therefore generated again by the new one. Like the rebuild, this regeneration is skipped when the configuration DistributedStores:RebuildOnStart is false.

The RebuildTenantStyles server method generates again the CSS of every theme, whatever its version, before it rebuilds the published styles (see Rebuild the published styles). If another replica is already generating the CSS of the themes, the call fails with a message asking to retry in a few moments.

A theme whose values are no longer accepted by the new version, for example because its base theme no longer exists, keeps its previous CSS and its version, and the other themes are still generated. The same applies when the new CSS cannot be saved, for example because a style that uses the theme would exceed the size limit, and when an unexpected error occurs on a theme. A theme based on a cluster version also keeps its CSS when the themes of that version are not in the state store, for example after a loss of Redis before the cluster has started again: without them, the CSS could be built on the standard theme of the same name instead of the theme that the cluster redefines. It is generated again once the cluster has published its themes, at the next start of Tenant Management or by the RebuildTenantStyles method. A cluster version older than the publication of its themes never publishes them, so its themes keep their CSS. Tenant Management logs a warning that names the theme and gives the reason, such as The CSS of the tenant theme Brand (12) was not generated again because its values are not accepted by the version 3.3.0 of Neos: .... The theme is tried again at each start of Tenant Management. Open it, fix its values, and save it.

Preview

The Components tab of the Theme screen shows a Preview group on the right of the attributes, in a splitter that the user can resize. The General and Colors tabs have no preview. It is the theme preview of the shared NeosThemes module, the same page that Neos Studio uses for its own themes, embedded in the screen. It displays representative components with the CSS of the theme as it is on screen, and the page is asked for it through the tenantThemeDraft CSS source, whose key is the identifier of a draft that the screen chose when it opened. The generated CSS is applied only inside the preview, not to the administration screen. A component that is added to the catalog gets its sample in the module, and then both Neos Studio and Tenant Management show it. A unit test of the module fails when a component of the catalog has no sample. The labels of the samples are resources of the module, in English and in French.

  • The preview does not need the theme to be saved. After each change of a color, of an attribute or of the base theme, and after a short pause, the screen sends the theme as it is on screen to the SaveThemeDraft server method. The server builds the CSS with the same rules as a save and keeps it in the state store for one hour, under the identifier of the draft, and the preview then loads again. A value that is not accepted does not change the preview, and the error is reported when the theme is saved. The Components tab, and so the preview, only exists for a theme that is saved.
  • It shows the right colors, attributes and color scheme, but it uses a system font instead of the font of the cluster.
  • If the user switches the theme of the application while the preview is open, the preview takes the theme of the application again until it is reloaded.

What a theme cannot change

A theme only holds colors and component attributes, because it is written as CSS variables. It cannot change:

  • The font.
  • The color scheme of the theme of the cluster. A tenant theme only applies in the mode of its color scheme: a dark tenant theme applies only when the active theme of the cluster is dark.
  • Animated buttons, the order of the buttons and the breakpoints. They are options of the theme of the cluster that are not written as CSS variables.

To go further than the colors and the component attributes, create a CSS and choose it in the style.

CSS

A CSS is a named text of CSS, written for the light or for the dark mode. Like a theme, it is global: it is not tied to a tenant, so any style can use it, and several styles can share the same CSS.

Warning

It is not guaranteed that the CSS overrides that work in a version of Neos still work in a future version of Neos. A CSS overrides the rules of the components, and their class names and their structure can change without notice, even in a minor version. Check the result of every CSS after each upgrade of Neos. This does not concern the themes, which only change the values of the theme variables.

A CSS holds:

  • A Name, which is required and unique. It has at most 100 characters and no leading or trailing space.
  • An optional Description of at most 500 characters.
  • A Color scheme, Light or Dark. It decides in which slot of a style the CSS can be used. It is read-only once the CSS is saved, in the screen and in the API.
  • A Base cluster version, which is optional, and a Base theme, with the same meaning as for a theme (see Base themes). The base theme is the theme that the CSS is compared with, it does not change what is published.
  • The CSS itself, which is required.

Create a CSS

  1. Open Tenant Management, choose Visual identity in the menu, then the CSS tab. The list shows the name, the color scheme, the base theme and the base cluster of each CSS.
  2. Click Add. Type a Name, choose the Color scheme, optionally choose a Base cluster version, choose the Base theme and optionally type a description.
  3. Type the CSS in the editor, or click Load a .css file (Charger un fichier .css in French) to load it from a file. The file is read in the browser and is not sent to the server: its text fills the editor, and the CSS is saved with the rest of the form.
  4. To see what the CSS changes, click Changes above the editor. The screen lists the custom properties (--primary-500 and so on) that the CSS sets to another value than its base theme, with the value of the base theme next to the value of the CSS, the ones that the base theme does not have, and the ones that the CSS sets on another element than the root. It also lists the selectors of the other rules that the CSS adds. The comparison uses the CSS that is on screen, saved or not, and the base theme that is chosen. Click Edit to come back to the editor. Only the custom properties are compared one by one, because a CSS of a tenant adds rules and overrides some, so it cannot be compared line by line with the CSS of its theme.
  5. Save the CSS. Its Images tab then appears, where the images that the CSS references are added (see Images).
  6. Choose the CSS in the Light CSS or in the Dark CSS field of a style.

To remove a CSS, open it from the list and use the delete button of its toolbar.

What happens when a CSS changes

  • Republication. Saving a CSS republishes every tenant and cluster that uses a style that has the CSS, in the light slot or in the dark slot. The users see the change as described in When a change becomes visible.
  • Size. The CSS that is published for a slot, the theme CSS plus the CSS, cannot exceed the limit of Limits and validation. A save of a CSS that would make a style exceed it is refused, and the message names a style concerned.
  • Deletion. A CSS that is used by at least one style cannot be deleted: the database refuses it, with a message saying that the CSS is used. Choose another CSS in those styles, or remove them, then delete the CSS.

Use a style for a tenant

A style does nothing until a tenant uses it. A tenant uses a style as its default style, which gives it its look once for all its clusters, and by assignments to cluster versions, which give it another look from one version of a cluster.

Default style

The default style of a tenant applies to every cluster the tenant is assigned to, whatever the version of the cluster. It is the same kind of style as any other: it has a light slot, an optional dark slot and the images of its CSS.

Integrator steps:

  1. Open the tenant in Tenant Management, to create it or to edit it. The General tab has a Default visual identity group (Identité visuelle par défaut in French).
  2. Choose the Default visual identity of the tenant. Leave the field empty for a tenant that keeps the standard look.
  3. Save the tenant. Tenant Management republishes the styles of every cluster the tenant is assigned to.

Style for a cluster version

To give a tenant another style from one cluster version, open the Clusters tab of the tenant. The Visual identity column shows, for each assignment of the tenant to a database, the style that is assigned to the cluster version of its database. Choose the style directly in the cell, with its lookup: the choice applies at once, for the tenant and the cluster version of the row, without any popup and without waiting for the save of the tenant. The cell of a row that is not saved yet is read-only, because the row has no cluster version to assign the style to.

The cell is empty when no style is assigned to the cluster version. The default style of the tenant then applies, or the style that is assigned to an earlier version (see Which style applies).

A style that is assigned to a cluster version is also used by the next versions of the cluster, until a higher version has its own assignment. Clear the cell of a cluster version that has a style to give it no style: the default style of the tenant then applies from this cluster version, instead of the style of an earlier version. An assignment without style is kept, because it is what stops the style of an earlier version. Clearing the cell of a cluster version that has no assignment changes nothing.

The same change can be made through the SetTenantStyle server method of Tenant Management, which takes the tenant, the cluster version and the style, and needs the Tenant visual identity permission. An assignment is unique per tenant and cluster version.

Which style applies

A cluster chooses the style of a tenant with these rules:

  1. The cluster takes the style that is assigned to exactly its own version, when there is one. Otherwise it takes the style that is assigned to the highest version that is lower than or equal to its own version, among the assignments of that tenant for that cluster. An assignment without style gives the default style of the tenant, with its images. Versions are compared with the semantic versioning order, so 1.2.0-beta.1 is lower than 1.2.0, and the build metadata (1.2.0+abc) is ignored by the order. A version that is not a valid semantic version is ignored.
  2. When no assignment has a version lower than or equal to the cluster version, the default style of the tenant applies. When the tenant has no default style either, nothing changes: the tenant users see the standard look.
  3. Inside the chosen style, a dark theme uses the dark slot (its dark theme and its dark CSS) when it is not empty, and the light slot otherwise.
  4. The chosen style is used as a whole, never mixed with the default style of the tenant: its CSS and its images always come from the same style. A style that has no dark slot is not completed by the dark slot of the default style.
Styles assigned to the tenant for the cluster Cluster version Style used
1.0.0, 2.0.0 1.5.0 The one of 1.0.0
1.0.0, 2.0.0 2.0.0 The one of 2.0.0
1.0.0, 2.0.0 3.1.0 The one of 2.0.0
2.0.0 1.5.0 None, native look

The style is chosen as a whole, never per slot. A cluster at version 3.1.0 with a style assigned to 2.0.0 and another to 1.0.0 never mixes the dark slot of the older one with the light slot of the newer one.

The default style is published as an entry of version 0.0.0, the lowest one, so that every cluster version reaches it. A cluster takes it, by its flag isDefault in the manifest, when no assignment has a version lower than or equal to its own, including for a pre-release such as 0.0.0-beta, which is lower than 0.0.0. A style that is assigned to the version 0.0.0 itself is kept next to it, and a pre-release of 0.0.0, which is lower than 0.0.0, still gets the default style.

The choice between the light and the dark slot follows the colorScheme of the active theme (see Color scheme). When the user switches theme, the client switches the stylesheet too.

Choose the style from the cluster code

The code that a cluster runs at authentication, a tenant resolved interceptor, can replace the style chosen by the rules above, for example according to the user, a role or business data of the tenant. Before the first interceptor runs, the VisualIdentity property of the result holds the name of the style chosen by the rules, or null when the tenant keeps the standard look. WithVisualIdentity replaces it:

public class TenantResolvedInterceptor : ITenantResolvedInterceptor
{
    private readonly ICurrentUser _currentUser;

    public TenantResolvedInterceptor(ICurrentUser currentUser)
    {
        _currentUser = currentUser;
    }

    public Task<OnTenantResolvedResult> OnTenantResolvedAsync(NeosTenantInfo? tenant, OnTenantResolvedResult previousTenantResolvedResult)
    {
        // The partners of the tenant see the style of the partner network, the other users keep the style of the tenant
        if (_currentUser.IsPartner)
        {
            return Task.FromResult(previousTenantResolvedResult.WithVisualIdentity("Partner network"));
        }

        return Task.FromResult(previousTenantResolvedResult);
    }
}

In this example, ICurrentUser stands for a service of the cluster.

  • Any style. The cluster can choose any style of Tenant Management, whether a tenant uses it or not. The rules of the cluster versions do not apply to a style chosen by its name: the style is used as a whole, with its light slot, its dark slot and its images.
  • Exact name. The name is compared with its case. WithVisualIdentity(null) gives the standard look, even when the tenant has a style.
  • Unknown name. A name that no style has, for example after a style was renamed or deleted, is logged as a warning (The visual identity ... chosen for the tenant ... is unknown) and the style chosen by the rules is kept. The sign-in never fails because of a style.
  • Several interceptors. Each interceptor reads the value left by the previous one, and the value left by the last one is applied, whatever the order of the interceptors. An interceptor that returns a new result without calling WithVisualIdentity keeps the value of the previous one.
  • When it applies. The choice is made at each authentication of a user. A user who is already signed in keeps the style of the page until the page is reloaded.
  • Images. A style chosen by the code, even the one that the rules chose too, is served under its own path, so its images stay the ones of its stylesheet while the page is open.
  • After an upgrade. When Tenant Management does not rebuild the store at its start (DistributedStores:RebuildOnStart set to false), the store published by an earlier version has neither the styles on their own nor their names. The clusters then ask Tenant Management for them at each authentication, until the RebuildTenantStyles method is run (see Rebuild the published styles).

A style chosen by its name is published once for all the tenants and all the clusters, and any signed-in user of a multitenant cluster can load it by its identifier. Do not put content that must stay private to a tenant in a style. The theme of a style is built on the theme of a cluster version (see Base themes): a style built on the theme of another cluster can look wrong in this cluster, so check it in the cluster before choosing it from the code.

Limits and validation

The following rules are checked when a style is saved, with a message in the editor.

Rule Value
Name of a style Required and unique, 100 characters at most, no leading or trailing space
Light slot Required: a light theme or a light CSS. Either one alone is enough
Theme and slot A light theme or CSS must have the Light color scheme, and a dark theme or CSS must have the Dark color scheme
Size of each slot 1 MB (1,048,576 bytes in UTF-8) for the CSS published for the slot: its theme CSS, a line break, then its CSS
Encoding of a CSS Must be encodable in UTF-8. A .css file is read in the browser as UTF-8: a BOM is ignored, an invalid byte becomes U+FFFD
Size of an image 2 MB
Images per CSS 20
Name of an image Starts with a letter, a digit, an underscore or a hyphen, then contains only letters, digits, dots, underscores and hyphens
Unique names An image name is unique inside a CSS, and across the light CSS and the dark CSS of a style
Type of an image One of the allowed image types listed below

The allowed image types are image/avif, image/bmp, image/gif, image/jpeg, image/png, image/svg+xml, image/vnd.microsoft.icon, image/webp and image/x-icon. The name rule rejects path separators, spaces and any character that has a meaning in a URL or in a CSS url().

The line break exists only when the slot has both a theme CSS and a CSS. The size limit covers both parts together. A dark slot that has a theme and no CSS counts the CSS of the light slot as its CSS. The limit is also checked when a theme or a CSS is saved, for every style that uses it (see What happens when a theme changes and CSS). A CSS is also limited to 1 MB on its own. The rules on the values of a theme are in Values that are accepted.

How the styles reach the browser

The browser never reads the Tenant Management database. Tenant Management publishes the styles in the framework state store, and each cluster reads them from there.

sequenceDiagram
    participant TM as Tenant Management
    participant SS as State store
    participant BE as Cluster backend
    participant UI as Browser

    TM->>SS: Publish the manifest and the CSS bodies
    UI->>BE: on-authenticated
    BE->>SS: Read the manifest of the tenant and the cluster
    BE-->>UI: Client-only context with the light and dark hashes
    UI->>BE: GET stylesheet by hash
    BE->>SS: Read the CSS body
    BE-->>UI: CSS
    UI->>BE: GET images referenced by the CSS
    BE->>TM: Read the image when not cached
    BE-->>UI: Image

Publication:

  • Tenant Management publishes, for each tenant and cluster, a small manifest that lists the styles of its versions and the SHA-256 hashes of their CSS and images, and the CSS bodies, stored under the tenant and the hash of their content. The body of a slot is its theme CSS followed by its CSS, hashed as one file.
  • It republishes the manifest of a tenant and cluster each time an assignment, a default style, a style that it uses or one of its images is created, modified or deleted, and each time a theme or a CSS that such a style uses is modified.
  • It also publishes every style on its own, whether a tenant uses it or not, so that a cluster can apply it by its name (see Choose the style from the cluster code). A style is stored under its identifier, with a key that gives the identifier of its name, and it is republished each time it, its theme, its CSS or one of its images changes. A rename only moves the key of the name.
  • It rebuilds the whole store when Tenant Management starts, unless the configuration DistributedStores:RebuildOnStart is false, and when the RebuildTenantStyles server method is called (see Rebuild the published styles). Before that, it generates again the CSS of the themes that another version of Neos generated (see When a new version of Neos is deployed).
  • Images are not stored in the state store. Each cluster replica fetches them from Tenant Management and keeps them in memory by hash, for one hour at most. The CSS bodies that a replica reads from the state store are kept in memory in the same way, so a CSS that Tenant Management no longer keeps is not served for longer than that.

Delivery to the browser:

  1. During authentication, the cluster backend runs the on-authenticated call. It reads the style that applies to the tenant for this cluster version before the interceptors of the cluster, and applies the style that they leave after them. When a style applies, it adds a TenantStyles entry to the client-only context, with the hashes of the light and dark stylesheets, and the identifier of the style when the code of the cluster chose it by its name. Without a style, nothing is added and no request is made.
  2. The client adds a link element for the stylesheet after the style element of the theme, so the tenant CSS overrides the theme. When both hashes are equal, one link serves both color schemes. The stylesheet of a style chosen by the code of the cluster is loaded under the path of that style, so its relative url(assets/<name>) resolves to its own images.
  3. The application waits for the stylesheet before it is shown, to avoid a flash of the standard look, but at most 3 seconds. A slow or failing request never blocks the application. After 3 seconds the application starts with the standard look and the stylesheet keeps loading: it is applied as soon as it arrives, also when the color scheme changes later. A request that fails removes the stylesheet and the standard look is kept.

Routes served by the cluster, under its webapi prefix:

Route Content Caching
$neos/tenant-styles/{hash}.css The CSS whose SHA-256 hash is {hash} Immutable, because the hash is the content
$neos/tenant-styles/assets/{name} The image {name} of the style that applies Revalidated at each use, with its hash and its MIME type as ETag
$neos/tenant-styles/identities/{id}/{hash}.css The CSS whose SHA-256 hash is {hash}, of the style {id} Immutable, because the hash is the content
$neos/tenant-styles/identities/{id}/assets/{name} The image {name} of the style {id} Revalidated at each use, with its hash and its MIME type as ETag

The first two routes only answer for the tenant of the current request. The routes of a style {id} serve a style chosen by the code of the cluster, to any signed-in user of a tenant. A CSS response carries X-Content-Type-Options: nosniff. Every image response carries X-Content-Type-Options: nosniff and Content-Security-Policy: sandbox, so that an SVG opened directly in the browser cannot run a script.

If the state store is not completely built yet, for example right after it was lost, the cluster asks Tenant Management for the hashes, the CSS and the images instead. The application keeps working while Tenant Management rebuilds the store.

When a change becomes visible

The cluster reads the manifest from the state store at each authentication, with no expiration delay. A change saved in Tenant Management is therefore seen by the next page load or sign-in of a user, whichever replica answers. This holds for the edit of a CSS and for the edit of a theme alike.

A change is not pushed to a session that is already open: the page keeps the stylesheet it loaded until the user reloads it.

Rebuild the published styles

Tenant Management republishes the styles when an assignment is saved or deleted, when a tenant is saved, when a style that a tenant uses is saved, or when a theme or a CSS that such a style uses is saved. It also follows the change of the identifier of a tenant, the rename or the deletion of a cluster, and the deletion, the renumbering or the move to another cluster of a cluster version: the manifest under the former identifier or name is replaced by an empty one and its CSS bodies expire after one day, and the manifest under the new one is published. A change made directly in the database is not seen. In that case, rebuild the store with the RebuildTenantStyles server method of Tenant Management, which rebuilds the styles of every tenant and cluster:

POST /api/v1/tenant-styles/rebuild

The method first generates again the CSS of every theme with the running version of Neos (see When a new version of Neos is deployed). It can also repair a state store that was lost. The caller needs the Tenant visual identity permission. If another replica is already rebuilding the store, the call fails with a message asking to retry in a few moments. A restart of Tenant Management does the same rebuild, and only generates again the CSS of the themes that another version of Neos generated.

Check the published styles

When a tenant does not see its style, look at what Tenant Management published in the state store (Redis). The keys use the colon as separator, so a Redis browser such as RedisInsight groups them in a tree: TenantStyles, then Manifest and Body.

Key Content
TenantStyles:Index The tenants and clusters whose manifest was published, in lowercase. Only Tenant Management reads and writes it
TenantStyles:completed The marker that tells the store has been completely built. Its value is a date
TenantStyles:Manifest:({tenant})_({cluster}) The manifest of a tenant for a cluster: the versions with the hashes of their CSS and the names of their images
TenantStyles:Body:({tenant})_({cluster})_({hash}) The CSS whose SHA-256 hash is {hash}, for this tenant and this cluster
TenantStyles:IdentityIndex The identifiers of the styles published on their own. Only Tenant Management reads and writes it
TenantStyles:IdentitiesPublished The marker that tells every style was published on its own by the last rebuild. Its value is a date
TenantStyles:IdentityName:({hash of the name}) The identifier of the style that has this exact name. The key holds the SHA-256 hash of the UTF-8 bytes of the name
TenantStyles:Identity:({id}) The style {id} on its own: its name, the hashes of its CSS and the names of its images
TenantStyles:IdentityBody:({id})_({hash}) The CSS whose SHA-256 hash is {hash}, for the style {id}

The tenant and the cluster are in lowercase in the keys, for example TenantStyles:Manifest:(akanea)_(technicaldemos). Each version of a manifest also tells the identifier and the name of its style (styleId and styleName), which is the name that a tenant resolved interceptor reads in VisualIdentity. The Helm component of the state store does not add a prefix to the keys (keyPrefix: none), so they appear in Redis as written here. Dapr stores each value as a Redis hash: the value is in its data field and its version in its version field.

To follow a style from the database to the browser, check in this order:

  1. The store is built. The key TenantStyles:completed exists. Without it, the clusters do not read the store and ask Tenant Management instead (see When the state store is not available).
  2. The manifest exists. Read TenantStyles:Manifest:({tenant})_({cluster}). A missing key or a manifest with an empty versions list means that no style is published for this tenant and this cluster, so the tenant keeps the standard look. The entry flagged isDefault, with the version 0.0.0, is the default style of the tenant.
  3. The version is the right one. The cluster takes the version of its own, otherwise the greatest version lower than or equal to its own, otherwise the entry flagged isDefault. See Which style applies.
  4. The bodies exist. For each lightHash and darkHash of the manifest, the key TenantStyles:Body:({tenant})_({cluster})_({hash}) must exist. A body that no manifest lists any more keeps a time to live of one day, which TTL shows. It stays readable for the sessions that are already open, then it disappears.

With redis-cli, on the Redis of the deployment:

# List the keys of the store
redis-cli --scan --pattern 'TenantStyles*'

# Is the store built?
redis-cli EXISTS 'TenantStyles:completed'

# Read the manifest of a tenant for a cluster
redis-cli HGET 'TenantStyles:Manifest:(acme)_(crm)' data

# Does the body of a hash exist, and until when?
redis-cli EXISTS 'TenantStyles:Body:(acme)_(crm)_(<hash>)'
redis-cli TTL 'TenantStyles:Body:(acme)_(crm)_(<hash>)'

TTL answers -1 for a body that a manifest lists (it never expires), a number of seconds for a body kept for the open sessions, and -2 when the key does not exist.

When a manifest or a body is missing or wrong, run the RebuildTenantStyles method to publish everything again from the database (see Rebuild the published styles). The logs of Tenant Management tell what happened: Tenant styles state store was built successfully. at the end of a rebuild, and an error such as The manifest ... could not be published after 5 attempts. when a publication failed.

When the state store is not available

The database of Tenant Management is the source of truth: the state store only holds a copy that the clusters read quickly. A Redis that does not answer, or that lost its data, therefore never loses a style, but it changes how the styles reach the users.

Business cluster. The cluster never fails because of the styles, and the page waits for the stylesheet 3 seconds at most.

  • If the cluster cannot read the marker TenantStyles_completed, it logs the error Failed to check if the state store TenantStyles is completed and behaves as if the store was not built: it asks Tenant Management for the hashes, the CSS and the images (methods GetTenantStyle, GetTenantStyleBody and GetTenantStyleAsset, and GetVisualIdentity, GetVisualIdentityBody and GetVisualIdentityAsset for a style chosen by the code of the cluster). Tenant Management reads them from its database, and the call does not go through Redis, so the tenants still get their style. Each authentication then costs a call to Tenant Management.
  • If the marker is read but the manifest is not, the cluster logs Tenant styles: GetHashesAsync failed and sends no style for this authentication. The user sees the standard look, and the next authentication tries again. Nothing is cached after a failure.
  • The CSS that a replica already served stays in its memory, by hash, and is still served. A CSS that is not in memory cannot be read: the stylesheet route answers 404, the page removes the stylesheet and keeps the standard look.
  • The images never come from Redis, the cluster fetches them from Tenant Management. They are still served.

If Redis is down, the other state stores of Tenant Management (tenant resolution, clusters, users) are unavailable too, so the multitenant application is usually affected before its styles are.

Tenant Management. A save is committed in the database first, and the publication to the state store follows. If Redis does not answer, or the publication lock cannot be taken, the save reports an error such as The style could not be published because it was changed at the same time by another operation. Save it again, or run the tenant styles rebuild., but the data is stored. The manifest in Redis keeps its previous content, so the clusters keep applying the previous style. When Redis is back, save the style again, or run RebuildTenantStyles, to publish the change.

Redis restarted without its data. The clusters see that the marker is missing and use Tenant Management, as described above. Tenant Management does not detect it while it runs: it rebuilds the store when it starts, and when RebuildTenantStyles is called. After a loss of Redis, run RebuildTenantStyles (or restart Tenant Management) to return to the normal operation. The call needs the Tenant visual identity permission.

Known limits

  • Login and tenant selection pages. They show the standard look. The tenant is not known before authentication, so no tenant CSS can be sent.
  • Changes made outside Tenant Management. The rename of a tenant, of a cluster or of a cluster version is followed by Tenant Management when it is saved from its screens or its API. A change made directly in the database is only taken into account by a full rebuild (a start of Tenant Management or the RebuildTenantStyles method). The rebuild also replaces by empty manifests the ones of the tenants and clusters that no longer have any style in the database, which it finds in an index that Tenant Management keeps in the state store.
  • Development mode without Dapr. The local state store is used, as for the other framework state stores.
  • Remote applications with their own theme manager. A remote application that runs without a parent application, and therefore manages its own theme, does not apply tenant styles. A remote that runs inside a parent application shares the page, so the stylesheet of the parent applies to it.
  • Size of a published CSS. The state store value is limited by the request size of the Dapr sidecar, which Neos does not change. The CSS is stored as a JSON string, and the escaping can make it larger than its size on disk. A CSS close to the 1 MB limit can therefore fail to publish. The generated CSS of a theme counts in this size, next to the CSS. Keep the CSS small, and use images for heavy content.
  • Images of a session that is already open. The images are read from the style that applies at the time of the request, not from the one of the stylesheet that the page loaded. After a tenant is given another style, a page that is already open can lose the images of its old stylesheet until it is reloaded. A style chosen by the code of the cluster does not have this limit: its images are read under the path of its stylesheet.
  • Style chosen by the code of the cluster. It is chosen at authentication only, by a tenant resolved interceptor, which does not run for a cluster that is not multitenant. A remote application that runs without a parent application does not apply it either.
  • Light and dark choice. It relies on the colorScheme of the active theme. A theme that declares itself light but looks dark receives the light slot, and a dark tenant theme applies only when the active theme of the cluster is dark.
  • Empty dark slot. The content of the light slot, its theme and its CSS, is also used in the dark mode, and it overrides the dark colors with light ones. Give a dark theme or a dark CSS to every tenant that supports the dark mode. See Light and dark slots.
  • Scope of a theme. A theme only edits colors and component attributes. The font, the color scheme of the theme of the cluster, the animated buttons, the order of the buttons and the breakpoints cannot be changed with a theme. See What a theme cannot change.
  • Theme saved during a rolling update. While a new version of Neos is being deployed, a replica of the previous version can still serve the screens. A theme saved through such a replica gets the CSS of the previous version, after the new replicas have generated the CSS again at their start. A version of Tenant Management that does not know the version of the CSS even leaves the version of the new one on the theme, so the next start does not detect it either. When a theme was saved during the deployment, run the RebuildTenantStyles method once the deployment is over: it generates again the CSS of every theme (see When a new version of Neos is deployed).
  • Policy of the application. A tenant CSS is served from the application origin, so it respects a strict Content-Security-Policy. An external resource in the CSS (@import, url(https://...)) is blocked by such a policy. See Content-Security-Policy at the Gateway.

See also