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
Associated consultation ID (if applicable)
Associated service ID (if applicable)
Whether message has been read (default: false)
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
Link message to a consultation
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
- Always filter by user: Ensure RLS policies are working by filtering queries
- Clean up subscriptions: Always unsubscribe when components unmount
- Handle offline: Implement retry logic for failed message sends
- Validate content: Sanitize message content on client and server
- Rate limiting: Implement client-side throttling for message sending
- 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