VibeWeek
Home/Grow/Workspace Branding, Custom Domains & White-Label — Chat Prompts

Workspace Branding, Custom Domains & White-Label — Chat Prompts

⬅️ Back to 6. Grow

If your B2B SaaS has any customer-facing surface — email notifications, share links, customer portal, embedded widgets, public dashboards, customer-invitation flow — eventually a serious customer asks: "Can we put OUR logo on this? Can it use OUR domain? Can our customers see this without seeing your branding?" Three increasingly-deep asks: cosmetic branding (logo + colors), custom domains (portal.theircompany.com), and full white-label (Powered by [Product] removed, branding is theirs).

Each level unlocks bigger deals: cosmetic branding lands SMB self-serve customers happy ("our logo at the top"); custom domains land mid-market deals ("our brand on the URL"); full white-label unlocks reseller/embedder/PaaS deals ("our customers don't know we use [Product]"). Each adds engineering complexity exponentially: cosmetic is a logo upload + theme tokens; custom domain is automatic-cert-issuance + multi-domain routing; white-label is per-tenant branding across email, in-app, public surfaces, OAuth screens, error pages, and every email-from address.

This chat walks through implementing branding + custom domains + white-label as a tiered product feature: data model, theme system, custom domain provisioning + cert issuance, email branding, white-label policy, and the operational realities of multi-domain SaaS.

