Skip to content

Partner White-Labeling

What it does

Allows partners (e.g., Rillet) to run Stacksync under their own brand. Users see the partner's logo, name, and colors across the login page, app UI, emails, and error notifications.

Why it exists

Partners resell Stacksync to their customers. Those customers should see the partner's brand, not Stacksync's.

How it works

Partner Detection (workflows-frontend)

Single source of truth: detectPartnerFromUrl() in src/api/partners/get-config.ts.

Detection Method Example
Hostname map connect.rillet.comrillet
URL path /partner/rillet/...rillet
localStorage override __partner_override__ = rillet (dev/testing)

Branding Config

Stored in partners table → branding JSONB column. Fetched via GET /v1/partners/{id}/config.

{
  "preferred_theme": "light",
  "texts": {
    "company_display_name": "Rillet",
    "product_name": "Rillet Integration Platform",
    "page_title": "Rillet",
    "ai_copilot_name": "Rillet Copilot",
    "support_email": "hello@rillet.com",
    "documentation_url": "https://docs.stacksync.com"
  },
  "colors": {
    "light": {
      "primary": "#5B4CFF",
      "primary_hover": "#4A3DE6",
      "primary_active": "#3A2ECC"
    }
  },
  "logos": {
    "icon_url": "https://asset.brandfetch.io/...",
    "favicon_url": "https://asset.brandfetch.io/..."
  },
  "create_resource_page_header_image": "https://cdn.brandfetch.io/...",
  "emails": {
    "logo_url": "https://cdn.brandfetch.io/.../logo.png",
    "logo_width": "80",
    "frontend_url": "https://connect.rillet.com",
    "support_email": "hello@rillet.com",
    "button_color": "#5B4CFF"
  }
}

What Gets White-Labeled

Area How Where
Login page Auth0 Universal Login template detects Rillet client ID, swaps logo Auth0 tenant config
Passwordless email Auth0 connection template uses application.client_metadata.partner_email_logo_url Auth0 passwordless connection
App UI usePartnerConfig() hook provides texts, colors, logos to all components src/hooks/use-branding.ts
CSS variables Partner colors override Carbon Design System variables via !important src/app/(carbon)/layout.tsx
Error notification emails {platform_name} in all error templates, resolved from workspace → partner connectors/utils/error_notification_builder.py
SendGrid email templates Handlebars: {{platform_name}}, {{logo_url}}, {{button_color}}, {{support_email}} SendGrid dynamic templates
Workspace invite emails platform_name passed as template variable + sender name endpoints/common/workspaces.py
Write error messages Generic wording: "in our platform" instead of "in Stacksync" apps/*/write.py
Workflow modules Backend replaces "Stacksync" in module names + swaps icons for stacksync*/file_service app types. Category icons also replaced for system. fe-logics/utils/a_utils/partner_branding_modules.py
Apps branding Category names/icons in module sidebar branded when workspace_id passed fe-logics/endpoints/common/apps_branding/endpoint.py
Resource table icons "Apps" column uses appBrandings from store for partner icon resolution resource-app-icons-tooltip.tsx

Partner Provisioning Gate (workflows-frontend)

When a user logs in via a partner URL but isn't provisioned:

  1. POST /login with X-Partner-Id header
  2. Backend checks user.options.partners.{partner_id}
  3. Returns partner_provisioned: false if not found
  4. Frontend shows "Account Not Provisioned" page with partner branding

Workspace Filtering

  • Partner mode: /login returns only workspaces with options.partner.partner_id matching
  • Normal mode: /login excludes all partner-provisioned workspaces

Hidden UI Elements in Partner Mode

  • "Create new workspace" button (workspace switcher)
  • Pricing section (settings page)
  • Stacksync-specific text replaced with partner name throughout

Key Files

File Purpose
wf-fe/src/api/partners/get-config.ts Partner detection, config types, hostname map
wf-fe/src/hooks/use-branding.ts React hook for partner branding
wf-fe/src/components/partner-provisioning-gate.tsx "Not provisioned" gate
fe-logics/routes/partners/endpoint.py Public partner config API
fe-logics/routes/partners/partner_api.py Partner workspace/user CRUD API
fe-logics/functions/partners/branding.py resolve_branding_vars() — all SendGrid template vars
fe-logics/db/workspaces.py get_workspace_partner_branding() — single JOIN query
fe-logics/endpoints/common/notifications.py send_email_notification() — merges branding vars
fe-logics/endpoints/common/detect_schema_changes/utils.py switch_off_base_due_to_error() — schema change notifications with branding
fe-logics/endpoints/common/users/users.py Login with partner context
wf-fe/src/utils/oauth/app-oauth-handler.ts OAuth popup handlers — Airtable PKCE with cross-origin postMessage
connectors/commons/partners.py resolve_workspace_partner() — single lookup for name, docs URL, email vars
connectors/utils/error_notification_builder.py Partner-aware error emails with full branding vars and details_link
fe-logics/utils/a_utils/partner_branding_modules.py apply_partner_branding_to_modules(), apply_partner_branding_to_app_brandings(), translate_search_term()
fe-logics/db/partners.py get_partner_branding_for_workspace() — single JOIN (workspaces → partners)
fe-logics/endpoints/workflows/modules/endpoint.py Module list/search/info with partner branding on responses
fe-logics/endpoints/common/apps_branding/endpoint.py Apps branding with optional workspace_id for partner context
wf-fe/src/components/tables/_carbon/resources/resource-app-icons-tooltip.tsx Resource table icon resolution using appBrandings
wf-fe/src/api/app_brandings/get.ts getAppBrandings() — passes workspace_id, in-memory cache with workspace invalidation