How to Build Real-Time Chat Applications with Supabase

Learn to build scalable real-time chat applications using Supabase with WebSocket subscriptions, message broadcasting, and user presence features. Perfect for modern web applications requiring instant communication.

Building real-time chat applications has never been easier with Supabase's powerful real-time capabilities. In this comprehensive guide, we'll walk through creating a production-ready chat application that leverages Supabase's WebSocket subscriptions, PostgreSQL triggers, and built-in authentication. Whether you're building a customer support chat, team collaboration tool, or social messaging app, this tutorial will give you the foundation you need.

Why Choose Supabase for Real-Time Chat?

Supabase stands out as the ideal platform for real-time chat applications due to its native PostgreSQL real-time subscriptions, row-level security, and seamless authentication integration. Unlike traditional solutions that require separate WebSocket servers and complex state management, Supabase provides everything out of the box.

  • Built-in real-time subscriptions with PostgreSQL triggers
  • Row-level security for message privacy and access control
  • Integrated user authentication with social providers
  • Automatic scaling without infrastructure management
  • Full TypeScript support with generated types

Setting Up the Database Schema

First, let's design our chat database schema. We'll need tables for chat rooms, messages, and user presence tracking.

  • Chat rooms or channels
  • Individual chat messages
  • Track online/offline status
  • Room membership and permissions
