Skip to main content

Messaging API

Real-time messaging system for communication between clients and lawyers.

Database Schema

Messages are stored in the messages table with the following structure:

Message Object

id
string
Message ID (UUID)
sender_id
string
Sender’s user ID
receiver_id
string
Receiver’s user ID
content
string
Message content (text)
consultation_id
string | null
Associated consultation ID (if applicable)
service_id
string | null
Associated service ID (if applicable)
read
boolean
Whether message has been read (default: false)
created_at
string
Message timestamp (ISO 8601)

Send Message

Messages are sent using Supabase client-side operations:
const { data, error } = await supabase
  .from('messages')
  .insert({
    sender_id: currentUserId,
    receiver_id: recipientId,
    content: messageText,
    consultation_id: consultationId, // optional
    service_id: serviceId, // optional
    read: false
  })
  .select()
  .single();

Parameters

sender_id
string
required
Current user’s ID
receiver_id
string
required
Recipient’s user ID
content
string
required
Message text content
consultation_id
string
Link message to a consultation
service_id
string
Link message to a service

Get Messages

Retrieve messages for a conversation:
const { data: messages, error } = await supabase
  .from('messages')
  .select('*')
  .or(`sender_id.eq.${userId},receiver_id.eq.${userId}`)
  .order('created_at', { ascending: true });

Filter by Conversation

Get messages between two specific users:
const { data: messages, error } = await supabase
  .from('messages')
  .select('*')
  .or(
    `and(sender_id.eq.${user1Id},receiver_id.eq.${user2Id}),` +
    `and(sender_id.eq.${user2Id},receiver_id.eq.${user1Id})`
  )
  .order('created_at', { ascending: true });

Filter by Consultation

Get all messages for a consultation:
const { data: messages, error } = await supabase
  .from('messages')
  .select('*')
  .eq('consultation_id', consultationId)
  .order('created_at', { ascending: true });

Mark as Read

Mark messages as read:
const { error } = await supabase
  .from('messages')
  .update({ read: true })
  .eq('receiver_id', currentUserId)
  .eq('read', false);

Mark Specific Message as Read

const { error } = await supabase
  .from('messages')
  .update({ read: true })
  .eq('id', messageId);

Real-time Subscriptions

Listen for new messages in real-time:
const messageSubscription = supabase
  .channel('messages')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages',
      filter: `receiver_id=eq.${currentUserId}`
    },
    (payload) => {
      console.log('New message:', payload.new);
      // Handle new message
    }
  )
  .subscribe();

// Cleanup
return () => {
  messageSubscription.unsubscribe();
};

Subscribe to Conversation Updates

const conversationChannel = supabase
  .channel('conversation')
  .on(
    'postgres_changes',
    {
      event: '*',
      schema: 'public',
      table: 'messages',
      filter: `or(and(sender_id.eq.${user1Id},receiver_id.eq.${user2Id}),and(sender_id.eq.${user2Id},receiver_id.eq.${user1Id}))`
    },
    (payload) => {
      console.log('Conversation update:', payload);
      // Handle conversation update
    }
  )
  .subscribe();

Get Unread Count

Count unread messages for current user:
const { count, error } = await supabase
  .from('messages')
  .select('*', { count: 'exact', head: true })
  .eq('receiver_id', currentUserId)
  .eq('read', false);

console.log('Unread messages:', count);

Get Conversations List

Get list of all conversations with latest message:
const { data: conversations, error } = await supabase
  .rpc('get_conversations', {
    user_id: currentUserId
  });
This requires a custom database function get_conversations that returns unique conversations with the most recent message.

Example: Complete Chat Component

import { useEffect, useState } from 'react';
import { supabase } from '@/lib/supabaseClient';

interface Message {
  id: string;
  sender_id: string;
  receiver_id: string;
  content: string;
  read: boolean;
  created_at: string;
}

export function ChatComponent({ 
  currentUserId, 
  recipientId 
}: { 
  currentUserId: string; 
  recipientId: string; 
}) {
  const [messages, setMessages] = useState<Message[]>([]);
  const [newMessage, setNewMessage] = useState('');

  // Load messages
  useEffect(() => {
    const loadMessages = async () => {
      const { data } = await supabase
        .from('messages')
        .select('*')
        .or(
          `and(sender_id.eq.${currentUserId},receiver_id.eq.${recipientId}),` +
          `and(sender_id.eq.${recipientId},receiver_id.eq.${currentUserId})`
        )
        .order('created_at', { ascending: true });

      if (data) setMessages(data);
    };

    loadMessages();

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

    return () => {
      subscription.unsubscribe();
    };
  }, [currentUserId, recipientId]);

  // Send message
  const sendMessage = async () => {
    if (!newMessage.trim()) return;

    const { data, error } = await supabase
      .from('messages')
      .insert({
        sender_id: currentUserId,
        receiver_id: recipientId,
        content: newMessage,
        read: false
      })
      .select()
      .single();

    if (data) {
      setMessages(prev => [...prev, data]);
      setNewMessage('');
    }
  };

  return (
    <div>
      <div className="messages">
        {messages.map(msg => (
          <div key={msg.id}>
            <strong>{msg.sender_id === currentUserId ? 'You' : 'Them'}:</strong>
            {msg.content}
          </div>
        ))}
      </div>
      <input
        value={newMessage}
        onChange={(e) => setNewMessage(e.target.value)}
        onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
      />
      <button onClick={sendMessage}>Send</button>
    </div>
  );
}

RLS Policies

Messages Table

  • SELECT: Users can read messages they sent or received
  • INSERT: Authenticated users can send messages
  • UPDATE: Users can update messages they received (for marking as read)
  • DELETE: Users can delete their own sent messages

Best Practices

  1. Always filter by user: Ensure RLS policies are working by filtering queries
  2. Clean up subscriptions: Always unsubscribe when components unmount
  3. Handle offline: Implement retry logic for failed message sends
  4. Validate content: Sanitize message content on client and server
  5. Rate limiting: Implement client-side throttling for message sending
  6. Pagination: Load messages in chunks for long conversations
  • consultations: Link messages to legal consultations
  • services: Link messages to specific services
  • profiles: Get sender/receiver profile information

Build docs developers (and LLMs) love