Skip to content

Customer support

Next.js boilerplate with Crisp

Live chat and a shared inbox from one script tag, kept off your critical path.

The Crisp chat widget, loaded through next/script at idle and behind a consent gate. A typed wrapper covers the $crisp command queue. Identification is HMAC-verified and read from the shared @/lib/auth/session surface. Route-based hiding keeps it off your marketing hero.

What Crisp adds to the agent layer: 2 rules · 1 skill · 5 solution docs

Maintained by @raviMITNext.js on Vercel

From the manifest

Should you pick Crisp?

Pick it if

Small teams who need a real support inbox this week and do not want to run one. Chat and email land in one shared inbox, with a mobile app. Setup is one website id in an env var.

Watch out for

  • It is a third-party script on your pages, plus a websocket. Loaded at idle it stays out of your metrics. Loaded eagerly it shows up in LCP and INP.
  • The API is a command queue, not an SDK. $crisp.push(["do", "chat:open"]) is stringly typed and fails silently on a typo, which is why this battery wraps every call it uses.
Show 4 more
  • Verified identification needs an auth battery, so selecting Crisp pulls one in. src/lib/support/identity.ts reads getSessionUser() from @/lib/auth/session, never an auth SDK, so which one you pick makes no difference here.
  • Cookies and a session id are set the moment the script runs. Where consent is required, the widget cannot load before the user agrees. The gate is not optional, and adding it later means auditing what already shipped.
  • Routing, tagging and automation are simpler than in Intercom or Zendesk. Fine for a team of three. A support org of thirty will outgrow them.
  • Email identification is only trustworthy with HMAC verification on. Without it, anyone can set any email in the chat and pose as a customer to your support team.

What it costs

Free plan with 2 seats and the chat widget. Mini is $45/month with 4 seats, Essentials $95/month with 10 and Plus $295/month with 20. Each extra seat on a paid plan is $10/month.

Prices change. Check with Crisp before you commit.

registry/tested.yaml

Tested with Crisp

Each pair was installed, typechecked, linted, built and booted together.

Database
NeonSupabase
Admin panel
Admin panel
Error tracking
Sentry

What it adds

What Crisp adds to the repo

Read straight from the crisp manifest, so it is exactly what lands in your repo.

Environment variables

  • NEXT_PUBLIC_CRISP_WEBSITE_IDRequiredPublic, reaches the browser

    Crisp website id: a uuid, public by design, visible in the page source of every site using Crisp. It identifies the inbox, not the account, and holds no privileges on its own.

    Where to get it
    Crisp -> Settings -> Website Settings -> Setup instructions (the id in the snippet)
    Placeholder
    00000000-0000-4000-8000-000000000000
  • CRISP_IDENTITY_SECRETOptional

    HMAC-SHA256 secret used to sign a signed-in user's email so Crisp marks the conversation as verified. Server-side only. Without it, an email set from the browser is a claim anyone can make: see the cookbook doc on identifying signed-in users.

    Where to get it
    Crisp -> Settings -> Website Settings -> Chatbox & Email Security -> Email verification
    Placeholder
    0000000000000000000000000000000000000000000000000000000000000000

Dependencies

  • server-only^0.0.1

Files it writes

5 files, at these exact paths.

  • src/5 files
    • lib/5 files
      • support/5 files
        • consent.ts
        • crisp-widget.tsx
        • crisp.ts
        • identity.ts
        • support-button.tsx

Stack slots it fills

The stack declares these injection points; this battery supplies the fragment, so the provider tree, the env check and the schema stay one file each instead of many.

  • @slot env-required
  • @slot legal-processors
  • @slot providers

The differentiator

What Crisp teaches your agent

Other starter kits stop at the package. This is the part an agent reads: where it may work, what it must never do there, and the problems someone already solved.

Rules (2)

Loaded when the agent opens a matching file.

Nothing secret goes into a support conversation

Loads onsrc/lib/support/**src/app/**src/components/**.claude/rules/chat-context.md
What a chat context actually is

Everything pushed with session:data, session:segments or user:* is stored by Crisp, shown to every operator in the workspace, retained with the conversation, and forwarded to whatever integrations that workspace has enabled: Slack, email, a CRM, an automation. Treat it as a third-party log with a friendly UI.

Never push a credential
  • No session tokens, JWTs, API keys, signed URLs, password reset links, one-time codes, card numbers or cookie values. Not "temporarily", not "for debugging", not in a segment name.
  • Always go through setSessionData() from @/lib/support/crisp. It strips keys matching token|secret|password|authorization|cookie|api_key|session|jwt|card|cvv and values shaped like credentials, and warns in development. Never call window.$crisp.push(["set", "session:data", ...]) directly to get around it.
  • Do not spread an object in: setSessionData({ ...user }) ships every field the type gains later, including the ones added after this line was written. Name the three or four keys an operator actually needs.
  • If an operator needs to see something sensitive to help, they look it up in your admin panel behind their own login. That is what the admin panel is for, and it leaves an audit trail Crisp cannot.
Identify only from the server
  • An email set from the browser is a claim. Pass an identity resolved on the server by currentSupportIdentity(), with the HMAC signature from signIdentity(), so Crisp marks the conversation verified.
  • That helper reads the session through @/lib/auth/session and nothing else. Never import an auth SDK from src/lib/support/**: the support layer works with whichever auth battery is installed precisely because it does not know which one that is.
  • SessionUser.email and .name are both nullable. Push each only when it is there: an account with no address gets an anonymous conversation, never one labelled with a placeholder.
  • Never read an email out of a form field, a query parameter or localStorage and push it as the user's identity. That is how a support team ends up acting on an impersonated address.
  • Without CRISP_IDENTITY_SECRET set, every address in the inbox is unverified. Say so in your support runbook and never action an account change on the strength of an unverified label.
  • Call resetSession() on sign-out, before the redirect. Otherwise the next person on that browser opens the previous user's conversation history.
Minimal by default
  • Useful context is small and stable: plan, account id, app version, the page the chat was opened from, the feature area. That removes the first three questions of a support conversation and nothing more.
  • No free text a user typed: a search query or a note can contain anything, including a third party's personal data.
  • No internal identifiers that mean nothing to an operator. If nobody in support can act on a field, it is noise that will still be there in a year.
Segments are routing, not data
  • Segments drive who picks a conversation up and which automations fire. Keep them few, lowercase and stable: billing, onboarding, enterprise.
  • Never encode a value in a segment: plan_pro is a segment, user_1a2b3c is a leak dressed as routing.

The support widget loads late, and never before consent

Loads onsrc/lib/support/**src/app/**.claude/rules/widget-loading.md
One loader, and it is <CrispWidget />
  • Crisp is loaded in exactly one place: src/lib/support/crisp-widget.tsx, through next/script with strategy="lazyOnload". Never paste Crisp's own snippet into a layout, a <head>, an inline <script> or dangerouslySetInnerHTML.

  • lazyOnload is the strategy, deliberately. It downloads during browser idle time, after hydration.

    • beforeInteractive puts a third-party script ahead of your own application code. Indefensible for a chat bubble.
    • afterInteractive still competes with your JavaScript for the main thread during the most sensitive part of the load.
    • A bare <script src> in a layout blocks parsing and lands directly in LCP.
  • Nothing else may block on Crisp. No await, no spinner waiting for it, no component that renders null until crispLoaded() is true. If the script never arrives (an extension blocked it, the CDN is down) the product must be entirely unaffected.

  • Crisp writes cookies and a session identifier the moment l.js executes. Hiding the launcher afterwards does not undo that.
  • Therefore: when consent is required and not granted, the <Script> element is not rendered at all. hasSupportConsent() from src/lib/support/consent.ts is the only source of that decision, and CONSENT_MODE is the only place the policy is set.
  • Never load the widget "just for signed-in users" as a consent workaround. Being signed in is not consent to third-party tracking cookies.
  • Consent must be as easy to withdraw as to give. revokeSupportConsent() reloads the page, because Crisp has no teardown API once it is running.
Route rules
  • HIDDEN_ROUTES in crisp-widget.tsx lists the pages where the launcher must not appear: the marketing home page, sign-in, sign-up, pricing. A floating bubble competes with the one action those pages exist for.
  • Navigating onto a hidden route calls hideLauncher(); navigating off calls showLauncher(). Neither unloads the script, because nothing can.
  • On pages where support belongs but the bubble does not, use <SupportButton /> and keep the launcher hidden.
Hydration
  • crisp-widget.tsx and every component that reads consent are client components. Consent lives in localStorage, which does not exist on the server, so it is read in an effect after mount, never during render, which would make the server and client markup disagree.
  • Never render different markup on the server and client based on typeof window. That is a hydration error, and in production React silently keeps the server output, so the bug only appears for real users.
  • Nothing in src/lib/support/** may be imported by a Server Component except identity.ts, which is server-only by design and reads CRISP_IDENTITY_SECRET.

Skills (1)

Invoked by name.

  • /add-support-trigger

    Add a contextual "talk to support" trigger (a button, a link or an error-state action) that opens Crisp with the right context and segment.

    .claude/skills/add-support-trigger/SKILL.md

Solution docs (5)

Written before you hit the problem. Each one ships in the repo at docs/solutions/ and is published here as a cookbook page.

How it fits

What Crisp needs, and what it goes well with

The resolver enforces this before it generates anything, and names every addition it makes.

Requires

  • An auth battery. The resolver adds the default one for you and tells you why.

Pairs well with

Nothing extra. Add any tested battery alongside Crisp.

Cannot be combined with

No hard conflicts.

Build a repo with Crisp

Free and MIT. The builder opens with Crisp picked. You download the zip right away, and we email you the link too.