Why Crisp?
Building a lovable product starts with a tight feedback loop with your customers, especially early, before product-market fit. Embedding a live conversation directly inside the app is a cheat code: it turns curiosity into momentum, objections into insights, and free users into paying advocates. Personally, it has helped me convert dozens of prospects; you won't find a hotter lead than someone already using your product on the free plan, just a couple of clicks away from upgrading. For that, I've relied on Crisp for years. It's simple, reliable, and keeps the conversation where it matters: in your product.
Customer support is crucial for any application, but implementing it shouldn't take hours of development time.
In this article, I'll show you how to integrate Crisp into your Next.js application with a single prompt that generates all the necessary code.
The Challenge
Integrating third-party chat widgets often involves:
- Managing script loading and SSR compatibility
- Handling user authentication state using NextAuth.js
- Creating type-safe interfaces
- Building reusable components to handle the chat widget (open/close, send messages, etc.)
The Solution: One-Prompt Installation
Instead of spending hours implementing and debugging, use this comprehensive prompt with your AI assistant to generate production-ready Crisp integration code:
I need to integrate Crisp into my Next.js application with TypeScript.
Reference implementation: https://www.dopamine.dev/blog/crisp-chat-single-prompt-installation
Please create a complete, production-ready integration that includes:
1. Core Components Structure:
- A type-safe context for managing Crisp state
- A provider component that handles initialization
- A custom hook for consuming Crisp functionality
- SSR-safe initialization component
- TypeScript types and interfaces
2. API Requirements:
The useCrisp hook should provide:
- `openChat()` - Opens the chat widget
- `closeChat()` - Closes the chat widget
- `sendMessage(message: string)` - Sends a message
- `isReady: boolean` - Crisp initialization status
- `isChatOpen: boolean` - Current chat state
- `setUserEmail(email: string)` - Updates user email
- `setUserNickname(name: string)` - Updates user nickname
3. Implementation Details:
- Use `crisp-sdk-web` package for the integration
- Use Next.js dynamic imports for SSR safety
- Integrate with NextAuth.js for automatic user data
- Environment variable `NEXT_PUBLIC_CRISP_WEBSITE_ID` for configuration
- Create components in `src/components/crisp/` directory
4. File Structure:
src/components/crisp/
├── index.ts # Main exports
├── types.ts # TypeScript definitions
├── crisp-context.tsx # React Context
├── crisp-provider.tsx # Provider with logic
├── crisp-initializer.tsx # SSR-safe initializer
└── use-crisp.ts # Custom hookComplete Implementation
Here's the production-ready code that the AI prompt generates:
Package Dependencies
First, install the required package:
npm install crisp-sdk-webEnvironment Configuration
Add your Crisp Website ID to your environment variables:
# .env.local
NEXT_PUBLIC_CRISP_WEBSITE_ID=your-website-id-hereYou can find your Website ID in Crisp Settings → Setup & Integrations.
Core Types (src/components/crisp/types.ts)
export type CrispContextType = {
// Actions
openChat: () => void;
closeChat: () => void;
sendMessage: (message: string) => void;
// State
isReady: boolean;
isChatOpen: boolean;
// User data management
setUserEmail: (email: string) => void;
setUserNickname: (name: string) => void;
};
export type CrispUser = {
email?: string;
nickname?: string;
};React Context (src/components/crisp/crisp-context.tsx)
'use client';
import { createContext } from 'react';
import type { CrispContextType } from './types';
export const CrispContext = createContext<CrispContextType | null>(null);SSR-Safe Initializer (src/components/crisp/crisp-initializer.tsx)
'use client';
import { useEffect, useRef } from 'react';
import { Crisp } from 'crisp-sdk-web';
import { useSession } from 'next-auth/react';
type CrispInitializerProps = {
onReady: () => void;
onChatToggle: (isOpen: boolean) => void;
};
/**
* CrispInitializer handles the initialization and management of Crisp chat
* This component is SSR-safe and only runs client-side
*/
export default function CrispInitializer({ onReady, onChatToggle }: CrispInitializerProps) {
const { data: session } = useSession();
const isInitialized = useRef(false);
// Initialize Crisp once
useEffect(() => {
if (isInitialized.current) return;
const websiteId = process.env.NEXT_PUBLIC_CRISP_WEBSITE_ID;
if (!websiteId) {
console.warn('NEXT_PUBLIC_CRISP_WEBSITE_ID is not defined');
return;
}
try {
// Configure Crisp
Crisp.configure(websiteId);
// Set up event listeners
Crisp.chat.onChatOpened(() => onChatToggle(true));
Crisp.chat.onChatClosed(() => onChatToggle(false));
isInitialized.current = true;
onReady();
} catch (error) {
console.error('Failed to initialize Crisp:', error);
}
}, [onReady, onChatToggle]);
// Update user data when session changes
useEffect(() => {
if (!isInitialized.current || !session?.user) return;
const { email, name } = session.user;
if (email) Crisp.user.setEmail(email);
if (name) Crisp.user.setNickname(name);
}, [session?.user]);
return null; // This component renders nothing
}Main Provider (src/components/crisp/crisp-provider.tsx)
'use client';
import { ReactNode, useCallback, useState } from 'react';
import dynamic from 'next/dynamic';
import { Crisp } from 'crisp-sdk-web';
import { CrispContext } from './crisp-context';
import type { CrispContextType } from './types';
// Dynamic import to ensure SSR safety
const CrispInitializer = dynamic(() => import('./crisp-initializer'), {
ssr: false,
});
type CrispProviderProps = {
children: ReactNode;
};
/**
* CrispProvider provides Crisp chat functionality throughout the app
* It handles initialization, state management, and exposes chat actions
*/
export function CrispProvider({ children }: CrispProviderProps) {
const [isReady, setIsReady] = useState(false);
const [isChatOpen, setIsChatOpen] = useState(false);
// Chat actions
const openChat = useCallback(() => {
if (!isReady) return;
Crisp.chat.open();
}, [isReady]);
const closeChat = useCallback(() => {
if (!isReady) return;
Crisp.chat.close();
}, [isReady]);
const sendMessage = useCallback(
(message: string) => {
if (!isReady) return;
Crisp.message.send('text', message);
},
[isReady],
);
// User data actions
const setUserEmail = useCallback(
(email: string) => {
if (!isReady) return;
Crisp.user.setEmail(email);
},
[isReady],
);
const setUserNickname = useCallback(
(nickname: string) => {
if (!isReady) return;
Crisp.user.setNickname(nickname);
},
[isReady],
);
// Event handlers
const handleReady = useCallback(() => {
setIsReady(true);
}, []);
const handleChatToggle = useCallback((isOpen: boolean) => {
setIsChatOpen(isOpen);
}, []);
const contextValue: CrispContextType = {
// Actions
openChat,
closeChat,
sendMessage,
// State
isReady,
isChatOpen,
// User data
setUserEmail,
setUserNickname,
};
return (
<CrispContext.Provider value={contextValue}>
<CrispInitializer onReady={handleReady} onChatToggle={handleChatToggle} />
{children}
</CrispContext.Provider>
);
}Custom Hook (src/components/crisp/use-crisp.ts)
'use client';
import { useContext } from 'react';
import { CrispContext } from './crisp-context';
export function useCrisp() {
const context = useContext(CrispContext);
if (!context) {
throw new Error('useCrisp must be used within a CrispProvider');
}
return context;
}Main Exports (src/components/crisp/index.ts)
// Export all Crisp-related components and hooks
export { CrispProvider } from './crisp-provider';
export { useCrisp } from './use-crisp';
export type { CrispContextType, CrispUser } from './types';Integration with Your App
Add the CrispProvider to your app root (usually in your providers component):
// src/components/client-providers.tsx
import { CrispProvider } from '@/components/crisp';
export function ClientProviders({ children }: { children: React.ReactNode }) {
return (
<SessionProvider>
<CrispProvider>
{children}
</CrispProvider>
</SessionProvider>
);
}Example Usage Components
Book Demo Button
'use client';
import { useCrisp } from '@/components/crisp';
export function BookDemoButton() {
const { openChat, sendMessage, isReady } = useCrisp();
const handleBookDemo = () => {
if (!isReady) return;
openChat();
sendMessage("Hi! I'd like to book a demo. Can you help me?");
};
return (
<button
onClick={handleBookDemo}
disabled={!isReady}
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50">
Book a Demo
</button>
);
}Contact Support Button
'use client';
import { useCrisp } from '@/components/crisp';
export function ContactSupportButton() {
const { openChat, isReady } = useCrisp();
return (
<button
onClick={openChat}
disabled={!isReady}
className="rounded-md bg-green-600 px-4 py-2 text-white hover:bg-green-700 disabled:cursor-not-allowed disabled:opacity-50">
Contact Support
</button>
);
}Contextual Help Section
'use client';
import { useCrisp } from '@/components/crisp';
export function HelpSection({ topic }: { topic: string }) {
const { openChat, sendMessage, isReady } = useCrisp();
const handleHelp = () => {
if (!isReady) return;
openChat();
sendMessage(`I need help with: ${topic}`);
};
return (
<div className="rounded-lg border p-4">
<h3 className="font-semibold">Need help with {topic}?</h3>
<p className="text-sm text-gray-600">Our support team is here to help you.</p>
<button
onClick={handleHelp}
disabled={!isReady}
className="mt-2 rounded-md bg-blue-600 px-3 py-1 text-sm text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-50">
Get Help
</button>
</div>
);
}Conclusion
Copy the prompt, paste it into your AI assistant, and you'll have a fully functional Crisp.chat integration ready to deploy.