Skip to main content
Branding customization allows you to tailor the appearance of your Featul workspace to match your company’s visual identity. Control logos, color schemes, layout styles, and more.
Advanced branding features are available on Starter and Professional plans. The Free plan has limited branding options.

Branding Configuration

Each workspace has a brandingConfig record with these customization options:
type BrandingConfig = {
  id: string
  workspaceId: string
  logoUrl: string | null           // Workspace logo (stored in workspace.logo)
  primaryColor: string             // Hex color code
  theme: 'light' | 'dark' | 'system'
  showLogo: boolean                // Display logo in header
  showWorkspaceName: boolean       // Display workspace name
  hidePoweredBy: boolean           // Hide "Powered by Featul" footer
  layoutStyle: string              // Layout variation
  sidebarPosition: string          // Sidebar placement
}

Retrieving Branding Settings

Get current branding configuration for a workspace:
const branding = await client.branding.byWorkspaceSlug.$get({
  slug: 'acme'
})
Response:
{
  "config": {
    "id": "branding_123",
    "logoUrl": "https://www.google.com/s2/favicons?domain_url=acme.com&sz=128",
    "primaryColor": "#3b82f6",
    "theme": "system",
    "showLogo": true,
    "showWorkspaceName": true,
    "hidePoweredBy": false,
    "layoutStyle": "default",
    "sidebarPosition": "left"
  }
}
This endpoint is public and cached for 30 seconds to optimize performance for visitor-facing pages.

Updating Branding

Color Customization

Set your primary brand color:
await client.branding.update.$post({
  slug: 'acme',
  primaryColor: '#10b981' // Green
})
The primary color is used for:
  • Call-to-action buttons
  • Links and active states
  • Progress indicators
  • Board highlights

Theme Selection

Choose the color theme for your workspace:
await client.branding.update.$post({
  slug: 'acme',
  theme: 'dark' // or 'light', 'system'
})
Theme options:
  • light: Always use light mode
  • dark: Always use dark mode
  • system: Follow user’s system preference (default)

Logo Configuration

Upload and display your workspace logo:
await client.branding.update.$post({
  slug: 'acme',
  logoUrl: 'https://cdn.acme.com/logo.png',
  showLogo: true
})
Logo upload requires Starter or Professional plan. The Free plan uses the auto-fetched favicon from your workspace domain.

Workspace Name Display

Control whether the workspace name appears alongside the logo:
await client.branding.update.$post({
  slug: 'acme',
  showWorkspaceName: false // Hide workspace name, show logo only
})

Layout Customization

Adjust the layout style and sidebar position:
await client.branding.update.$post({
  slug: 'acme',
  layoutStyle: 'compact',
  sidebarPosition: 'right'
})

Removing Featul Branding

Hide the “Powered by Featul” footer on Professional plans:
await client.branding.update.$post({
  slug: 'acme',
  hidePoweredBy: true
})
The hidePoweredBy option requires Professional plan. Attempting to enable it on Free or Starter plans will return a 403 Forbidden error.

Plan-Based Feature Restrictions

The branding router enforces plan limits using getPlanLimits() from branding.ts:41-46:

Free Plan

  • ❌ Custom logo upload
  • ❌ Custom primary color
  • ❌ Logo visibility toggle
  • ❌ Workspace name toggle
  • ❌ Hide powered by footer
  • ✅ Theme selection
  • ✅ Layout style
  • ✅ Sidebar position

Starter Plan

  • ✅ Custom logo upload
  • ✅ Custom primary color
  • ✅ Logo visibility toggle
  • ✅ Workspace name toggle
  • ❌ Hide powered by footer
  • ✅ Theme selection
  • ✅ Layout style
  • ✅ Sidebar position

Professional Plan

  • ✅ All branding features
  • ✅ Hide powered by footer

Automatic Logo Fetching

When you create a workspace, Featul automatically fetches your company’s favicon:
const host = new URL(input.domain).host
const favicon = `https://www.google.com/s2/favicons?domain_url=${encodeURIComponent(host)}&sz=128`

await ctx.db.insert(workspace).values({
  // ...
  logo: favicon
})
This provides a default logo until you upload a custom one.

Updating Logo in Workspace Settings

The logo URL is stored in the workspace.logo field, not directly in brandingConfig. When updating via the branding API:
// Logo is updated in workspace table
if (typeof input.logoUrl !== 'undefined') {
  await ctx.db.update(workspace)
    .set({ logo: input.logoUrl })
    .where(eq(workspace.id, ws.id))
}
This design allows the branding configuration to reference the workspace logo without duplication.

Branding Initialization

When a workspace is created, an empty branding configuration is automatically provisioned:
await ctx.db.insert(brandingConfig).values({
  workspaceId: ws.id
})
Default values are applied by the database schema:
  • primaryColor: #3b82f6 (blue)
  • theme: system
  • showLogo: true
  • showWorkspaceName: true
  • hidePoweredBy: false

Permission Requirements

Branding updates require one of:
  • Workspace owner role
  • Admin role (implied permission)
  • canConfigureBranding permission
The requireBrandingManagerBySlug() helper from branding.ts:40 enforces this:
const ws = await requireBrandingManagerBySlug(ctx, input.slug)
Unauthorized requests receive a 403 Forbidden response.

Error Handling

Plan Restrictions

Attempting to use premium features on lower plans:
{
  "status": 403,
  "message": "Branding not available on current plan"
}
Or for specific features:
{
  "status": 403,
  "message": "Removing 'Powered by' requires a higher plan"
}

Logo Upload Validation

Logo URLs should be:
  • Publicly accessible HTTPS URLs
  • Valid image formats (PNG, JPEG, SVG)
  • Reasonable file size (< 2MB recommended)

Best Practices

1

Use high-quality logos

Upload logos at least 256x256px for crisp display on high-DPI screens.
2

Choose accessible colors

Ensure your primary color has sufficient contrast ratio (WCAG AA: 4.5:1 minimum) against white and dark backgrounds.
3

Test both themes

If using a custom color, verify it looks good in both light and dark modes.
4

Optimize logo files

Use SVG for logos when possible for scalability. For raster images, use optimized PNG or WebP.
5

Cache considerations

Branding configuration is cached for 30 seconds. Changes may take up to a minute to appear for all users.

CSS Variable Integration

The primary color is typically applied as a CSS custom property:
:root {
  --brand-primary: #3b82f6;
}

.cta-button {
  background-color: var(--brand-primary);
}
This allows dynamic theme switching without rebuilding the application.

Example: Complete Branding Setup

// 1. Upload logo to your CDN
const logoUrl = await uploadToS3(logoFile)

// 2. Apply comprehensive branding
await client.branding.update.$post({
  slug: 'acme',
  logoUrl: logoUrl,
  primaryColor: '#10b981',
  theme: 'light',
  showLogo: true,
  showWorkspaceName: false,
  hidePoweredBy: true, // Professional plan only
  layoutStyle: 'compact',
  sidebarPosition: 'left'
})

// 3. Verify changes
const updated = await client.branding.byWorkspaceSlug.$get({
  slug: 'acme'
})

Branding vs Workspace Settings

Some visual settings are stored in the workspace table rather than brandingConfig: In workspace table:
  • logo - Logo URL
  • theme - Originally in workspace, now deprecated in favor of brandingConfig
  • primaryColor - Originally in workspace, now deprecated
  • hideBranding - Originally in workspace, now hidePoweredBy in brandingConfig
In brandingConfig table:
  • All current branding settings (primaryColor, theme, layout preferences)
This migration maintains backward compatibility while centralizing branding configuration.

Build docs developers (and LLMs) love