-- Create chat rooms table
CREATE TABLE chat_rooms (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  name TEXT NOT NULL,
  description TEXT,
  created_by UUID REFERENCES auth.users(id),
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Create messages table
CREATE TABLE messages (
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
  room_id UUID REFERENCES chat_rooms(id) ON DELETE CASCADE,
  user_id UUID REFERENCES auth.users(id),
  content TEXT NOT NULL,
  message_type TEXT DEFAULT 'text' CHECK (message_type IN ('text', 'image', 'file')),
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Create user presence table
CREATE TABLE user_presence (
  user_id UUID REFERENCES auth.users(id) PRIMARY KEY,
  last_seen TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  is_online BOOLEAN DEFAULT false,
  status TEXT DEFAULT 'offline'
);

-- Create room members table
CREATE TABLE room_members (
  room_id UUID REFERENCES chat_rooms(id) ON DELETE CASCADE,
  user_id UUID REFERENCES auth.users(id),
  role TEXT DEFAULT 'member' CHECK (role IN ('owner', 'admin', 'member')),
  joined_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  PRIMARY KEY (room_id, user_id)
);

Implementing Row-Level Security

Security is crucial for chat applications. Let's implement row-level security (RLS) to ensure users can only access messages from rooms they're members of.

  • Enable RLS on all tables
  • Users can read messages from rooms they're members of
  • Users can insert messages to rooms they're members of
  • Users can only access rooms they're members of
-- Enable RLS on all tables
ALTER TABLE chat_rooms ENABLE ROW LEVEL SECURITY;
ALTER TABLE messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE room_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE user_presence ENABLE ROW LEVEL SECURITY;

-- Policy for reading messages
CREATE POLICY "Users can read messages from rooms they're members of" 
ON messages 
FOR SELECT 
USING (
  room_id IN (
    SELECT room_id FROM room_members 
    WHERE user_id = auth.uid()
  )
);

-- Policy for inserting messages
CREATE POLICY "Users can insert messages to rooms they're members of" 
ON messages 
FOR INSERT 
WITH CHECK (
  user_id = auth.uid() AND
  room_id IN (
    SELECT room_id FROM room_members 
    WHERE user_id = auth.uid()
  )
);

-- Policy for room members
CREATE POLICY "Users can see room members of rooms they're in" 
ON room_members 
FOR SELECT 
USING (
  room_id IN (
    SELECT room_id FROM room_members 
    WHERE user_id = auth.uid()
  )
);

Setting Up Real-Time Subscriptions

Now let's implement the client-side real-time functionality using Supabase's JavaScript client.

Set up message subscriptions for real-time updates:

import { useEffect, useState } from 'react'
import { supabase } from './supabase'

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([])

  useEffect(() => {
    // Fetch initial messages
    const fetchMessages = async () => {
      const { data } = await supabase
        .from('messages')
        .select('*')
        .eq('room_id', roomId)
        .order('created_at', { ascending: true })
      
      setMessages(data || [])
    }

    fetchMessages()

    // Subscribe to new messages
    const subscription = supabase
      .channel(`room:${roomId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'messages',
          filter: `room_id=eq.${roomId}`
        },
        (payload) => {
          setMessages(prev => [...prev, payload.new])
        }
      )
      .subscribe()

    return () => {
      subscription.unsubscribe()
    }
  }, [roomId])

  return (
    <div className="chat-room">
      {/* Chat UI implementation */}
    </div>
  )
}

Implementing User Presence

User presence shows who's online and when users were last active. Let's implement this feature using Supabase's real-time presence.

import { useEffect, useState } from 'react'

function useUserPresence(roomId) {
  const [onlineUsers, setOnlineUsers] = useState([])

  useEffect(() => {
    const channel = supabase.channel(`presence:${roomId}`)

    channel
      .on('presence', { event: 'sync' }, () => {
        const state = channel.presenceState()
        const users = Object.keys(state)
        setOnlineUsers(users)
      })
      .on('presence', { event: 'join' }, ({ key }) => {
        setOnlineUsers(prev => [...prev, key])
      })
      .on('presence', { event: 'leave' }, ({ key }) => {
        setOnlineUsers(prev => prev.filter(id => id !== key))
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          await channel.track({
            user_id: user?.id,
            online_at: new Date().toISOString(),
          })
        }
      })

    return () => {
      channel.unsubscribe()
    }
  }, [roomId])

  return onlineUsers
}

Message Broadcasting and Handling

Let's implement message sending and handle different message types including text, images, and files.

async function sendMessage(roomId, content, messageType = 'text') {
  const { data, error } = await supabase
    .from('messages')
    .insert({
      room_id: roomId,
      user_id: user?.id,
      content,
      message_type: messageType
    })
    .select()

  if (error) {
    console.error('Error sending message:', error)
    return null
  }

  return data[0]
}

// Handle file uploads
async function sendFileMessage(roomId, file) {
  // Upload file to Supabase Storage
  const fileName = `${Date.now()}-${file.name}`
  const { data: uploadData, error: uploadError } = await supabase.storage
    .from('chat-files')
    .upload(fileName, file)

  if (uploadError) {
    console.error('Error uploading file:', uploadError)
    return
  }

  // Get public URL
  const { data: { publicUrl } } = supabase.storage
    .from('chat-files')
    .getPublicUrl(fileName)

  // Send message with file URL
  await sendMessage(roomId, publicUrl, 'file')
}

Performance Optimization Tips

To ensure your chat application performs well at scale, consider these optimization strategies:

  • Implement message pagination to avoid loading too many messages at once
  • Add database indexes on frequently queried columns (room_id, created_at)
  • Use message compression for large text content
  • Implement automatic cleanup of old messages and files
  • Cache user profiles and room metadata to reduce database queries
-- Add indexes for better performance
CREATE INDEX idx_messages_room_created ON messages(room_id, created_at DESC);
CREATE INDEX idx_room_members_user ON room_members(user_id);
CREATE INDEX idx_messages_user ON messages(user_id);

Production Considerations

Before deploying your chat application to production, consider these important factors:

  • Implement rate limiting to prevent spam and abuse
  • Add content moderation and reporting features
  • Set up automated database backups
  • Monitor real-time connection counts and message volume
  • Plan for horizontal scaling as your user base grows

Conclusion

Building real-time chat applications with Supabase provides a robust, scalable foundation for modern communication features. With built-in real-time subscriptions, row-level security, and seamless authentication, you can focus on creating great user experiences rather than managing infrastructure. The combination of PostgreSQL's reliability and Supabase's real-time capabilities makes it an ideal choice for chat applications of any scale.

Ready to Build Your Real-Time Chat Application?

Our team of Swiss Supabase experts specializes in building scalable real-time applications. From chat systems to collaborative tools, we can help you leverage Supabase's full potential for your next project.

Get Expert Consultation
llms.txt