2 min read

Perfect Prompt: Install Crisp in Your Next.js App

A complete guide with production-ready code examples that allows developers to integrate Crisp customer support in their Next.js application using just one AI prompt.

by Jonathan Beurel

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 hook

Complete Implementation

Here's the production-ready code that the AI prompt generates:

Package Dependencies

First, install the required package:

npm install crisp-sdk-web

Environment Configuration

Add your Crisp Website ID to your environment variables:

# .env.local
NEXT_PUBLIC_CRISP_WEBSITE_ID=your-website-id-here

You 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.