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"; empty APP_ACCENT_HEX values omit the cor field 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:

  1. Name and Logo: Uses primeiroDefinido to select the first non-empty nome and logoUrl from the stack
  2. Color: Iterates through layers calling resolverCor on each until finding a valid CorResolvida
// 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 resolverCor function
  • Error propagation: Collected into the motivos array and returned with the resolved brand
  • UI safety: app/layout.tsx receives a complete MarcaResolvida object 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_branding provides installation defaults, and organizations.settings.branding offers tenant-specific overrides
  • Field-level precedence: The resolver in lib/branding/resolve.ts evaluates nome, logoUrl, and cor independently rather than replacing entire configurations
  • Defensive programming: Validation errors become MotivoDaMarca objects that propagate without throwing exceptions, protecting the UI from configuration crashes
  • Pure data layers: Each source transforms into a CamadaDeMarca with standardized fields, enabling isolated unit testing and clear source attribution via the origem property

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →