<CustomerService /> mount in the locale layout that
delegates to one of four providers. Flip the config switch, set the matching
public env var, and the bubble appears in the bottom-right of every page.
Providers self-gate on their env vars, so an unconfigured one renders nothing
instead of crashing the page.
Prerequisites
- An account with one of: Crisp, Tawk.to, Intercom, or Chatwoot.
- The site/property/app ID from that provider’s dashboard.
- Your production domain allow-listed in the provider’s settings (most reject embedded widgets from unknown origins).
Step-by-step
-
Pick a provider in
src/config/site.ts: -
Add the matching env vars to
.env.local. All areNEXT_PUBLIC_*— they need to ship to the browser so the loader script can boot. - Allow-list your domain in the provider’s dashboard (Crisp → Settings → Website Settings, Tawk → Property → Widget → Restrictions, etc).
-
Restart
pnpm dev— public env vars are baked at build time.
Pick the right provider
| Provider | When to pick |
|---|---|
| Crisp | Generous free tier (2 seats, unlimited contacts), nice dashboard, fastest setup. Good default for indie hackers. |
| Tawk.to | Fully free forever — no plan limits at all. Trade-off: branded widget unless you pay $19/mo to remove it. |
| Intercom | Enterprise standard. Pick this if you have a real support team and want product tours, ticketing, and outbound campaigns. |
| Chatwoot | Open-source, self-hostable. Pick this if you need data sovereignty or want to host the chat backend on your own infra. |
Verify it works
- Open the site in an incognito window — the bubble should appear bottom-right within ~1 second of page load.
- Send a test message; check the inbox in the provider’s dashboard.
- Visit
/loginor/register— the widget is intentionally hidden on auth pages (the<CustomerService />component lives in the app layout, not the auth layout). - View source: you should see exactly one loader script tag for your provider and zero for the others.
Common pitfalls
- Missing
NEXT_PUBLIC_prefix. Anything the browser reads needs the prefix; otherwise Next.js strips it from the bundle and the widget silently fails to mount. - Forgot to allow-list your domain. Most providers reject the widget on unknown origins (you’ll see an empty bubble or a CORS error in devtools).
- CSP blocking the script. If you’ve added a Content-Security-Policy
header, add the provider’s CDN to
script-srcandconnect-src(e.g.https://client.crisp.chat,https://embed.tawk.to,https://widget.intercom.io, your Chatwoot base URL). - Two widgets at once. Setting
enable: trueplus a stale env var from a previous provider doesn’t double-render — the switch insrc/customer-service/index.tsxonly mounts the active one. But leftover env vars are harmless; clean them up. - Widget on auth pages. It’s hidden by design (separate layout). If you
want it everywhere, mount
<CustomerService />in the root layout instead of the app layout.
Add a new provider
The pattern is intentionally tiny — a single client component that reads its env var and returns a<Script> tag (or null when unset). Look at any of
src/customer-service/provider/*.tsx for the shape, then:
- Add the provider to the union in
src/customer-service/types.ts. - Add a new
caseto the switch insrc/customer-service/index.tsx. - Add the env var to
src/env.tsunderclient.NEXT_PUBLIC_*.
Official docs
- Crisp: docs.crisp.chat
- Tawk.to: help.tawk.to
- Intercom: intercom.com/help/en
- Chatwoot: chatwoot.com/docs