Branding API

The branding API provides per-tenant brand customization management. Organizations can configure custom colors, upload logos and favicons, customize the login page, and optionally inject custom CSS (Enterprise plan). All procedures require organization-level authentication (orgProcedure).

Plan Requirements

Custom branding requires the Pro plan or higher (plan level >= 2). The router checks the organization’s subscription plan before allowing updates. Plan levels:

PlanLevelBranding Access
Free0No branding customization
Starter1No branding customization
Pro2Full branding (colors, logos, login page)
Enterprise3Full branding + custom CSS + “Powered by” removal

Default Branding Values

When no custom branding is configured, the system uses these defaults:

PropertyDefault Value
primaryColor#115D9C
accentColor#9AC443
sidebarColor#1E293B
sidebarTextColor#F8FAFC

branding.get

Retrieve the current branding configuration for the authenticated organization. Available to all members because the layout renderer needs branding data for every page load.

Type: Query

Input: None (uses organization context from session)

Output:

{
  branding: {
    primaryColor?: string;      // Hex color (e.g., "#115D9C")
    accentColor?: string;       // Hex color
    sidebarColor?: string;      // Hex color for sidebar background
    sidebarTextColor?: string;  // Hex color for sidebar text
    headerLogo?: string;        // S3 key or URL
    loginLogo?: string;         // S3 key or URL
    favicon?: string;           // S3 key or URL
    loginWelcomeText?: string;  // Max 500 characters
    loginBackground?: string;   // S3 key or URL
    poweredByVisible?: boolean; // Default true
  } | null;
  customCss: string | null;     // Enterprise plan only, max 10,000 chars
  plan: string;                 // Organization's subscription plan name
}

branding.update

Update the organization’s branding configuration. Performs a partial merge with the existing configuration: provided fields overwrite the current values, omitted fields are preserved unchanged.

Type: Mutation

Input Schema:

{
  branding?: {
    primaryColor?: string;      // Hex color matching /^#[0-9A-Fa-f]{6}$/
    accentColor?: string;       // Hex color
    sidebarColor?: string;      // Hex color
    sidebarTextColor?: string;  // Hex color
    headerLogo?: string;        // S3 key after upload
    loginLogo?: string;         // S3 key after upload
    favicon?: string;           // S3 key after upload
    loginWelcomeText?: string;  // Max 500 characters
    loginBackground?: string;   // S3 key after upload
    poweredByVisible?: boolean; // Enterprise plan only for false
  };
  customCss?: string | null;    // Max 10,000 chars, sanitized for XSS
}

Output:

{
  branding: { ... };   // Merged branding configuration
  customCss: string | null;
}

Error Codes:

CodeCondition
FORBIDDENCaller is not admin/owner, or plan level is below Pro
NOT_FOUNDOrganization not found

Custom CSS Validation: The customCss field is sanitized to prevent XSS attacks. The following patterns are rejected:

  • <script tags (case-insensitive)
  • Event handler attributes (onload=, onerror=, etc.)
  • javascript: URLs

Side Effects:

  • Logs branding.updated activity with the list of changed field names.

branding.getUploadUrl

Generate a presigned S3 upload URL for branding assets. The client uses this URL to upload images directly to S3, bypassing the API server for the file transfer.

Type: Mutation

Input Schema:

{
  type: "logo" | "favicon" | "loginBackground";
  contentType: "image/jpeg" | "image/png" | "image/svg+xml";
}

Output:

{
  uploadUrl: string;   // Presigned PUT URL (15-minute expiry)
  fileUrl: string;     // Permanent S3 key to store in branding config
}

S3 Key Format:

branding/{organizationId}/{type}-{timestamp}.{extension}

Error Codes:

CodeCondition
FORBIDDENCaller is not admin/owner, or plan level is below Pro
NOT_FOUNDOrganization not found

Upload Workflow:

  1. Call branding.getUploadUrl with the desired asset type and content type.
  2. Use the returned uploadUrl to PUT the file directly to S3.
  3. Call branding.update with the fileUrl in the appropriate branding field.

branding.preview

Compute CSS custom properties from a branding configuration without persisting anything to the database. This enables real-time preview in the admin UI: as the user adjusts color pickers, the client calls this procedure and applies the returned CSS variables instantly.

Type: Query

Input Schema:

{
  primaryColor?: string;
  accentColor?: string;
  sidebarColor?: string;
  sidebarTextColor?: string;
}

Output:

{
  "--color-primary": string;    // e.g., "#115D9C"
  "--color-accent": string;     // e.g., "#9AC443"
  "--sidebar-bg": string;       // e.g., "#1E293B"
  "--sidebar-text": string;     // e.g., "#F8FAFC"
}

Three-Layer Merge: The preview procedure uses a three-layer priority system for each CSS variable:

  1. Input values (user’s unsaved preview) — highest priority
  2. Current org branding (persisted config) — middle priority
  3. System defaults — lowest priority (ensures no variable is undefined)

Permissions

ProcedureRequired Role
branding.getAny authenticated member
branding.updateAdmin or Owner (Pro+ plan)
branding.getUploadUrlAdmin or Owner (Pro+ plan)
branding.previewAny authenticated member

Security

  • All procedures are scoped via orgProcedure for tenant isolation.
  • Custom CSS is sanitized to reject script injection, event handlers, and javascript: URLs.
  • Upload URLs are presigned with 15-minute expiry and scoped to the organization’s S3 prefix.
  • Only image MIME types (JPEG, PNG, SVG) are accepted for branding asset uploads.