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 Applications?

Our team of Swiss Supabase experts can help you design and implement type-safe database schemas for your next project. From initial schema design to full application development, we've got you covered.

Get Expert Consultation
llms.txt
How to Build Real-Time Chat Applications with Supabase – HeapSoft