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
Use high-quality logos
Upload logos at least 256x256px for crisp display on high-DPI screens.
Choose accessible colors
Ensure your primary color has sufficient contrast ratio (WCAG AA: 4.5:1 minimum) against white and dark backgrounds.
Test both themes
If using a custom color, verify it looks good in both light and dark modes.
Optimize logo files
Use SVG for logos when possible for scalability. For raster images, use optimized PNG or WebP.
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.