What you're building

  • Per-workspace theme (logo, primary color, favicon)
  • Theme application across in-app surfaces, emails, share pages
  • Custom domain support (portal.customer.com → your app)
  • Automatic TLS certificate issuance + renewal (Let's Encrypt / equivalent)
  • Per-domain routing (request → correct workspace)
  • Email branding (per-workspace from-address; SPF/DKIM/DMARC)
  • "Powered by [Product]" toggle (white-label tier)
  • Branded transactional emails
  • Customer-facing error pages with branding
  • Tier gating (which tier gets which level)

1. Decide tiers BEFORE building

Help me decide what shape of branding to ship.

Three increasingly-deep tiers. Pick what matches your business + customer demand:

TIER 1: COSMETIC BRANDING (the easiest 80% case)
- Customer uploads logo
- Customer picks 1-3 brand colors (primary, accent, optional)
- Logo + colors apply to: in-app sidebar, share pages, customer portal, in-product emails
- "Powered by [Product]" still visible
- URL is still yourco.com or yourdomain.app
- Pros: ships in 2-4 weeks
- Cons: customer URL still yours; not enough for some enterprise

TIER 2: CUSTOM DOMAIN
- Customer brings a domain: portal.customer.com or app.customer.com
- They CNAME to your edge (e.g., custom-domains.yourco.com)
- We issue + auto-renew TLS certs (Let's Encrypt / ACME)
- Login + share links + customer portal use customer domain
- Cosmetic branding from Tier 1 still applies
- "Powered by [Product]" usually still in footer
- Pros: huge deal-enabler for mid-market; perceived ownership
- Cons: TLS provisioning is real engineering; ongoing ops

TIER 3: FULL WHITE-LABEL
- Tier 2 + everything else customer-facing
- "Powered by [Product]" removed (everywhere)
- All emails: From: noreply@customer.com (with their SPF/DKIM)
- No yourco.com URLs in emails (all link back to portal.customer.com)
- Custom email templates with their branding
- Customer-portal favicon, page titles, meta tags
- OAuth consent screen branding (where applicable)
- Error pages with their branding
- Status pages (if exposed) with their branding
- Customer brand visible to YOUR support team's perspective too (when they handle ticket from white-label customer)
- Pros: opens reseller / embedded SaaS / PaaS deals (often 5-10x ARR per customer)
- Cons: 6-12 weeks of engineering; ongoing complexity tax

PRICING / TIER GATING:
- Cosmetic: usually included in Standard or Pro tier
- Custom Domain: usually Pro or Business tier (+$X/mo or bundled)
- White-Label: Enterprise only; often +50-100% premium

DEFAULT FOR MOST B2B SaaS:
- Year 1-2: Tier 1 (cosmetic; bundled with paid plans)
- Year 2-3: Tier 2 (custom domain; Pro tier and above)
- Year 3+: Tier 3 (white-label) when an enterprise/reseller customer pays for it

Don't pre-build Tier 3 without a buyer. Custom domains have ongoing ops cost; white-label has more.

Output: a written tier scope agreed by product/eng/sales.

Output: a clear tier scope; product/eng/sales aligned.

2. Build the theme system (Tier 1 — cosmetic branding)

Implement the theme system.

Schema:

workspace_branding (
  workspace_id     uuid pk
  logo_url         text  -- S3 / Blob; uploaded by customer
  logo_dark_url    text  -- optional dark-mode variant
  favicon_url      text
  primary_color    text  -- hex string '#RRGGBB'
  accent_color     text
  email_header_color text
  custom_css       text  -- optional; admin-only; sanitized
  custom_domain    text  -- nullable; for Tier 2
  remove_powered_by bool default false  -- for Tier 3
  email_from_name  text  -- for Tier 3
  email_from_email text  -- for Tier 3
  status           text  -- 'pending' | 'active' | 'failed'
  applied_at       timestamptz
)

Logo + favicon:
- Upload via standard S3-presigned-URL flow (see [File Uploads](./file-uploads-chat))
- Constraints: PNG/SVG; max 1MB; max 800px wide
- Auto-resize on upload to 64x64 favicon variants
- Validate: not malicious SVG (sanitize <script> tags)

Color tokens:
- Apply via CSS variables: --color-primary, --color-accent, --color-email-header
- Component library reads from variables (Tailwind: configure with var(...) in theme)
- Light/dark mode: variables change in dark mode

Where branding applies:
- In-app sidebar (logo + favicon)
- Share-link pages (header)
- Customer portal (entire UI)
- Email header (logo + colors)
- Public-facing pages owned by that workspace

Where it does NOT apply:
- Marketing site (yourco.com) — your brand
- Login page (yourco.com) — your brand
- Pricing / docs / blog — your brand
- Admin pages YOU access — your brand

Implementation:

A. CSS variables approach (cleanest):

:root {
  --color-primary: #2563eb;  /* default; customer overrides */
  --color-primary-foreground: #ffffff;
  --color-accent: #16a34a;
}

[data-workspace-theme="abc-uuid"] {
  --color-primary: #ff5500;  /* customer-set */
  --color-accent: #00bb55;
}

In React: <html data-workspace-theme={workspaceId}>...</html>
Server-side: inject <style> in <head> with workspace-specific overrides

B. Tailwind config:

theme: {
  colors: {
    primary: 'var(--color-primary)',
    accent: 'var(--color-accent)',
    // ...
  }
}

C. Email branding:
- Email templates use branded tokens
- Render at send time with workspace's colors + logo
- Include workspace logo as <img src="..." width=120 alt="..."> with absolute URL

Custom CSS (advanced):
- Admins paste custom CSS
- Sanitize: strip <script>, remove url(javascript:...), remove @import
- Apply only to scoped elements (don't break app shell)
- Document risks: "may break with future updates"

Validate accessibility:
- Color contrast check on submission
- Warn if primary color has poor contrast with light/dark backgrounds
- Don't block; just warn

Implement:
1. Schema migration
2. Logo upload flow
3. Color picker UI (workspace settings)
4. CSS variable injection (server + client)
5. Email template rendering with theme
6. Accessibility validation
7. Live preview ("here's what your share pages will look like")

Output: cosmetic branding that ships in 2-4 weeks.

3. Implement custom domains (Tier 2)

Add custom domain support. This is the engineering-heavy step.

Architecture:

Customer setup flow:
1. Customer enters domain in settings: portal.customer.com
2. We show: "CNAME portal.customer.com to custom-domains.yourco.com"
3. Customer adds CNAME at their DNS
4. We poll DNS until propagated (10 sec to 24 hrs)
5. We request TLS cert via ACME (Let's Encrypt)
6. We deploy cert to edge layer
7. Domain becomes 'active'
8. Customer's URL works

Schema additions:

workspace_custom_domains (
  workspace_id     uuid not null
  domain           text not null
  status           text  -- 'pending' | 'verifying' | 'cert_pending' | 'active' | 'failed' | 'archived'
  cname_target     text  -- 'custom-domains.yourco.com'
  verification_token text  -- optional TXT-record verification
  cert_issued_at   timestamptz
  cert_expires_at  timestamptz
  cert_renewal_attempts int default 0
  last_verified_at timestamptz
  error            text  -- on failure
  created_at       timestamptz
)

UNIQUE INDEX (domain) WHERE status='active'

Cert issuance options (pick one):

OPTION A: Use Vercel for Platforms / Cloudflare for SaaS / Fly.io custom domains
- These are managed services that handle ACME for you
- Vercel for Platforms is purpose-built for this; recommended in 2026
- You make API call: POST /domains; they handle the rest
- Pros: zero cert ops; minutes to ship
- Cons: vendor lock-in; pricing per domain

OPTION B: Caddy Server (self-managed, automatic TLS)
- Caddy automatically provisions Let's Encrypt certs
- Run as your edge layer (or behind Cloudflare)
- Configure: each tenant's domain → routes to your app
- Pros: free; battle-tested; no vendor lock-in
- Cons: you operate it; renewal monitoring needed

OPTION C: AWS API Gateway + ACM (or equivalent on GCP/Azure)
- Custom domain mapping in API Gateway
- ACM auto-renews certs
- Pros: AWS-native
- Cons: per-domain cost; more setup per customer

DEFAULT IN 2026: Vercel for Platforms (if you're on Vercel) OR Caddy self-hosted.

Routing flow:
1. Request hits edge: portal.customer.com/path
2. Edge identifies domain: looks up workspace_id
3. Edge proxies to your app with header: X-Workspace-Id: abc-uuid
4. App renders with that workspace's branding/data
5. Cookies scoped to portal.customer.com (NOT yourco.com)
6. Auth: separate session per domain (or cross-domain SSO; advanced)

Cookie + auth complexity:
- Cookies scoped to .customer.com (not .yourco.com)
- Login flow happens AT customer.com (not yourco.com)
- If user uses multiple workspaces with different custom domains: separate sessions
- DON'T set cookies for yourco.com from customer.com (would be a security issue)

Implementation:
1. Custom domain CRUD (settings UI)
2. CNAME verification job (polls DNS)
3. ACME / cert provisioning (via Vercel for Platforms / Caddy / etc.)
4. Edge routing (look up domain → workspace → proxy)
5. Per-domain cookie + session
6. Renewal monitoring + alerting (if cert renewal fails, notify customer + your ops)

Edge cases:
- Customer's DNS provider has caching delays (10 min to 24 hrs)
- Customer accidentally points root domain (customer.com → us); we should reject (only subdomains)
- Customer revokes the CNAME; cert renewal fails
- Customer sells the domain; we need to deprovision
- Domain expires; cert auto-renewal fails
- Multi-domain per workspace? (Pro tier: 1 domain; Enterprise: 5)

Error handling:
- Each step has clear customer-facing status
- Wizard shows where they are: "Step 2 of 4: Cert pending..."
- If failed: clear next step (Refresh DNS / Try again / Contact support)

Implement:
1. The custom domain settings page (with verification wizard)
2. The DNS polling logic
3. The cert issuance integration
4. The edge routing (Vercel middleware / Caddy config / equivalent)
5. The renewal monitoring
6. The cookie scoping logic
7. The deprovisioning flow

Anti-patterns:
- Manual cert issuance via support ticket (won't scale)
- Wildcard certs covering all customer domains (privacy issue)
- Not monitoring renewals (cert expires; customer outage)
- Allowing root domains (customer.com) — only subdomains
- Letting customer set redirect URIs that span domains

Output: working custom domains with auto-renewing TLS.

4. Implement email branding (per-workspace from-address)

For Tier 3 (white-label), email branding is the hardest piece.

Customer wants:
- From: noreply@customer.com (NOT noreply@yourco.com)
- Logo + colors from their brand
- Links to portal.customer.com (NOT yourco.com)

Implementation:

A. SPF / DKIM / DMARC setup
- Customer's emails come from your email provider (Resend / SendGrid / Postmark / SES)
- DNS records customer must add at their domain:
  - SPF: include:_spf.yourprovider.com
  - DKIM: provider-supplied CNAME (yourprovider gives unique key per customer)
  - DMARC: customer-controlled
- WITHOUT these, emails go to spam

B. Per-customer sender configuration

Most providers (Resend, SendGrid, Postmark) support:
- Custom domain authentication (verify customer's domain)
- DKIM signing on customer's behalf
- Per-domain rate limits

Setup flow:
1. Customer enters: "send emails from notifications@customer.com"
2. We provide DNS records to add (SPF, DKIM CNAMEs)
3. Customer adds them
4. We verify (provider's API)
5. Mark active
6. From now on, emails to customer's recipients use noreply@customer.com

C. Email template rendering

Templates parameterized by:
- workspace logo
- workspace primary color
- workspace.email_from_name + email_from_email
- workspace.custom_domain (for links)

Use a template engine (MJML, React Email, Postmark templates) that supports per-recipient theming.

Anti-spam considerations:
- Reputation: each customer's domain has its own sender reputation
- Throttling: customer's domain has its own rate limits
- Bounce handling: per-customer bounce rates tracked
- Customer admin can see: "you have a 6% bounce rate; investigate"

Failure modes:
- Customer's DNS records misconfigured: emails fail; alert customer
- Customer's domain reputation poor: spam filtering; alert customer
- Customer's domain expires: emails bounce; deactivate

Implement:
1. Per-workspace from-address configuration UI
2. DNS record verification flow
3. Email provider integration (Resend / Postmark with custom-domain support)
4. Per-customer bounce rate tracking
5. Templates that render with workspace branding + correct domains
6. Fallback to your default from-address if customer's not yet verified

Anti-patterns:
- Using your default From: for white-label customers ("noreply@yourco.com" leaks branding)
- Embedding yourco.com URLs in white-label emails
- Not handling bounce events per-customer
- One-size-fits-all rate limiting

Output: emails that look like they're from the customer.

5. Implement "Remove Powered By" + full white-label

Tier 3 finalization.

Surface scrub:
- Footer: "Powered by [Product]" hidden when workspace.remove_powered_by = true
- Login pages: customer's branding only (no yourco.com hints)
- Error pages: "Something went wrong on [Customer]" not "on [Product]"
- 404 pages: customer-branded
- Email signatures: scrubbed
- OAuth consent screen (if applicable): rebrand "Sign in with [Product]"
- Status messages: replace "[Product] is investigating" with "[Customer]"
- API responses (where applicable): scrub product references in error messages

Page metadata:
- <title>: customer-controlled
- <meta name="description">: customer-controlled
- <meta property="og:title">, og:image: customer-controlled
- favicon: customer's

Customer-portal admin UI:
- Tabs to set: Workspace name, logo, favicon, page title, meta description, og:image, error-page tagline

Edge cases:
- Customer-uploaded logo broken / 404: fallback to text-only "[Customer]"
- Customer's CSS breaks rendering: fallback to default theme
- Customer-set domain expires: fallback to yourco.com domain (warn customer first)

Customer support implications:
- Customer support emails received at noreply@customer.com may need forwarding to your support
- Customer's customers may write back to noreply@customer.com asking questions
- Set up email-forwarding for replies; route to customer's support team OR yours (per agreement)

Operational considerations:
- Internal tools: when YOUR support team views a white-label customer's tenant, you should still see "this is white-label customer X with brand Y" (don't get confused about whose tenant it is)
- Logging: capture white-label workspace_id in logs (don't lose context)
- Status page: have your status page show "Service X is degraded"; customer's branded portal shows "[Customer's product] is degraded" (separate copy)

Implement:
1. The "remove powered by" toggle and its enforcement across all surfaces
2. The custom-page-metadata system
3. The OAuth consent screen rebranding
4. The error page customization
5. The customer-support email routing
6. The logging + internal-tools separation

Anti-patterns:
- Removing "Powered by" without ensuring no leakage on every surface (one stray reference and trust breaks)
- Not maintaining your own status page separately
- Letting white-label customers confuse your support team about their tier
- Forgetting to scrub product references from error messages / API errors

Output: full white-label that customer security review accepts.

6. Build the operational + admin tooling

Multi-domain SaaS has operational complexity. Build the tooling.

Internal admin UI:
- List all custom domains across all workspaces
- Filter by status (pending / active / cert-failing / archived)
- Per-domain: cert expiry, last verified, renewal status
- Force-renew button (admin only)
- Force-archive button (admin only)
- Per-customer: which tier (cosmetic / custom-domain / white-label)

Monitoring:
- Cert-renewal-failing alert (any cert <7 days expiry without successful renewal)
- DNS-misconfigured alert (CNAME no longer points to us)
- Bounce-rate-spike alert (per-customer email reputation)
- White-label-leakage alert (heuristic: detected yourco.com reference in white-label customer's surface)

Customer-facing audit log:
- Custom domain added / verified / archived
- Branding changed (logo / colors)
- Email-from-address changed
- White-label toggle changed

Per-tier policy enforcement:
- Free / Standard tier: no custom domain (block UI; show upsell)
- Pro tier: 1 custom domain
- Enterprise tier: 5 custom domains + white-label

Customer-facing analytics:
- Per-domain traffic counts
- Email send + bounce + delivery stats
- "Your branding is applied to X surfaces" summary

Implement:
1. Internal admin UI for domain + branding management
2. Monitoring + alerting jobs
3. Customer-facing audit log
4. Tier-policy enforcement
5. Customer-facing analytics
6. Health checks (every 1 hour for active domains)

Output: operational maturity that scales beyond manual.

7. Edge cases + failure modes

Walk me through edge cases:

1. Customer points root domain (customer.com) to us
   - Reject: only subdomains allowed (portal.customer.com)
   - Document why (root-domain CNAME + email conflicts)

2. Customer has CDN in front (Cloudflare orange-cloud)
   - CNAME-flattening / orange-cloud breaks our setup
   - Document: "set CNAME with grey cloud (DNS-only)"
   - Or: support Cloudflare-for-SaaS integration (advanced)

3. Multiple workspaces share a domain (oops)
   - Hard-error on add: domain already in use
   - Show: "this domain is registered by another workspace"

4. Customer migrates the domain elsewhere
   - Cert renewal fails
   - Detect via DNS check
   - Mark domain failed; notify customer; archive after 7 days

5. Customer wants HTTP redirect (http://customer.com → https://customer.com)
   - Redirect at edge layer
   - HSTS header: enforce HTTPS

6. SEO concerns with white-label
   - Each customer's branded portal could rank
   - Add canonical URLs to prevent duplicate-content issues
   - For purely-private portals: noindex by default

7. Email deliverability tanks for one customer
   - Their domain reputation low; their emails go to spam
   - Alert their admin
   - Suggest: warmup, list cleaning, content review

8. Customer's domain expires
   - Cert renewal fails; emails bounce; customer's customers see broken portal
   - High-priority alert to customer admin
   - 7-day grace; then archive

9. Customer requests cert for HTTPS-only domain
   - Our default; correct

10. White-label customer asks to remove ALL product mentions
    - Audit every surface; doc every reference
    - Common misses: error pages, OAuth screens, API error messages, email signatures, mobile app titles

11. Custom domain on a country-specific TLD (.de, .uk, .jp)
    - Some country TLDs have CNAME restrictions; document
    - Some have unicode (xn--...); ensure punycode handling

12. Customer wants their brand visible to YOUR support team
    - Internal tools should show: "this is workspace X, brand Y, white-label tier"
    - Don't let support team accidentally reveal your product to customer

13. Switching tiers mid-contract
    - Customer downgrades from white-label to standard
    - Cert + custom domain remain (Pro tier)
    - Powered-by re-appears; email-from reverts
    - Document the switch

14. CDN / edge cache poisoning
    - Multi-domain caching: ensure cache key includes domain
    - Don't serve customer A's branded pages to customer B's URL

For each, walk me through code change + customer comms + ops impact.

Output: ops that survive edge cases.

8. Recap

What you've built:

  • Tier 1: cosmetic branding (logo, colors, favicon)
  • Tier 2: custom domain (CNAME + auto-cert)
  • Tier 3: full white-label (email branding, "powered by" removal)
  • Per-tier feature gating
  • Cert renewal monitoring + alerting
  • Email reputation tracking per customer
  • Internal admin tooling
  • Customer-facing audit + analytics
  • Operational runbooks for cert failures, domain expiry, etc.

What you're explicitly NOT shipping in v1:

  • Custom mobile apps per customer (different feature; massive scope)
  • Custom OAuth provider per customer (uses your OAuth)
  • Customer's own login screen with SSO (different feature; existing SSO chat)
  • Per-customer documentation portal (different scope)
  • Cross-domain SSO between yourco.com and customer.com (advanced; defer unless needed)
  • IPv6 support per domain (most ACME / cert providers handle automatically)

Ship Tier 1 in 2-4 weeks; Tier 2 in 6-12 weeks; Tier 3 only when an enterprise customer is paying for it.

The biggest mistake teams make: pre-building Tier 3 (full white-label) before any customer pays for it. White-label is a 6-12 week investment + ongoing complexity. Charge before you build.

The second mistake: skipping cert-renewal monitoring. Cert expires → customer site down → customer churn signal. Monitor every active cert; alert at 14/7/3 days.

The third mistake: leaking your branding from white-label customers. One stray reference (error page, email signature, OAuth screen) and the customer's customers see it; trust gone.

See Also