In B2B SaaS applications, multi-tenancy means that a single, active production build of your frontend must render a unique look-and-feel, distinct capabilities, and customized validations depending on the logged-in B2B customer (the tenant).
We solved this at scale by separating layout definitions from custom style parameters and user authorization models.
1. Runtime Theming via CSS Custom Properties
Instead of compiling separate CSS stylesheets for each tenant, we leverage
dynamic CSS custom properties. At boot time, the tenant configuration is fetched from a metadata endpoint, and custom color variables are injected directly into the document root:
// src/plugins/theme.ts
export function injectTenantTheme(themeConfig) {
const root = document.documentElement;
root.style.setProperty('--v-primary-base', themeConfig.primaryColor || '#00d4aa');
root.style.setProperty('--v-secondary-base', themeConfig.secondaryColor || '#161616');
root.style.setProperty('--v-theme-radius', ${themeConfig.borderRadius || 8}px);
// Dynamically update favicon based on B2B metadata
const link = document.querySelector("link[rel*='icon']") || document.createElement('link');
link.type = 'image/x-icon';
link.rel = 'shortcut icon';
link.href = themeConfig.faviconUrl || '/default-favicon.ico';
document.getElementsByTagName('head')[0].appendChild(link);
}
This runtime transformation enables B2B clients to customize button shapes, colors, and logos instantly without requiring any developer intervention or project redeployment.
2. CASL-Powered Client-Side Guard Rails
Authorization in a multi-tenant platform is highly variable. Some clients enable event management modules, while others restrict them to basic registration.
We integrated
CASL to maintain strict client-side role guard rails. User abilities are parsed from cookies and loaded into global stores:
// src/plugins/casl.ts
import { defineAbility } from '@casl/ability';
export const updateAbilities = (userPermissions) => {
return defineAbility((can) => {
userPermissions.forEach(permission => {
can(permission.action, permission.subject);
});
});
};
In the templates, checking permissions is extremely simple:
<v-btn v-if="$can('create', 'InnovationProgram')" @click="createNewProgram">
Launch Program
</v-btn>
Combined with HTTP-only session cookies on the backend, this configuration ensures robust safety and clean, modules-level layout configuration.