Skip to main content
DocsReferenceTenant & Whitelabel
Back to docs

Tenant & Whitelabel

Multi-tenancy, branding, and custom domains

reference
tenant-whitelabel

Tenant & Whitelabel Configuration

Overview

Creatiq supports multi-tenancy with full whitelabel capabilities. Each tenant can have custom branding (colors, logo, app name, CSS) and an optional custom domain. Whitelabel features are restricted to the Premium plan.

Source files:

  • server/services/tenant/tenant-types.ts
  • server/services/tenant/tenant-service.ts
  • server/middleware/tenant-resolver.ts
  • server/routes/tenant.ts
  • server/middleware/feature-gate.ts
  • server/config/features.ts

Multi-Tenancy Model

Database Schema

Tenants are stored in the tenants table:

ColumnTypeDescription
idUUID (PK)Auto-generated tenant ID
slugVARCHAR(63), UNIQUEURL-safe identifier (e.g., acme-school)
nameVARCHAR(255)Display name
domainVARCHAR(500)Primary domain (e.g., acme.creatiq.app)
custom_domainVARCHAR(500), UNIQUE, nullableCustom domain (e.g., content.acme.edu)
brand_configJSONBBranding configuration
smtp_configJSONB, nullableTenant-specific SMTP settings
planVARCHAR(50)Subscription plan (free, pro, premium)
activeBOOLEANWhether the tenant is active
created_atTIMESTAMPCreation timestamp
updated_atTIMESTAMPLast update timestamp

Indexes:

  • idx_tenants_slug on slug
  • idx_tenants_domain on domain
  • idx_tenants_custom_domain on custom_domain

Tenant Interface

interface Tenant {
  id: string;
  slug: string;
  name: string;
  domain: string;
  customDomain: string | null;
  brandConfig: BrandConfig;
  smtpConfig: TenantSmtpConfig | null;
  plan: string;
  active: boolean;
  createdAt: Date;
  updatedAt: Date;
}

User-Tenant Association

Users are linked to tenants via the tenant_id column in creatiq_users. This association is established during JIT provisioning from the Keycloak token's tenant_id claim. A composite unique index ensures one external user ID per tenant:

CREATE UNIQUE INDEX idx_creatiq_users_tenant_external
  ON creatiq_users (tenant_id, external_user_id)
  WHERE tenant_id IS NOT NULL AND external_user_id IS NOT NULL;

Tenant Resolution

Tenant resolution is handled by the tenantResolver middleware, which runs on every request.

Resolution Strategy

The middleware resolves tenants in this order:

  1. Exact domain match: Looks up req.hostname against both tenants.domain and tenants.custom_domain columns. Only active tenants are matched.

  2. Subdomain extraction: If no exact match, the middleware extracts a subdomain from {slug}.{BASE_DOMAIN} (where BASE_DOMAIN defaults to creatiq.app) and looks up by slug.

  3. Default tenant: If no tenant is resolved, req.tenant remains undefined and the default Creatiq branding is used.

Subdomain Validation

Subdomains must match the slug pattern: ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$

  • Lowercase alphanumeric characters and hyphens only
  • Must start and end with alphanumeric character
  • Maximum 63 characters
  • Nested subdomains (containing dots) are rejected

Configuration

VariableDescriptionDefault
BASE_DOMAINBase domain for subdomain resolutioncreatiq.app

Examples

HostnameResolution
acme.creatiq.appSubdomain acme -> lookup by slug
content.acme.eduExact match on custom_domain
creatiq.appNo match -> default tenant
localhostNo match -> default tenant

Branding Configuration

BrandConfig Interface

interface BrandConfig {
  primaryColor: string;       // Hex color (e.g., "#6366f1")
  logoUrl: string | null;     // URL to logo image
  faviconUrl: string | null;  // URL to favicon
  appName: string;            // Application display name
  customCss: string | null;   // Custom CSS (max 50,000 characters)
}

Default Values

When no tenant-specific branding is configured, these defaults are used:

PropertyDefault Value
primaryColor#6366f1
logoUrlnull
faviconUrlnull
appNameCreatiq
customCssnull

Get Current Tenant Branding

Endpoint: GET /api/tenant/current

No authentication required. Returns the resolved tenant's branding or the default config.

Response when a tenant is resolved (200):

