--- name: real-time-features description: Expert guide for real-time features using Supabase Realtime, WebSockets, live updates, presence, and collaborative features. Use when building chat, live updates, or collaborative apps. --- # Real-Time Features Skill ## Overview This skill helps you implement real-time features in your Next.js application. From live data updates to collaborative editing, this covers everything you need for interactive, real-time experiences. ## Supabase Realtime ### Setup ```typescript // lib/supabase/client.ts import { createBrowserClient } from '@supabase/ssr' export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY! ) } ``` ### Real-Time Subscriptions **Subscribe to Table Changes:** ```typescript 'use client' import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' export function RealtimeMessages() { const [messages, setMessages] = useState([]) const supabase = createClient() useEffect(() => { // Fetch initial data supabase .from('messages') .select('*') .order('created_at', { ascending: true }) .then(({ data }) => setMessages(data || [])) // Subscribe to new messages const channel = supabase .channel('messages') .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages', }, (payload) => { setMessages((prev) => [...prev, payload.new as Message]) } ) .subscribe() return () => { supabase.removeChannel(channel) } }, [supabase]) return (
{messages.map((msg) => (
{msg.content}
))}
) } ``` **Subscribe to All Events:** ```typescript const channel = supabase .channel('messages-all') .on( 'postgres_changes', { event: '*', // INSERT, UPDATE, DELETE schema: 'public', table: 'messages', }, (payload) => { if (payload.eventType === 'INSERT') { setMessages((prev) => [...prev, payload.new as Message]) } if (payload.eventType === 'UPDATE') { setMessages((prev) => prev.map((msg) => msg.id === payload.new.id ? (payload.new as Message) : msg ) ) } if (payload.eventType === 'DELETE') { setMessages((prev) => prev.filter((msg) => msg.id !== payload.old.id) ) } } ) .subscribe() ``` **Filter Subscriptions:** ```typescript // Only listen to messages in a specific room const channel = supabase .channel(`room:${roomId}`) .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages', filter: `room_id=eq.${roomId}`, }, (payload) => { setMessages((prev) => [...prev, payload.new as Message]) } ) .subscribe() ``` ## Custom Realtime Hook ```typescript // hooks/use-realtime-subscription.ts 'use client' import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' import type { RealtimeChannel } from '@supabase/supabase-js' type UseRealtimeOptions = { table: string filter?: string event?: 'INSERT' | 'UPDATE' | 'DELETE' | '*' onInsert?: (record: T) => void onUpdate?: (record: T) => void onDelete?: (record: T) => void } export function useRealtimeSubscription({ table, filter, event = '*', onInsert, onUpdate, onDelete, }: UseRealtimeOptions) { const supabase = createClient() useEffect(() => { const channel = supabase .channel(`${table}-changes`) .on( 'postgres_changes', { event, schema: 'public', table, filter, }, (payload) => { if (payload.eventType === 'INSERT' && onInsert) { onInsert(payload.new as T) } if (payload.eventType === 'UPDATE' && onUpdate) { onUpdate(payload.new as T) } if (payload.eventType === 'DELETE' && onDelete) { onDelete(payload.old as T) } } ) .subscribe() return () => { supabase.removeChannel(channel) } }, [table, filter, event, onInsert, onUpdate, onDelete, supabase]) } // Usage function Chat({ roomId }: { roomId: string }) { const [messages, setMessages] = useState([]) useRealtimeSubscription({ table: 'messages', filter: `room_id=eq.${roomId}`, event: '*', onInsert: (msg) => setMessages((prev) => [...prev, msg]), onUpdate: (msg) => setMessages((prev) => prev.map((m) => (m.id === msg.id ? msg : m)) ), onDelete: (msg) => setMessages((prev) => prev.filter((m) => m.id !== msg.id)), }) return
{/* Render messages */}
} ``` ## Presence (Who's Online) ### Track User Presence ```typescript 'use client' import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' type PresenceState = { [key: string]: { user_id: string username: string online_at: string }[] } export function usePresence(roomId: string) { const [onlineUsers, setOnlineUsers] = useState({}) const supabase = createClient() useEffect(() => { const channel = supabase.channel(`room:${roomId}`) channel .on('presence', { event: 'sync' }, () => { const state = channel.presenceState() setOnlineUsers(state) }) .subscribe(async (status) => { if (status === 'SUBSCRIBED') { // Get current user const { data: { user }, } = await supabase.auth.getUser() if (user) { // Track presence await channel.track({ user_id: user.id, username: user.email, online_at: new Date().toISOString(), }) } } }) return () => { channel.untrack() supabase.removeChannel(channel) } }, [roomId, supabase]) return onlineUsers } // Usage function ChatRoom({ roomId }: { roomId: string }) { const onlineUsers = usePresence(roomId) const count = Object.keys(onlineUsers).length return (

{count} users online

{Object.values(onlineUsers).map((presences) => presences.map((presence) => (
{presence.username}
)) )}
) } ``` ## Broadcast (Send Custom Messages) ### Real-Time Collaboration ```typescript 'use client' import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' type CursorPosition = { x: number y: number user_id: string username: string } export function CollaborativeCanvas({ roomId }: { roomId: string }) { const [cursors, setCursors] = useState([]) const supabase = createClient() useEffect(() => { const channel = supabase.channel(`canvas:${roomId}`) channel .on('broadcast', { event: 'cursor-move' }, ({ payload }) => { setCursors((prev) => { const filtered = prev.filter( (c) => c.user_id !== payload.user_id ) return [...filtered, payload as CursorPosition] }) }) .subscribe() return () => { supabase.removeChannel(channel) } }, [roomId, supabase]) const handleMouseMove = async (e: React.MouseEvent) => { const { data: { user }, } = await supabase.auth.getUser() if (user) { supabase.channel(`canvas:${roomId}`).send({ type: 'broadcast', event: 'cursor-move', payload: { x: e.clientX, y: e.clientY, user_id: user.id, username: user.email, }, }) } } return (
{cursors.map((cursor) => (
{cursor.username}
))}
) } ``` ## Live Chat Application ### Chat Component ```typescript 'use client' import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' type Message = { id: string content: string user_id: string username: string created_at: string } export function LiveChat({ roomId }: { roomId: string }) { const [messages, setMessages] = useState([]) const [newMessage, setNewMessage] = useState('') const [typing, setTyping] = useState>(new Set()) const supabase = createClient() useEffect(() => { // Fetch initial messages supabase .from('messages') .select('*') .eq('room_id', roomId) .order('created_at', { ascending: true }) .then(({ data }) => setMessages(data || [])) const channel = supabase.channel(`chat:${roomId}`) // Listen for new messages channel .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'messages', filter: `room_id=eq.${roomId}`, }, (payload) => { setMessages((prev) => [...prev, payload.new as Message]) } ) // Listen for typing indicators .on('broadcast', { event: 'typing' }, ({ payload }) => { setTyping((prev) => new Set(prev).add(payload.user_id)) setTimeout(() => { setTyping((prev) => { const next = new Set(prev) next.delete(payload.user_id) return next }) }, 3000) }) .subscribe() return () => { supabase.removeChannel(channel) } }, [roomId, supabase]) const sendMessage = async () => { if (!newMessage.trim()) return const { data: { user }, } = await supabase.auth.getUser() if (!user) return await supabase.from('messages').insert({ content: newMessage, room_id: roomId, user_id: user.id, username: user.email, }) setNewMessage('') } const handleTyping = async () => { const { data: { user }, } = await supabase.auth.getUser() if (user) { supabase.channel(`chat:${roomId}`).send({ type: 'broadcast', event: 'typing', payload: { user_id: user.id }, }) } } return (
{messages.map((msg) => (
{msg.username}: {msg.content}
))}
{typing.size > 0 && (

{typing.size} {typing.size === 1 ? 'person is' : 'people are'} typing...

)}
{ setNewMessage(e.target.value) handleTyping() }} onKeyDown={(e) => e.key === 'Enter' && sendMessage()} placeholder="Type a message..." />
) } ``` ## Live Notifications ### Notification System ```typescript 'use client' import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' type Notification = { id: string title: string message: string read: boolean created_at: string } export function NotificationBell() { const [notifications, setNotifications] = useState([]) const [unreadCount, setUnreadCount] = useState(0) const supabase = createClient() useEffect(() => { const fetchNotifications = async () => { const { data: { user }, } = await supabase.auth.getUser() if (!user) return const { data } = await supabase .from('notifications') .select('*') .eq('user_id', user.id) .order('created_at', { ascending: false }) setNotifications(data || []) setUnreadCount(data?.filter((n) => !n.read).length || 0) } fetchNotifications() const channel = supabase .channel('notifications') .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'notifications', }, (payload) => { setNotifications((prev) => [payload.new as Notification, ...prev]) setUnreadCount((prev) => prev + 1) // Show browser notification if ('Notification' in window && Notification.permission === 'granted') { new Notification(payload.new.title, { body: payload.new.message, }) } } ) .subscribe() return () => { supabase.removeChannel(channel) } }, [supabase]) const markAsRead = async (id: string) => { await supabase .from('notifications') .update({ read: true }) .eq('id', id) setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, read: true } : n)) ) setUnreadCount((prev) => prev - 1) } return (
{notifications.map((notification) => (
markAsRead(notification.id)} >

{notification.title}

{notification.message}

))}
) } ``` ## Live Dashboard Updates ### Real-Time Analytics ```typescript 'use client' import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' type Stats = { total_users: number active_sessions: number revenue_today: number } export function LiveDashboard() { const [stats, setStats] = useState({ total_users: 0, active_sessions: 0, revenue_today: 0, }) const supabase = createClient() useEffect(() => { // Fetch initial stats const fetchStats = async () => { const { data } = await supabase.rpc('get_dashboard_stats') setStats(data) } fetchStats() // Listen for relevant table changes const channel = supabase .channel('dashboard-updates') .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'users', }, () => { setStats((prev) => ({ ...prev, total_users: prev.total_users + 1, })) } ) .on( 'postgres_changes', { event: 'INSERT', schema: 'public', table: 'orders', }, (payload) => { setStats((prev) => ({ ...prev, revenue_today: prev.revenue_today + payload.new.amount, })) } ) .subscribe() return () => { supabase.removeChannel(channel) } }, [supabase]) return (

