Building Realtime Applications with Supabase

EagerUI Digital Team

Building Realtime Applications with Supabase
In today's digital landscape, users expect applications to be responsive and collaborative, with changes appearing instantly across all connected clients. Realtime functionality has evolved from a luxury to a necessity for many applications, from chat platforms and collaborative tools to live dashboards and multiplayer games.
Supabase makes implementing these realtime features remarkably straightforward with its built-in realtime capabilities. In this guide, we'll explore how to leverage Supabase's realtime features to create dynamic, interactive applications.
Understanding Supabase Realtime
Supabase Realtime is built on Phoenix Channels and PostgreSQL's powerful change notification features. It allows you to:
- Listen to database changes in realtime
- Broadcast custom messages to connected clients
- Create presence awareness for users
- Build collaborative features with minimal backend code
The system works by monitoring PostgreSQL's replication functionality and broadcasting changes to connected clients over WebSockets, providing a scalable solution for realtime applications.
Getting Started with Supabase Realtime
Prerequisites
Before diving into realtime features, ensure you have:
- A Supabase project set up
- The Supabase client library installed in your application
- Basic familiarity with Supabase tables and Row Level Security (RLS)
Enabling Realtime for Your Tables
By default, realtime is disabled for all tables. You need to explicitly enable it for the tables you want to monitor:
- Go to your Supabase dashboard
- Navigate to Database > Replication
- Add the tables you want to enable for realtime by clicking "Add Table"
- Select the operations you want to track (INSERT, UPDATE, DELETE)
Alternatively, you can enable realtime via SQL:
-- Enable realtime for the messages table
alter publication supabase_realtime add table messages;
Subscribing to Database Changes
The most common use case for realtime is listening to database changes. Here's how to implement it:
Basic Subscription
// Initialize the Supabase client
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
'https://your-project-url.supabase.co',
'your-anon-key'
);
// Subscribe to changes in the messages table
const subscription = supabase
.channel('schema-db-changes')
.on(
'postgres_changes',
{
event: '*', // Listen to all changes (INSERT, UPDATE, DELETE)
schema: 'public',
table: 'messages',
},
(payload) => {
console.log('Change received!', payload);
// Handle the change based on the event type
if (payload.eventType === 'INSERT') {
// Add the new message to the UI
addMessageToChat(payload.new);
} else if (payload.eventType === 'UPDATE') {
// Update the existing message in the UI
updateMessageInChat(payload.new);
} else if (payload.eventType === 'DELETE') {
// Remove the message from the UI
removeMessageFromChat(payload.old.id);
}
}
)
.subscribe();
// Later, when you want to unsubscribe
subscription.unsubscribe();
Filtering Subscriptions
You can filter the changes you receive to make your application more efficient:
// Subscribe only to messages in a specific chat room
const roomId = '123';
const subscription = supabase
.channel('room-specific-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}`, // Only listen to messages in this room
},
(payload) => {
console.log('New message in room!', payload);
// Handle the change
}
)
.subscribe();
Listening to Specific Events
You can also listen to specific events (INSERT, UPDATE, DELETE) rather than all changes:
// Listen only to new messages being added
const newMessagesSubscription = supabase
.channel('new-messages')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
},
(payload) => {
console.log('New message added!', payload.new);
// Add the new message to the UI
}
)
.subscribe();
// Listen only to messages being updated
const updatedMessagesSubscription = supabase
.channel('updated-messages')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'messages',
},
(payload) => {
console.log('Message updated!', payload.new);
// Update the message in the UI
}
)
.subscribe();
Implementing Common Realtime Features
Let's explore how to implement some common realtime features using Supabase:
1. Realtime Chat Application
A chat application is the classic example of realtime functionality:
// Component for a simple chat room
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [newMessage, setNewMessage] = useState('');
// Load initial messages
useEffect(() => {
const loadMessages = async () => {
const { data, error } = await supabase
.from('messages')
.select('*')
.eq('room_id', roomId)
.order('created_at', { ascending: true });
if (error) {
console.error('Error loading messages:', error);
} else {
setMessages(data || []);
}
};
loadMessages();
}, [roomId]);
// Subscribe to new messages
useEffect(() => {
const subscription = supabase
.channel(`room-${roomId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}`,
},
(payload) => {
setMessages((current) => [...current, payload.new]);
}
)
.subscribe();
return () => {
subscription.unsubscribe();
};
}, [roomId]);
// Send a new message
const sendMessage = async (e) => {
e.preventDefault();
if (!newMessage.trim()) return;
const { error } = await supabase
.from('messages')
.insert([
{
room_id: roomId,
content: newMessage,
user_id: supabase.auth.user().id,
},
]);
if (error) {
console.error('Error sending message:', error);
} else {
setNewMessage('');
}
};
return (
<div className="chat-room">
<div className="messages">
{messages.map((message) => (
<div key={message.id} className="message">
{message.content}
</div>
))}
</div>
<form onSubmit={sendMessage}>
<input
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type a message..."
/>
<button type="submit">Send</button>
</form>
</div>
);
}
2. Collaborative Document Editing
For collaborative editing, you might want to track changes to a document:
function CollaborativeEditor({ documentId }) {
const [content, setContent] = useState('');
const [lastSaved, setLastSaved] = useState(null);
const [users, setUsers] = useState([]);
// Load initial document
useEffect(() => {
const loadDocument = async () => {
const { data, error } = await supabase
.from('documents')
.select('*')
.eq('id', documentId)
.single();
if (error) {
console.error('Error loading document:', error);
} else {
setContent(data.content || '');
setLastSaved(new Date(data.updated_at));
}
};
loadDocument();
}, [documentId]);
// Subscribe to document changes
useEffect(() => {
const subscription = supabase
.channel(`document-${documentId}`)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'documents',
filter: `id=eq.${documentId}`,
},
async (payload) => {
// Only update if the change wasn't made by the current user
if (payload.new.updated_by !== supabase.auth.user().id) {
setContent(payload.new.content);
setLastSaved(new Date(payload.new.updated_at));
}
}
)
.subscribe();
return () => {
subscription.unsubscribe();
};
}, [documentId]);
// Save changes to the document
const saveChanges = async () => {
const { error } = await supabase
.from('documents')
.update({
content,
updated_by: supabase.auth.user().id,
})
.eq('id', documentId);
if (error) {
console.error('Error saving document:', error);
} else {
setLastSaved(new Date());
}
};
// Auto-save changes every 5 seconds if content has changed
useEffect(() => {
const interval = setInterval(() => {
if (content !== '') {
saveChanges();
}
}, 5000);
return () => clearInterval(interval);
}, [content]);
return (
<div className="collaborative-editor">
<div className="editor-header">
<div className="users-editing">
{users.map((user) => (
<div key={user.id} className="user-avatar">
{user.name.charAt(0)}
</div>
))}
</div>
<div className="last-saved">
{lastSaved ? `Last saved: ${lastSaved.toLocaleTimeString()}` : 'Not saved yet'}
</div>
</div>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
className="editor-content"
/>
<button onClick={saveChanges}>Save Now</button>
</div>
);
}
3. Live Dashboard with Realtime Updates
For a dashboard that updates in realtime:
function SalesDashboard() {
const [salesData, setSalesData] = useState({
today: 0,
thisWeek: 0,
thisMonth: 0,
recentTransactions: [],
});
// Load initial data
useEffect(() => {
const loadDashboardData = async () => {
// Fetch summary data
const { data: summaryData, error: summaryError } = await supabase
.rpc('get_sales_summary');
// Fetch recent transactions
const { data: recentData, error: recentError } = await supabase
.from('transactions')
.select('*')
.order('created_at', { ascending: false })
.limit(10);
if (summaryError || recentError) {
console.error('Error loading dashboard data:', summaryError || recentError);
} else {
setSalesData({
today: summaryData.today_sales,
thisWeek: summaryData.week_sales,
thisMonth: summaryData.month_sales,
recentTransactions: recentData || [],
});
}
};
loadDashboardData();
}, []);
// Subscribe to new transactions
useEffect(() => {
const subscription = supabase
.channel('dashboard-updates')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'transactions',
},
async (payload) => {
// Update recent transactions list
setSalesData((current) => ({
...current,
recentTransactions: [
payload.new,
...current.recentTransactions.slice(0, 9),
],
}));
// Refresh summary data
const { data, error } = await supabase.rpc('get_sales_summary');
if (!error) {
setSalesData((current) => ({
...current,
today: data.today_sales,
thisWeek: data.week_sales,
thisMonth: data.month_sales,
}));
}
}
)
.subscribe();
return () => {
subscription.unsubscribe();
};
}, []);
return (
<div className="sales-dashboard">
<div className="summary-cards">
<div className="card">
<h3>Today's Sales</h3>
<p className="amount">${salesData.today.toFixed(2)}</p>
</div>
<div className="card">
<h3>This Week</h3>
<p className="amount">${salesData.thisWeek.toFixed(2)}</p>
</div>
<div className="card">
<h3>This Month</h3>
<p className="amount">${salesData.thisMonth.toFixed(2)}</p>
</div>
</div>
<div className="recent-transactions">
<h3>Recent Transactions</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Amount</th>
<th>Customer</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{salesData.recentTransactions.map((transaction) => (
<tr key={transaction.id}>
<td>{transaction.id}</td>
<td>${transaction.amount.toFixed(2)}</td>
<td>{transaction.customer_name}</td>
<td>{new Date(transaction.created_at).toLocaleTimeString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
Advanced Realtime Features
Presence
Supabase Realtime also includes a Presence feature, which allows you to track which users are currently active in a particular context:
function CollaborativeApp() {
const [onlineUsers, setOnlineUsers] = useState([]);
const currentUser = supabase.auth.user();
useEffect(() => {
const channel = supabase.channel('room-presence');
// Join the presence channel with user info
channel
.on('presence', { event: 'sync' }, () => {
// Get the current state of all users
const state = channel.presenceState();
const users = Object.values(state).flat();
setOnlineUsers(users);
})
.on('presence', { event: 'join' }, ({ newPresences }) => {
// Someone has joined
setOnlineUsers((prevUsers) => [...prevUsers, ...newPresences]);
})
.on('presence', { event: 'leave' }, ({ leftPresences }) => {
// Someone has left
const leftIds = leftPresences.map((p) => p.user_id);
setOnlineUsers((prevUsers) =>
prevUsers.filter((u) => !leftIds.includes(u.user_id))
);
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
// Track user presence once subscribed
await channel.track({
user_id: currentUser.id,
username: currentUser.email,
online_at: new Date().toISOString(),
});
}
});
return () => {
channel.unsubscribe();
};
}, [currentUser]);
return (
<div className="app">
<div className="online-users">
<h3>Online Users ({onlineUsers.length})</h3>
<ul>
{onlineUsers.map((user) => (
<li key={user.user_id}>
{user.username}
{user.user_id === currentUser.id ? ' (You)' : ''}
</li>
))}
</ul>
</div>
{/* Rest of your application */}
</div>
);
}
Broadcast
You can also use Supabase Realtime to broadcast custom messages to all connected clients:
function DrawingApp() {
const [strokes, setStrokes] = useState([]);
const canvasRef = useRef(null);
const channelRef = useRef(null);
// Set up the broadcast channel
useEffect(() => {
const channel = supabase.channel('drawing-room');
channel
.on('broadcast', { event: 'stroke' }, (payload) => {
// Add the received stroke to the canvas
setStrokes((current) => [...current, payload.stroke]);
drawStroke(payload.stroke);
})
.subscribe();
channelRef.current = channel;
return () => {
channel.unsubscribe();
};
}, []);
// Draw a stroke on the canvas
const drawStroke = (stroke) => {
const canvas = canvasRef.current;
const ctx = canvas.getContext('2d');
ctx.beginPath();
ctx.moveTo(stroke.points[0].x, stroke.points[0].y);
for (let i = 1; i < stroke.points.length; i++) {
ctx.lineTo(stroke.points[i].x, stroke.points[i].y);
}
ctx.strokeStyle = stroke.color;
ctx.lineWidth = stroke.width;
ctx.stroke();
};
// Handle mouse movements to create strokes
const handleMouseMove = (e) => {
if (!isDrawing) return;
const rect = canvasRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
currentStroke.points.push({ x, y });
// Draw locally
drawStroke(currentStroke);
// Broadcast to other users every few points to reduce network traffic
if (currentStroke.points.length % 5 === 0) {
channelRef.current.send({
type: 'broadcast',
event: 'stroke',
payload: { stroke: currentStroke },
});
}
};
// Rest of the drawing app implementation...
return (
<div className="drawing-app">
<canvas
ref={canvasRef}
width={800}
height={600}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
/>
<div className="color-picker">
{/* Color selection UI */}
</div>
</div>
);
}
Performance Considerations
When implementing realtime features, keep these performance considerations in mind:
-
Filter subscriptions: Only subscribe to the specific data you need to reduce unnecessary network traffic.
-
Debounce frequent updates: For high-frequency updates like collaborative editing, consider debouncing changes before sending them to the server.
-
Batch operations: When possible, batch multiple operations together rather than sending many small updates.
-
Clean up subscriptions: Always unsubscribe from channels when components unmount to prevent memory leaks.
-
Consider payload size: Be mindful of the size of the data being sent in realtime updates, especially for applications with many concurrent users.
Security Considerations
Realtime features introduce additional security considerations:
-
Row Level Security: Ensure your tables have appropriate RLS policies to prevent unauthorized access to data.
-
Validate client-side data: Always validate data on the server before saving it to the database.
-
Rate limiting: Implement rate limiting for realtime operations to prevent abuse.
-
Monitor usage: Keep an eye on your realtime usage metrics to detect unusual patterns that might indicate security issues.
Conclusion
Supabase's realtime features provide a powerful foundation for building interactive, collaborative applications with minimal backend code. By leveraging PostgreSQL's built-in replication capabilities, Supabase offers a scalable, secure approach to realtime functionality that integrates seamlessly with the rest of your application.
Whether you're building a chat application, a collaborative tool, or a live dashboard, Supabase Realtime gives you the tools you need to create engaging, responsive user experiences. As you explore these capabilities, you'll discover new ways to make your applications more interactive and valuable to your users.
Remember that realtime features should enhance the user experience without compromising performance or security. By following the patterns and best practices outlined in this guide, you can create realtime applications that are both powerful and maintainable.