GenerateSaaS

Support chat

Render a Crisp or Chatwoot live-chat widget from config.support, identified to the signed-in user, on both web and the desktop app.

config.support mounts a client-side live-chat widget. Two providers are wired - Crisp via config.support.crisp and Chatwoot (cloud or self-hosted) via config.support.chatwoot. Until you set one, no widget loads; configure exactly one, since two active keys would load two widgets.

Configuration

Values live in packages/config/src/support.ts, which the config index imports back as config.support - one definition, so web and desktop can never disagree.

KeyTypeDefaultDescription
support.crisp.websiteIdstringunsetCrisp Website ID from the Crisp dashboard. Presence of crisp enables the Crisp widget.
support.chatwoot.baseUrlstringunsetChatwoot installation URL - https://app.chatwoot.com for cloud, or your own. The widget script loads from this origin.
support.chatwoot.websiteTokenstringunsetChatwoot website inbox token (Inboxes → Settings → Configuration). Presence of chatwoot enables the Chatwoot widget.
support.enableInDevbooleanfalseLoad the widget during local development.
// packages/config/src/support.ts - Crisp
export const supportConfig = {
  enableInDev: true,
  crisp: { websiteId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }
};

// ...or Chatwoot
export const supportConfig = {
  enableInDev: true,
  chatwoot: {
    baseUrl: "https://support.example.com",
    websiteToken: "xxxxxxxxxxxxxxxxxxxxxxxx"
  }
};

support.ts sits outside the config index because the desktop build reads it under plain Node to whitelist widget origins in the packaged Content-Security-Policy. Keep any relative import in it type-only.

Behavior

ScenarioResult
ConfiguredThe bubble renders once crisp.websiteId or chatwoot.websiteToken is set.
OffRemove the provider sub-key (or unset config.support) to hide it entirely.
DevLoads only when enableInDev is truthy.
Logged-in user (Crisp)Identified on session change - email, name, and a user-id tag - so agents see who they are chatting with. Re-pushed only when the user ID changes.
Logged-in user (Chatwoot)Identified via $chatwoot.setUser(userId, { email, name }) on session change, deferred until the SDK is ready. Re-identified when the user ID changes; reset on sign-out.
  • Self-hosted Chatwoot behaves identically to cloud: point chatwoot.baseUrl at your installation and the SDK loads from ${baseUrl}/packs/js/sdk.js.
  • Both providers reset on sign-out, so the next visitor on a shared browser is never attributed to the previous user.

Chatwoot identity validation (identifier_hash) is not implemented - the identify call sends no HMAC. Leave "Enforce user identity validation" disabled in the inbox settings, or extend the identify call with a server-signed hash yourself.

Crisp and Chatwoot drop cookies, so under cookieBanner: "auto" their presence surfaces the consent banner. The widget itself is not held back - it loads as soon as it is configured, regardless of the visitor's choice. See cookie consent for how "auto" decides.

Beside the chat dock

The AI chat dock is anchored to the same bottom-right corner as the widget, so an open dock would otherwise sit under the bubble.

  • The dock publishes its live width on <html>, and @repo/styles/globals.css shifts the widget - launcher and open chat window alike - just clear of it, tracking a resize drag.
  • Closed, or with no provider configured, nothing is published and each widget sits exactly where its vendor puts it.
  • SUPPORT_WIDGET_CLEARANCE in packages/ui/src/lib/support-widget-offset.ts is the room always kept free in the corner - raise it if you have widened your widget. The shift is capped so the widget's own chat window is never pushed off-screen.

Desktop app

A project that ships the desktop app mounts the same widget from the same config.support - no separate setup, same enableInDev gating, same identify-and-reset behavior.

  • The packaged renderer's strict Content-Security-Policy is extended at build time with exactly the configured provider's origins; with no provider configured it stays fully locked down.
  • To run live chat on your website but not in the app, set supportChat: false in config.desktop (packages/config/src/desktop.mjs). The app then mounts no widget and drops the provider's origins from its policy. The website is untouched.

On this page