Total Users

{stats.total_users}

Active Sessions

{stats.active_sessions}

Revenue Today

${stats.revenue_today}

) } ``` ## Optimistic Updates ### Instant UI Updates ```typescript 'use client' import { useState } from 'react' import { createClient } from '@/lib/supabase/client' export function TodoList() { const [todos, setTodos] = useState([]) const supabase = createClient() const addTodo = async (text: string) => { // Optimistic update - update UI immediately const tempId = crypto.randomUUID() const optimisticTodo = { id: tempId, text, done: false, created_at: new Date().toISOString(), } setTodos((prev) => [...prev, optimisticTodo]) try { // Insert to database const { data, error } = await supabase .from('todos') .insert({ text, done: false }) .select() .single() if (error) throw error // Replace temp todo with real one setTodos((prev) => prev.map((todo) => (todo.id === tempId ? data : todo)) ) } catch (error) { // Rollback on error setTodos((prev) => prev.filter((todo) => todo.id !== tempId)) console.error('Failed to add todo:', error) } } return
{/* Render todos */}
} ``` ## Connection Status ### Track Connection State ```typescript 'use client' import { useEffect, useState } from 'react' import { createClient } from '@/lib/supabase/client' export function ConnectionStatus() { const [isConnected, setIsConnected] = useState(true) const supabase = createClient() useEffect(() => { const channel = supabase.channel('connection-test') channel .on('system', { event: 'online' }, () => setIsConnected(true)) .on('system', { event: 'offline' }, () => setIsConnected(false)) .subscribe() return () => { supabase.removeChannel(channel) } }, [supabase]) if (!isConnected) { return (
⚠️ Connection lost. Reconnecting...
) } return null } ``` ## Best Practices Checklist - [ ] Clean up subscriptions on unmount - [ ] Handle connection errors gracefully - [ ] Implement optimistic updates for better UX - [ ] Use presence for online status - [ ] Implement typing indicators in chat - [ ] Show connection status to users - [ ] Rate limit broadcast messages - [ ] Use filters to reduce unnecessary updates - [ ] Implement reconnection logic - [ ] Handle duplicate messages - [ ] Use channels efficiently - [ ] Test with slow/offline connections - [ ] Implement message queuing for offline - [ ] Monitor realtime usage/costs ## When to Use This Skill Invoke this skill when: - Building chat applications - Implementing live notifications - Creating collaborative features - Adding presence/online status - Building real-time dashboards - Implementing live updates - Creating multiplayer features - Debugging realtime connections - Optimizing realtime performance - Implementing typing indicators