Skip to main content
vibestrap ships with a <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

  1. Pick a provider in src/config/site.ts:
    customerService: {
      enable: true,
      provider: 'crisp', // 'crisp' | 'tawk' | 'intercom' | 'chatwoot'
    },
    
  2. Add the matching env vars to .env.local. All are NEXT_PUBLIC_* — they need to ship to the browser so the loader script can boot.
    # Crisp
    NEXT_PUBLIC_CRISP_WEBSITE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
    
    # Tawk.to (both required)
    NEXT_PUBLIC_TAWK_PROPERTY_ID=000000000000000000000000
    NEXT_PUBLIC_TAWK_WIDGET_ID=000000000
    
    # Intercom
    NEXT_PUBLIC_INTERCOM_APP_ID=abcd1234
    
    # Chatwoot (token required, base URL defaults to app.chatwoot.com)
    NEXT_PUBLIC_CHATWOOT_WEBSITE_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxx
    NEXT_PUBLIC_CHATWOOT_BASE_URL=https://app.chatwoot.com
    
  3. Allow-list your domain in the provider’s dashboard (Crisp → Settings → Website Settings, Tawk → Property → Widget → Restrictions, etc).
  4. Restart pnpm dev — public env vars are baked at build time.

Pick the right provider

ProviderWhen to pick
CrispGenerous free tier (2 seats, unlimited contacts), nice dashboard, fastest setup. Good default for indie hackers.
Tawk.toFully free forever — no plan limits at all. Trade-off: branded widget unless you pay $19/mo to remove it.
IntercomEnterprise standard. Pick this if you have a real support team and want product tours, ticketing, and outbound campaigns.
ChatwootOpen-source, self-hostable. Pick this if you need data sovereignty or want to host the chat backend on your own infra.

Verify it works

  1. Open the site in an incognito window — the bubble should appear bottom-right within ~1 second of page load.
  2. Send a test message; check the inbox in the provider’s dashboard.
  3. Visit /login or /register — the widget is intentionally hidden on auth pages (the <CustomerService /> component lives in the app layout, not the auth layout).
  4. View source: you should see exactly one loader script tag for your provider and zero for the others.

Common pitfalls

  1. 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.
  2. 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).
  3. CSP blocking the script. If you’ve added a Content-Security-Policy header, add the provider’s CDN to script-src and connect-src (e.g. https://client.crisp.chat, https://embed.tawk.to, https://widget.intercom.io, your Chatwoot base URL).
  4. Two widgets at once. Setting enable: true plus a stale env var from a previous provider doesn’t double-render — the switch in src/customer-service/index.tsx only mounts the active one. But leftover env vars are harmless; clean them up.
  5. 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:
  1. Add the provider to the union in src/customer-service/types.ts.
  2. Add a new case to the switch in src/customer-service/index.tsx.
  3. Add the env var to src/env.ts under client.NEXT_PUBLIC_*.

Official docs