Building a Live Chat Support System with Supabase Realtime and Next.js 15
Discover how to create a robust live chat support system using Supabase Realtime and Next.js 15. This guide covers anonymous user support, real-time messaging, and an admin dashboard, providing full control over your customer interactions.

Customer support is the backbone of any successful SaaS business. While tools like Intercom and Zendesk are excellent, they can be expensive for early-stage startups. What if you could build your own live chat support system that's just as powerful, but integrated directly into your application stack?
In this post, I'll walk you through building a production-ready live chat support system using Supabase Realtime, Next.js 15, and React - complete with anonymous user support, real-time messaging, and a comprehensive admin dashboard.
Why Build Your Own Chat System?
Before diving into the implementation, let's understand why building your own chat system makes sense:
- Cost Control: Third-party chat solutions can cost $50-200+ per agent per month
- Data Ownership: All conversations and customer data stays in your database
- Custom Integration: Deep integration with your existing user system and workflows
- No Vendor Lock-in: Full control over features, scaling, and future development
- Brand Consistency: Matches your app's design and user experience perfectly
Architecture Overview
Our chat system consists of several key components:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Customer UI │ │ Agent Admin │ │ Database │
│ │ │ │ │ │
│ • ChatBubble │ │ • Dashboard │ │ • chat_sessions │
│ • ChatWindow │ │ • AgentView │ │ • chat_messages │
│ • ChatProvider │ │ • Session Mgmt │ │ • user_profiles │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└───────────────────────┼───────────────────────┘
│
┌─────────────────┐
│ Supabase │
│ Realtime │
│ WebSockets │
└─────────────────┘
Database Schema
The foundation of our chat system is built on two main tables:
-- Chat sessions table CREATE TABLE chat_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES user_profiles(id), anonymous_id TEXT, agent_id UUID REFERENCES user_profiles(id), status TEXT NOT NULL DEFAULT 'open', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), closed_at TIMESTAMPTZ ); -- Chat messages table CREATE TABLE chat_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), session_id UUID NOT NULL REFERENCES chat_sessions(id) ON DELETE CASCADE, sender_id UUID REFERENCES user_profiles(id), sender_type TEXT NOT NULL CHECK (sender_type IN ('customer', 'agent')), content TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW() );
Key design decisions:
- Anonymous Support: The
anonymous_id
field allows support for non-authenticated users - Session Management: Status tracking (open/assigned/closed) enables proper workflow management
- Flexible Sender Types: Clear distinction between customer and agent messages
- Cascading Deletes: When a session is deleted, all related messages are automatically removed
Building the Monorepo Package
Following modern development practices, we built this as a standalone package (@elite-saas/support-chat
) that can be easily extracted and reused across projects.
Package Structure
packages/support-chat/
├── src/
│ ├── components/ # Customer-facing UI
│ │ ├── ChatBubble.tsx # Floating chat button
│ │ ├── ChatWindow.tsx # Chat interface
│ │ ├── ChatProvider.tsx # Context provider
│ │ └── SupportChat.tsx # Main component
│ ├── admin/ # Agent dashboard
│ │ ├── SupportDashboard.tsx
│ │ └── ChatAgentView.tsx
│ ├── hooks/ # React hooks
│ │ ├── useChatRealtime.ts
│ │ └── useChatSession.ts
│ ├── services/ # Business logic
│ │ └── chat-service.ts
│ └── types/ # Local types
│ └── chat.ts
├── package.json
└── tsconfig.json
This modular approach ensures:
- Clean separation between customer and admin interfaces
- Reusable hooks for different use cases
- Service layer abstraction for database operations
- Type safety throughout the application
Real-time Implementation with Supabase
The heart of our chat system is Supabase's real-time functionality. Here's how we implemented it:
Custom Realtime Hook
export function useChatRealtime({ sessionId, onNewMessage, onSessionUpdate, }: UseChatRealtimeOptions) { const [isConnected, setIsConnected] = useState(false); const [error, setError] = useState<string | null>(null); const channelRef = useRef<RealtimeChannel | null>(null); useEffect(() => { if (!sessionId || !supabase) return; const channel = supabase .channel(`chat_session_${sessionId}`) .on( "postgres_changes", { event: "INSERT", schema: "public", table: "chat_messages", filter: `session_id=eq.${sessionId}`, }, (payload) => { const newMessage = payload.new as SupportChatMessage; onNewMessage?.(newMessage); }, ) .on( "postgres_changes", { event: "UPDATE", schema: "public", table: "chat_sessions", filter: `id=eq.${sessionId}`, }, (payload) => { const updatedSession = payload.new as ChatSession; onSessionUpdate?.(updatedSession); }, ) .subscribe((status) => { setIsConnected(status === "SUBSCRIBED"); if (status === "CHANNEL_ERROR") { setError("Failed to connect to chat"); } }); channelRef.current = channel; return () => { channel.unsubscribe(); }; }, [sessionId, supabase, onNewMessage, onSessionUpdate]); return { isConnected, error }; }
This hook handles:
- Connection management with automatic reconnection
- Message subscriptions for real-time updates
- Session updates for status changes
- Error handling with user feedback
Anonymous User Support
One of the key features is supporting anonymous users who haven't signed up yet:
// Generate anonymous ID for non-authenticated users useEffect(() => { if (!userId) { const stored = localStorage.getItem(CHAT_CONFIG.ANONYMOUS_ID_STORAGE_KEY); if (stored) { setAnonymousId(stored); } else { const newId = `anon_${Date.now()}_${Math.random() .toString(36) .substr(2, 9)}`; localStorage.setItem(CHAT_CONFIG.ANONYMOUS_ID_STORAGE_KEY, newId); setAnonymousId(newId); } } }, [userId]);
This approach:
- Persists across browser sessions using localStorage
- Generates unique identifiers to track conversations
- Seamlessly transitions when users sign up later
- Maintains conversation history even for anonymous users
Service Layer Architecture
Following clean architecture principles, all database operations are abstracted into a service layer:
export class ChatService { constructor(private supabase: SupabaseClient) {} async createSession(params: CreateSessionParams): Promise<ChatSession> { const sessionData: any = { status: "open", }; if (params.userId) { sessionData.user_id = params.userId; } else if (params.anonymousId) { sessionData.anonymous_id = params.anonymousId; } else { throw new ChatError("Either userId or anonymousId must be provided"); } const { data, error } = await this.supabase .from("chat_sessions") .insert(sessionData) .select() .single(); if (error) throw new ChatError("Failed to create session", error); return data; } async sendMessage(params: SendMessageParams): Promise<SupportChatMessage> { const { data, error } = await this.supabase .from("chat_messages") .insert({ session_id: params.sessionId, sender_id: params.senderId, sender_type: params.senderType, content: params.content.trim(), }) .select() .single(); if (error) throw new ChatError("Failed to send message", error); return data; } }
Benefits of this approach:
- Centralized business logic for easy testing and maintenance
- Consistent error handling across the application
- Type-safe operations with full TypeScript support
- Easy mocking for unit tests
Row Level Security (RLS) Implementation
Security is critical for a chat system. We implemented comprehensive RLS policies:
-- Messages: Users can only see messages from their own sessions CREATE POLICY "Users can view their own session messages" ON chat_messages FOR SELECT USING ( session_id IN ( SELECT id FROM chat_sessions WHERE user_id = auth.uid() OR anonymous_id = current_setting('app.anonymous_id', true) ) ); -- Agents can see all messages CREATE POLICY "Support agents can view all messages" ON chat_messages FOR SELECT USING ( EXISTS ( SELECT 1 FROM user_profiles WHERE id = auth.uid() AND is_support_agent = true ) );
This ensures:
- Data isolation between different customer sessions
- Agent access to all conversations for support purposes
- Anonymous user protection using session settings
- Automatic enforcement at the database level
Admin Dashboard Features
The admin dashboard provides agents with everything they need:
Session Management
- Real-time session list with status indicators
- Unread message counts for prioritization
- One-click assignment to take ownership of conversations
- Session status updates (open/assigned/closed)
Agent Interface
export function ChatAgentView({ sessionId, onBack }: ChatAgentViewProps) { const [session, setSession] = useState<ChatSession | null>(null); const [messages, setMessages] = useState<SupportChatMessage[]>([]); // Real-time message updates useChatRealtime({ sessionId, onNewMessage: (message) => { setMessages((prev) => [...prev, message]); }, onSessionUpdate: (updatedSession) => { setSession(updatedSession); }, }); const handleSendMessage = async (content: string) => { await chatService.sendMessage({ sessionId, senderId: currentUser.id, senderType: "agent", content, }); }; }
Key features:
- Real-time message delivery with instant updates
- Message history with sender identification
- Session controls for assignment and closure
- Responsive design for mobile and desktop use
Performance Optimizations
Several optimizations ensure smooth operation at scale:
Message Pagination
const { data: messages } = await supabase .from("chat_messages") .select("*") .eq("session_id", sessionId) .order("created_at", { ascending: true }) .range(offset, offset + MESSAGES_PER_PAGE - 1);
Connection Pooling
- Shared Supabase client instances across components
- Connection reuse for multiple subscriptions
- Automatic cleanup when components unmount
Optimistic Updates
const sendMessage = useCallback(async (content: string) => { // Optimistic update for immediate UI feedback const optimisticMessage = { id: `temp-${Date.now()}`, content, sender_type: senderType, created_at: new Date().toISOString(), }; setMessages(prev => [...prev, optimisticMessage]); try { await chatService.sendMessage({...}); } catch (error) { // Revert on error setMessages(prev => prev.filter(m => m.id !== optimisticMessage.id)); throw error; } }, []);
Integration and Usage
Integrating the chat system into your app is straightforward:
1. Provider Setup
// app/layout.tsx import { ChatProvider } from "@elite-saas/support-chat"; export default function RootLayout({ children }) { return ( <html> <body> <ChatProvider supabase={supabase} userId={user?.id}> {children} </ChatProvider> </body> </html> ); }
2. Add Chat to Pages
// Any page component import { SupportChat } from "@elite-saas/support-chat"; export default function HomePage() { return ( <div> <h1>Welcome to our app</h1> <SupportChat /> </div> ); }
3. Admin Dashboard
// admin/support/page.tsx import { SupportDashboard } from "@elite-saas/support-chat"; export default function SupportPage() { return <SupportDashboard />; }
Next Steps and Enhancements
This implementation provides a solid foundation that can be extended with:
- File attachments for screenshots and documents
- Canned responses for common support queries
- Chat routing based on user tier or issue type
- Analytics dashboard for support metrics
- Mobile push notifications for agents
- Multilingual support for international customers
- AI-powered chatbots for initial triage
Conclusion
Building your own live chat support system gives you complete control over the customer experience while keeping costs manageable. With Supabase's real-time capabilities and Next.js 15's performance optimizations, you can create a system that rivals expensive third-party solutions.
The modular architecture ensures you can start simple and add features as your business grows. Most importantly, you own the entire customer interaction history, giving you valuable insights for improving your product and support processes.
Try It Yourself
Want to see this live chat system in action? Check out EliteSaaS.dev - our complete Next.js SaaS starter template that includes this chat system along with authentication, billing, teams, and much more.
We'd love your feedback! Have you built similar systems? What features would you prioritize? Let us know in the comments or reach out to us directly through the chat system on our site.
The complete source code for this implementation is available as part of the EliteSaaS template. Start building your own SaaS with a professional support system from day one.