Support chat
Render a live-chat widget via Crisp or Chatwoot, gated by config.support and aware of the logged-in user.
config.support (set in packages/config/src/support.mjs, re-exported by @repo/config) 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 - values you add. Until you set one, no widget loads. Configure one provider; both keys active would load two widgets. If your project ships the desktop app, it mounts the same widget from the same values - configure once, every surface gets live chat.
Configuration
| Key | Type | Default | Description |
|---|---|---|---|
support.crisp.websiteId | string | unset | Crisp Website ID from the Crisp dashboard. Presence of crisp enables the Crisp widget. |
support.chatwoot.baseUrl | string | unset | Chatwoot installation URL - https://app.chatwoot.com for cloud or your self-hosted URL. The widget script loads from this origin. |
support.chatwoot.websiteToken | string | unset | Chatwoot website inbox token (Inboxes -> Settings -> Configuration). Presence of chatwoot enables the Chatwoot widget. |
support.enableInDev | boolean | false | Load the widget during local development. When unset it falls back to false, so the widget would not load in dev. |
Values live in the plain-JS packages/config/src/support.mjs (no env reads - the desktop app imports it directly, so web and desktop can never disagree). A configured supportConfig with Crisp:
// packages/config/src/support.mjs
export const supportConfig = {
enableInDev: true,
crisp: {
websiteId: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
};Or with Chatwoot:
// packages/config/src/support.mjs
export const supportConfig = {
enableInDev: true,
chatwoot: {
baseUrl: "https://support.example.com",
websiteToken: "xxxxxxxxxxxxxxxxxxxxxxxx"
}
};Behavior behind the flag
| Scenario | Result |
|---|---|
| Configured | Bubble renders once crisp.websiteId or chatwoot.websiteToken is set. |
| Off | Remove the provider sub-key (or unset config.support) to hide it entirely. |
| Dev | Loads when enableInDev is truthy; set it false to suppress it during development. |
| Logged-in user (Crisp) | Identified to Crisp on session change - email and name only, no user-id tag - so agents see who they are chatting with. Re-identified when the user changes. |
| Logged-in user (Chatwoot) | Identified via $chatwoot.setUser(userId, { email, name }) on session change, deferred until the SDK is ready. Re-identified only when the user ID changes; reset on sign-out. |
Consent
Crisp and Chatwoot drop cookies, so under cookieBanner: "auto" their presence surfaces the consent banner. The widget itself is not blocked behind consent - it loads as soon as it is configured. See cookie consent for how "auto" decides when the banner appears.
Desktop app
When your project ships the desktop app, it mounts the same widget from the same config.support - no separate setup. The packaged renderer's strict Content-Security-Policy is extended at build time with exactly the configured provider's origins (Crisp's documented origin set, or your Chatwoot installation's origin); with no provider configured the policy stays fully locked down. The signed-in desktop user is identified to the provider and reset on sign-out, mirroring the web behavior. enableInDev gates the widget in pnpm dev the same way it does on the web.
Frequently asked questions
Which providers are supported?
Crisp and Chatwoot. Each is a sub-key of config.support; its presence is the provider switch.
Can I use a self-hosted Chatwoot?
Yes - point chatwoot.baseUrl at your installation. The widget SDK (/packs/js/sdk.js) loads from that origin, so cloud and self-hosted behave identically.
Does the chat wait for cookie consent like analytics do?
No. Both providers surface the banner under "auto", but the widget loads immediately once configured - it is not gated on the visitor's choice.
Is the user's chat history preserved across sessions?
Crisp ties conversations to the pushed email, so a returning logged-in user resumes the same thread once identified - no user-id tag is set. Chatwoot ties them to the setUser identifier (the user ID) the same way.
Does Chatwoot identity validation (identifier_hash) work?
Not out of the box - the identify call sends no HMAC, so leave "Enforce user identity validation" disabled in the inbox settings, or extend the identify call with a server-signed identifier_hash yourself.