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 moreShow fewer
- Verified identification needs an auth battery, so selecting Crisp pulls one in.
src/lib/support/identity.tsreadsgetSessionUser()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.
- 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 matchingtoken|secret|password|authorization|cookie|api_key|session|jwt|card|cvvand values shaped like credentials, and warns in development. Never callwindow.$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 fromsignIdentity(), so Crisp marks the conversation verified. - That helper reads the session through
@/lib/auth/sessionand nothing else. Never import an auth SDK fromsrc/lib/support/**: the support layer works with whichever auth battery is installed precisely because it does not know which one that is. SessionUser.emailand.nameare 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
localStorageand push it as the user's identity. That is how a support team ends up acting on an impersonated address. - Without
CRISP_IDENTITY_SECRETset, 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_prois a segment,user_1a2b3cis 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, throughnext/scriptwithstrategy="lazyOnload". Never paste Crisp's own snippet into a layout, a<head>, an inline<script>ordangerouslySetInnerHTML.lazyOnloadis the strategy, deliberately. It downloads during browser idle time, after hydration.beforeInteractiveputs a third-party script ahead of your own application code. Indefensible for a chat bubble.afterInteractivestill 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 rendersnulluntilcrispLoaded()is true. If the script never arrives (an extension blocked it, the CDN is down) the product must be entirely unaffected.
Consent gates the render, not the visibility
- Crisp writes cookies and a session identifier the moment
l.jsexecutes. 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()fromsrc/lib/support/consent.tsis the only source of that decision, andCONSENT_MODEis 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_ROUTESincrisp-widget.tsxlists 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 callsshowLauncher(). 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.tsxand every component that reads consent are client components. Consent lives inlocalStorage, 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 exceptidentity.ts, which is server-only by design and readsCRISP_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.
- Consent gating a chat widget without breaking itChat sets cookies before anyone types a word, so hiding the launcher is not compliance. Gate the script tag itself, make withdrawal real, and know why a reload is the only honest teardown.docs/solutions/crisp/gdpr-consent-gating.md
- Keeping the chat bubble off the pages that need a clean herohide and show are visibility commands, not load control. Gate the script by route, hide on navigation, and give marketing pages a trigger you designed instead.docs/solutions/crisp/hiding-the-widget-on-marketing-pages.md
- Identifying signed-in users in chat without letting anyone impersonate themAn email set from the browser is a claim anyone can make. Sign it server-side with HMAC, pass the signature through, and give your support team a label they can act on.docs/solutions/crisp/identifying-signed-in-users-safely.md
- Loading a chat widget without wrecking your Core Web VitalsThe vendor snippet is a synchronous script in the head. Move it to next/script with lazyOnload, measure the difference, and keep the click working while it downloads.docs/solutions/crisp/loading-chat-without-hurting-lcp.md
- Routing support conversations with segments and session dataA shared inbox where everything looks the same is a queue, not a support system. Segments route, session data answers the first three questions, and both belong in code.docs/solutions/crisp/routing-conversations-with-segments.md
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.