Back to Blog
Development

Building Realtime Applications with Supabase

EagerUI Digital Team

EagerUI Digital Team

2025-08-1011 min read
Building Realtime Applications with Supabase

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:

  1. A Supabase project set up
  2. The Supabase client library installed in your application
  3. 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:

  1. Go to your Supabase dashboard
  2. Navigate to Database > Replication
  3. Add the tables you want to enable for realtime by clicking "Add Table"
  4. 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:

  1. Filter subscriptions: Only subscribe to the specific data you need to reduce unnecessary network traffic.

  2. Debounce frequent updates: For high-frequency updates like collaborative editing, consider debouncing changes before sending them to the server.

  3. Batch operations: When possible, batch multiple operations together rather than sending many small updates.

  4. Clean up subscriptions: Always unsubscribe from channels when components unmount to prevent memory leaks.

  5. 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:

  1. Row Level Security: Ensure your tables have appropriate RLS policies to prevent unauthorized access to data.

  2. Validate client-side data: Always validate data on the server before saving it to the database.

  3. Rate limiting: Implement rate limiting for realtime operations to prevent abuse.

  4. 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.