How the White-Label Branding Resolver Merges platform_branding and organizations.settings.branding in DeskcommCRM
The white-label branding resolver prioritizes organization-specific settings over installation defaults and environment variables by stacking three configuration layers into a unified brand resolution system.
DeskcommCRM implements a cascading configuration system that allows multi-tenant deployments to override branding at the organization level while maintaining fallbacks to installation-wide settings and .env defaults. The resolver treats each source as a CamadaDeMarca (brand layer), merging them through field-level precedence rather than wholesale replacement.
The Three-Layer Configuration Stack
The resolver in lib/branding/resolve.ts never reads configuration sources directly. Instead, dedicated helpers transform each source into a standardized layer object with an origem field identifying its source.
Environment Layer (.env)
The camadaDoAmbiente function processes standard environment variables into the base layer:
- Source:
NEXT_PUBLIC_APP_NAME,NEXT_PUBLIC_APP_LOGO_URL,NEXT_PUBLIC_APP_ACCENT_HEX - Behavior: Returns a layer with
origem: "env"; emptyAPP_ACCENT_HEXvalues omit thecorfield entirely, forcing the resolver to skip this layer for color selection - Location: Defined in
lib/branding/resolve.ts
Installation Layer (platform_branding)
The camadaDaInstalacao function in lib/branding/instalacao.ts queries the database table platform_branding:
// lib/branding/instalacao.ts
export async function camadaDaInstalacao(linha: LinhaDeMarcaDeInstalacao): Promise<CamadaDeMarca> {
const { app_name, logo_url, accent_hex } = linha;
return {
origem: "instalacao",
nome: app_name || null,
logoUrl: logo_url || null,
cor: accent_hex ? envelopeDeSemente(accent_hex) : undefined
};
}
If accent_hex is empty, the layer excludes the cor property, allowing the resolver to fall through to the environment layer.
Organization Layer (organizations.settings.branding)
The camadaDaOrganizacao function in lib/branding/organizacao.ts extracts branding from the JSONB column organizations.settings.branding:
- Fields parsed:
app_name,accent_hex,logo_path - Validation: Missing or malformed fields resolve to
null, triggering fallback to lower layers - Precedence: This layer sits at the top of the stack, making organization settings the highest priority
How the Resolver Stacks Configuration Layers
The resolverMarcaDaOrganizacao function (called from app/layout.tsx) constructs an ordered array representing the precedence hierarchy:
// lib/branding/organizacao.ts
export async function resolverMarcaDaOrganizacao(
settings: OrganizationSettings,
linha: LinhaDeMarcaDeInstalacao,
ambiente: VariaveisDeAmbiente
): Promise<MarcaResolvida> {
const camadas = [
camadaDaOrganizacao(marcaDaOrganizacaoDeSettings(settings)), // Highest priority
camadaDaInstalacao(linha), // Middle priority
camadaDoAmbiente(ambiente) // Lowest priority
];
return resolverMarca(camadas);
}
This stack ensures that organization-specific branding overrides installation defaults, which in turn override static .env values.
Field-Level Precedence and Graceful Fallbacks
The core resolverMarca function implements "precedência por campo" (field-level precedence). Rather than replacing the entire configuration when a higher layer exists, it evaluates each field independently:
- Name and Logo: Uses
primeiroDefinidoto select the first non-emptynomeandlogoUrlfrom the stack - Color: Iterates through layers calling
resolverCoron each until finding a validCorResolvida
// lib/branding/resolve.ts
for (const camada of camadas) {
if (!("cor" in camada)) continue;
const tentativa = resolverCor(camada.cor, camada.origem, regua);
motivos.push(...tentativa.motivos);
if (tentativa.cor) {
cor = tentativa.cor; // First defined colour wins
origemDaCor = camada.origem;
break;
}
}
This granular approach allows an organization to customize only its accent color while retaining the installation's default logo and application name, or vice versa.
Error Handling Without Exceptions
The resolver guarantees never throwing during brand resolution. All validation errors—malformed hex codes, invalid envelope shapes, or missing required fields—are captured as MotivoDaMarca objects:
- Validation location: Inside
resolverCorfunction - Error propagation: Collected into the
motivosarray and returned with the resolved brand - UI safety:
app/layout.tsxreceives a completeMarcaResolvidaobject even when branding configuration is partially invalid
This design prevents configuration errors from crashing the Next.js layout, ensuring the application remains accessible even with incomplete or corrupted branding settings.
Integration in the Application Layout
The entry point at app/layout.tsx orchestrates the resolution by fetching all three sources before invoking the resolver:
// app/layout.tsx
import { marcaDaInstalacao } from "@/lib/branding/instalacao";
import { resolverMarcaDaOrganizacao } from "@/lib/branding/organizacao";
async function getBranding(orgId: string): Promise<MarcaResolvida> {
// Load layer sources
const linhaInstalacao = await marcaDaInstalacao();
const ambiente = {
APP_NAME: process.env.NEXT_PUBLIC_APP_NAME,
APP_LOGO_URL: process.env.NEXT_PUBLIC_APP_LOGO_URL,
APP_ACCENT_HEX: process.env.NEXT_PUBLIC_APP_ACCENT_HEX,
};
const orgSettings = await fetchOrgSettings(orgId);
// Resolve with precedence
return resolverMarcaDaOrganizacao(orgSettings, linhaInstalacao, ambiente);
}
Because each layer is wrapped in a pure data object, the resolver remains fully testable without database or environment dependencies.
Summary
- Three-layer architecture: Environment variables form the base,
platform_brandingprovides installation defaults, andorganizations.settings.brandingoffers tenant-specific overrides - Field-level precedence: The resolver in
lib/branding/resolve.tsevaluatesnome,logoUrl, andcorindependently rather than replacing entire configurations - Defensive programming: Validation errors become
MotivoDaMarcaobjects that propagate without throwing exceptions, protecting the UI from configuration crashes - Pure data layers: Each source transforms into a
CamadaDeMarcawith standardized fields, enabling isolated unit testing and clear source attribution via theorigemproperty
Frequently Asked Questions
How does the resolver handle missing accent colors in higher priority layers?
If organizations.settings.branding contains no accent_hex value, the organization layer excludes the cor field entirely. The resolver continues iterating through the stack, checking the installation layer's platform_branding.accent_hex, and finally falling back to .env variables. Empty or null color values never cause errors—they simply trigger the fallback mechanism to the next layer.
Can an organization override just the logo without changing the application name?
Yes. Because the resolver uses field-level precedence, you can set only logo_path in organizations.settings.branding while leaving app_name undefined. The resolver will use the organization's logo but continue using the app_name from platform_branding or .env, depending on availability.
What happens if the hexadecimal color code in platform_branding is malformed?
The resolverCor function validates hex format internally. Malformed values generate a MotivoDaMarca error object that documents the validation failure, but the function returns undefined for that layer's color. The resolver then attempts to source the color from the next layer in the stack (typically .env). This ensures that invalid database entries never crash the application layout.
Where should I configure global branding for a single-tenant deployment?
For single-tenant installations, configure branding in the platform_branding database table rather than relying solely on .env files. This approach allows runtime updates without rebuilding the application, while the instalacao.ts module handles memoization and cache invalidation to maintain performance.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →