$ cat /posts/supabase-real-time-live-data-subscriptions-guide.md
[tags]Supabase

Supabase Real-time: Live Data Subscriptions Guide

drwxr-xr-x2026-01-255 min0 views
Supabase Real-time: Live Data Subscriptions Guide

Real-time data synchronization transforms static applications into dynamic, collaborative experiences where changes propagate instantly across all connected clients—enabling live chat, collaborative editing, real-time dashboards, multiplayer games, and social features like live notifications and presence indicators. Supabase provides comprehensive real-time capabilities built on Phoenix Channels and PostgreSQL's logical replication, offering three core features: Postgres Changes for database row updates, Presence for tracking online users, and Broadcast for ephemeral messaging. This complete guide covers subscribing to database changes (INSERT, UPDATE, DELETE), filtering subscriptions with Row Level Security, implementing presence tracking for online status, using broadcast channels for live communication, handling reconnections and network issues, optimizing performance with filters, and building real-world features like live comments, typing indicators, and collaborative features. Unlike polling-based solutions requiring constant server requests, Supabase Real-time uses WebSockets for efficient bidirectional communication. Before proceeding, understand database basics and queries.

Real-time Features Overview

FeatureDescriptionUse CasesRLS Support
Postgres ChangesDatabase row changes (INSERT/UPDATE/DELETE)Live feeds, notificationsYes
PresenceTrack online users in real-timeOnline status, user listsYes
BroadcastSend ephemeral messages between clientsTyping indicators, live cursorsNo (client-side)
WebSocketBidirectional persistent connectionAll featuresN/A

Enabling Real-time

sqlenable_realtime.sql
-- Enable real-time on tables in Supabase Dashboard
-- Go to: Database > Replication > Enable for specific tables

-- Or use SQL to enable replication
alter publication supabase_realtime add table posts;
alter publication supabase_realtime add table comments;
alter publication supabase_realtime add table messages;

-- Check enabled tables
select * from pg_publication_tables where pubname = 'supabase_realtime';

-- Real-time respects Row Level Security policies
-- Users only receive updates for rows they can access

Subscribing to Database Changes

javascriptpostgres_changes.jsx
import { supabase } from './supabaseClient'
import { useEffect, useState } from 'react'

// Subscribe to all changes on 'posts' table
function PostsFeed() {
  const [posts, setPosts] = useState([])

  useEffect(() => {
    // Initial fetch
    fetchPosts()

    // Subscribe to real-time changes
    const channel = supabase
      .channel('posts-changes')
      .on(
        'postgres_changes',
        {
          event: '*', // Listen to all events (INSERT, UPDATE, DELETE)
          schema: 'public',
          table: 'posts'
        },
        (payload) => {
          console.log('Change received!', payload)
          handleRealtimeEvent(payload)
        }
      )
      .subscribe()

    // Cleanup subscription
    return () => {
      supabase.removeChannel(channel)
    }
  }, [])

  async function fetchPosts() {
    const { data } = await supabase
      .from('posts')
      .select('*')
      .order('created_at', { ascending: false })
    setPosts(data || [])
  }

  function handleRealtimeEvent(payload) {
    if (payload.eventType === 'INSERT') {
      setPosts(prev => [payload.new, ...prev])
    } else if (payload.eventType === 'UPDATE') {
      setPosts(prev => prev.map(post => 
        post.id === payload.new.id ? payload.new : post
      ))
    } else if (payload.eventType === 'DELETE') {
      setPosts(prev => prev.filter(post => post.id !== payload.old.id))
    }
  }

  return (
    <div>
      <h2>Live Posts Feed</h2>
      {posts.map(post => (
        <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.content}</p>
        </div>
      ))}
    </div>
  )
}

export default PostsFeed

Filtered Subscriptions

javascriptfiltered_subscriptions.js
// Subscribe to specific row changes
function UserPosts({ userId }) {
  useEffect(() => {
    const channel = supabase
      .channel('user-posts')
      .on(
        'postgres_changes',
        {
          event: '*',
          schema: 'public',
          table: 'posts',
          filter: `user_id=eq.${userId}` // Only changes for this user
        },
        (payload) => {
          console.log('User post changed:', payload)
        }
      )
      .subscribe()

    return () => supabase.removeChannel(channel)
  }, [userId])

  // Component code...
}

// Subscribe to INSERT events only
const channel = supabase
  .channel('new-posts')
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'posts'
    },
    (payload) => {
      console.log('New post created:', payload.new)
    }
  )
  .subscribe()

// Subscribe to UPDATE events only
const channel = supabase
  .channel('post-updates')
  .on(
    'postgres_changes',
    {
      event: 'UPDATE',
      schema: 'public',
      table: 'posts',
      filter: 'id=eq.123'
    },
    (payload) => {
      console.log('Post updated:', payload.new)
    }
  )
  .subscribe()

Presence: Online Users

javascriptpresence.jsx
// Track online users with Presence
import { useEffect, useState } from 'react'
import { supabase } from './supabaseClient'

function OnlineUsers() {
  const [onlineUsers, setOnlineUsers] = useState([])

  useEffect(() => {
    const channel = supabase.channel('online-users')

    // Track this user's presence
    channel
      .on('presence', { event: 'sync' }, () => {
        const presenceState = channel.presenceState()
        const users = Object.values(presenceState).flat()
        setOnlineUsers(users)
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          // Get current user
          const { data: { user } } = await supabase.auth.getUser()
          
          if (user) {
            // Track this user
            await channel.track({
              user_id: user.id,
              email: user.email,
              online_at: new Date().toISOString()
            })
          }
        }
      })

    // Cleanup
    return () => {
      channel.untrack()
      supabase.removeChannel(channel)
    }
  }, [])

  return (
    <div>
      <h3>Online Users ({onlineUsers.length})</h3>
      <ul>
        {onlineUsers.map((user, index) => (
          <li key={index}>
            {user.email} - {new Date(user.online_at).toLocaleTimeString()}
          </li>
        ))}
      </ul>
    </div>
  )
}

export default OnlineUsers

Broadcast: Ephemeral Messages

javascriptbroadcast.jsx
// Typing indicator with Broadcast
import { useEffect, useState } from 'react'
import { supabase } from './supabaseClient'

function ChatRoom({ roomId }) {
  const [typingUsers, setTypingUsers] = useState(new Set())
  const [channel, setChannel] = useState(null)

  useEffect(() => {
    const roomChannel = supabase.channel(`room:${roomId}`)

    roomChannel
      .on('broadcast', { event: 'typing' }, (payload) => {
        const { user_id, typing } = payload.payload
        
        setTypingUsers(prev => {
          const updated = new Set(prev)
          if (typing) {
            updated.add(user_id)
          } else {
            updated.delete(user_id)
          }
          return updated
        })

        // Clear typing after 3 seconds
        if (typing) {
          setTimeout(() => {
            setTypingUsers(prev => {
              const updated = new Set(prev)
              updated.delete(user_id)
              return updated
            })
          }, 3000)
        }
      })
      .subscribe()

    setChannel(roomChannel)

    return () => {
      supabase.removeChannel(roomChannel)
    }
  }, [roomId])

  function handleTyping() {
    if (channel) {
      channel.send({
        type: 'broadcast',
        event: 'typing',
        payload: { user_id: 'current-user', typing: true }
      })
    }
  }

  return (
    <div>
      <input
        type="text"
        onKeyDown={handleTyping}
        placeholder="Type a message..."
      />
      {typingUsers.size > 0 && (
        <p>{typingUsers.size} user(s) typing...</p>
      )}
    </div>
  )
}

export default ChatRoom

Real-world Example: Live Comments

javascriptlive_comments.jsx
// Complete live comments component
import { useEffect, useState } from 'react'
import { supabase } from './supabaseClient'

function LiveComments({ postId }) {
  const [comments, setComments] = useState([])
  const [newComment, setNewComment] = useState('')
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    fetchComments()
    subscribeToComments()
  }, [postId])

  async function fetchComments() {
    const { data } = await supabase
      .from('comments')
      .select(`
        *,
        users (
          email
        )
      `)
      .eq('post_id', postId)
      .order('created_at', { ascending: true })

    setComments(data || [])
  }

  function subscribeToComments() {
    const channel = supabase
      .channel(`comments:${postId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'comments',
          filter: `post_id=eq.${postId}`
        },
        async (payload) => {
          // Fetch full comment with user data
          const { data } = await supabase
            .from('comments')
            .select('*, users(email)')
            .eq('id', payload.new.id)
            .single()

          setComments(prev => [...prev, data])
        }
      )
      .subscribe()

    return () => supabase.removeChannel(channel)
  }

  async function handleSubmit(e) {
    e.preventDefault()
    if (!newComment.trim()) return

    setLoading(true)

    const { data: { user } } = await supabase.auth.getUser()

    const { error } = await supabase
      .from('comments')
      .insert([{
        post_id: postId,
        user_id: user.id,
        content: newComment
      }])

    if (error) {
      alert(error.message)
    } else {
      setNewComment('')
    }

    setLoading(false)
  }

  return (
    <div className="live-comments">
      <h3>Comments ({comments.length})</h3>
      
      <div className="comments-list">
        {comments.map(comment => (
          <div key={comment.id} className="comment">
            <strong>{comment.users.email}</strong>
            <p>{comment.content}</p>
            <small>{new Date(comment.created_at).toLocaleString()}</small>
          </div>
        ))}
      </div>

      <form onSubmit={handleSubmit}>
        <textarea
          value={newComment}
          onChange={(e) => setNewComment(e.target.value)}
          placeholder="Add a comment..."
        />
        <button type="submit" disabled={loading}>
          {loading ? 'Posting...' : 'Post Comment'}
        </button>
      </form>
    </div>
  )
}

export default LiveComments

Real-time Best Practices

  • Cleanup Subscriptions: Always unsubscribe in component cleanup to prevent memory leaks
  • Filter Subscriptions: Use filters to reduce unnecessary updates and bandwidth
  • Enable RLS: Real-time respects Row Level Security—users only receive updates they're authorized to see
  • Handle Reconnections: Implement connection status monitoring and retry logic
  • Debounce Broadcasts: Rate-limit broadcast events like typing indicators to reduce traffic
  • Initial Fetch + Subscribe: Always fetch initial data before subscribing to changes
Performance Tip: Real-time subscriptions are lightweight but scale with concurrent connections. For high-traffic apps, use filtered subscriptions, implement connection pooling, and consider Edge Functions for complex real-time logic. Monitor usage in Supabase Dashboard.

Common Issues

  • No Updates Received: Check if real-time is enabled for the table in Dashboard > Database > Replication
  • RLS Blocking Updates: Users need SELECT permission on the table to receive real-time updates
  • Subscription Not Working: Ensure channel.subscribe() is called and check connection status
  • Memory Leaks: Always call supabase.removeChannel() in cleanup functions

Next Steps

  1. Build Live Features: Add real-time to your apps for chat, notifications, and collaborative editing
  2. Implement Advanced Logic: Use Edge Functions for complex real-time workflows
  3. Complete Projects: Build React todo app or Next.js application with real-time
  4. Optimize Performance: Learn Supabase architecture for scaling real-time features

Conclusion

Supabase Real-time transforms static applications into dynamic, collaborative experiences with database change subscriptions, presence tracking, and broadcast channels—all integrated with Row Level Security for automatic authorization. By leveraging WebSocket connections and PostgreSQL's logical replication, you eliminate polling-based approaches while maintaining efficient, scalable real-time communication. The three core features—Postgres Changes, Presence, and Broadcast—enable everything from live feeds and notifications to collaborative editing and multiplayer features. Always remember to enable real-time on tables, implement proper cleanup to prevent memory leaks, use filtered subscriptions for performance, and combine with Row Level Security for automatic access control. With real-time mastered, you're equipped to build modern applications with live comments, chat systems, online presence indicators, typing indicators, and collaborative features that keep users engaged. Continue building production applications with Edge Functions, CRUD tutorials, and framework integrations.

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.