{
  "success": true,
  "data": {
    "isDefault": false,
    "id": "uuid",
    "slug": "acme-school",
    "name": "Acme School",
    "brandConfig": {
      "primaryColor": "#2563eb",
      "logoUrl": "https://cdn.acme.edu/logo.png",
      "faviconUrl": null,
      "appName": "Acme Learn",
      "customCss": null
    },
    "plan": "premium"
  }
}

Response when no tenant is resolved (200):

{
  "success": true,
  "data": {
    "isDefault": true,
    "brandConfig": {
      "primaryColor": "#6366f1",
      "logoUrl": null,
      "faviconUrl": null,
      "appName": "Creatiq",
      "customCss": null
    }
  }
}

Update Brand Configuration

Endpoint: PUT /api/tenant/brand

Requires authentication and the whitelabel feature (Premium plan only).

Request:

{
  "primaryColor": "#2563eb",
  "logoUrl": "https://cdn.acme.edu/logo.png",
  "faviconUrl": "https://cdn.acme.edu/favicon.ico",
  "appName": "Acme Learn",
  "customCss": ".header { background: navy; }"
}

All fields are optional. Only provided fields are updated (JSONB merge via || operator).

Validation rules:

  • primaryColor: Must be a 6-digit hex color (^#[0-9a-fA-F]{6}$)
  • logoUrl: Valid URL, max 1000 characters, nullable
  • faviconUrl: Valid URL, max 1000 characters, nullable
  • appName: 1-100 characters
  • customCss: Max 50,000 characters, nullable

Response (200):

{
  "success": true,
  "data": {
    "brandConfig": {
      "primaryColor": "#2563eb",
      "logoUrl": "https://cdn.acme.edu/logo.png",
      "faviconUrl": "https://cdn.acme.edu/favicon.ico",
      "appName": "Acme Learn",
      "customCss": ".header { background: navy; }"
    }
  }
}

Custom Domains

Update Custom Domain

Endpoint: PUT /api/tenant/domain

Requires authentication and the whitelabel feature (Premium plan only).

Request:

{
  "customDomain": "content.acme.edu"
}

Validation rules:

  • 3-500 characters
  • Must match ^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$
  • Lowercase alphanumeric with dots and hyphens
  • Must start and end with alphanumeric character

Response (200):

{
  "success": true,
  "data": {
    "customDomain": "content.acme.edu"
  }
}

The custom domain column has a UNIQUE constraint, so no two tenants can use the same domain.

DNS setup: After setting a custom domain, the tenant must create a CNAME record pointing to the Creatiq platform domain. Domain verification is handled at the infrastructure level.


SMTP Configuration

Each tenant can optionally have its own SMTP configuration for sending emails:

interface TenantSmtpConfig {
  host: string;
  port: number;
  user: string;
  pass: string;
  from: string;
}

This field is stored as JSONB in the smtp_config column. There is currently no API endpoint to update SMTP config -- it is managed at the database level.


Whitelabel Feature Gate

All branding and custom domain endpoints are protected by the requireFeature('whitelabel') middleware, which restricts access to the Premium plan.

If a user on the Free or Pro plan attempts to update branding or domains, they receive:

{
  "success": false,
  "error": "feature_not_available",
  "requiredPlan": "premium",
  "message": "This feature requires the premium plan or higher"
}

Plan Requirements Summary

CapabilityFreeProPremium
View current tenant brandingyesyesyes
Update brand configuration--yes
Set custom domain--yes
Custom CSS injection--yes

Tenant Service API

The tenantService object in server/services/tenant/tenant-service.ts provides the following methods:

MethodDescription
getTenantByDomain(domain)Look up active tenant by domain or custom_domain
getTenantBySlug(slug)Look up active tenant by slug
getTenantById(id)Look up tenant by UUID (includes inactive)
createTenant(data)Create a new tenant with slug, name, domain, optional branding and plan
updateTenant(id, data)Update tenant fields (name, domain, customDomain, plan, active)
updateBrandConfig(id, config)Merge partial brand config into existing config

Slug Validation

Tenant slugs are validated against the pattern ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ during creation. Invalid slugs are rejected with an error.


Environment Variables

VariableDescriptionDefault
BASE_DOMAINBase domain for subdomain-based tenant resolutioncreatiq.app
Back to docsdocs/product/reference/tenant-whitelabel.md