mcpField
R
real-time-features

real-time-features

github.com/jmsktmDevelopment1066 installsUpdated 41w ago
Repository

About real-time-features

real-time-features is a skill for developers adding real-time behavior to Next.js applications. It focuses on Supabase Realtime patterns for subscriptions, presence, broadcasts, and interactive features.

Key features

  1. 01Supabase Realtime subscriptions to table changes
  2. 02Filtered event subscriptions
  3. 03Presence tracking for online users
  4. 04Broadcast messages for collaboration
  5. 05Examples for chat, notifications, and dashboards
  6. 06Connection status and optimistic updates

Use cases

  1. 01Building live chat and notifications
  2. 02Showing who is online
  3. 03Adding collaborative interactions
  4. 04Updating dashboards from live data

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/jmsktm/claude-settings --skill real-time-features

After installing, type/real-time-featuresin Claude Code to confirm it loaded.

Details

From the project README

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
// 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:

'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'

export function RealtimeMessages() {
  const [messages, setMessages] = useState<Message[]>([])
  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 (
    <div>
      {messages.map((msg) => (
        <div key={msg.id}>{msg.content}</div>
      ))}
    </div>
  )
}

Subscribe to All Events:

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:

// 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

// 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<T> = {
  table: string
  filter?: string
  event?: 'INSERT' | 'UPDATE' | 'DELETE' | '*'
  onInsert?: (record: T) => void
  onUpdate?: (record: T) => void
  onDelete?: (record: T) => void
}

export function useRealtimeSubscription<T>({
  table,
  filter,
  event = '*',
  onInsert,
  onUpdate,
  onDelete,
}: UseRealtimeOptions<T>) {
  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<Message[]>([])

  useRealtimeSubscription<Message>({
    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 <div>{/* Render messages */}</div>
}

Presence (Who's Online)

Track User Presence
'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<PresenceState>({})
  const supabase = createClient()

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

    channel
      .on('presence', { event: 'sync' }, () => {
        const state = channel.presenceState<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 (
    <div>
      <p>{count} users online</p>
      {Object.values(onlineUsers).map((presences) =>
        presences.map((presence) => (
          <div key={presence.user_id}>{presence.username}</div>
        ))
      )}
    </div>
  )
}

Broadcast (Send Custom Messages)

Real-Time Collaboration
'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<CursorPosition[]>([])
  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 (
    <div onMouseMove={handleMouseMove}>
      {cursors.map((cursor) => (
        <div
          key={cursor.user_id}
          style={{
            position: 'absolute',
            left: cursor.x,
            top: cursor.y,
            width: 10,
            height: 10,
            borderRadius: '50%',
            backgroundColor: 'blue',
            pointerEvents: 'none',
          }}
        >
          <span>{cursor.username}</span>
        </div>
      ))}
    </div>
  )
}

Live Chat Application

Chat Component
'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<Message[]>([])
  const [newMessage, setNewMessage] = useState('')
  const [typing, setTyping] = useState<Set<string>>(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 (
    <div>
      <div className="h-96 overflow-y-auto">
        {messages.map((msg) => (
          <div key={msg.id}>
            <strong>{msg.username}:</strong> {msg.content}
          </div>
        ))}
      </div>

      {typing.size > 0 && (
        <p className="text-sm text-gray-500">
          {typing.size} {typing.size === 1 ? 'person is' : 'people are'} typing...
        </p>
      )}

      <div className="flex gap-2">
        <input
          value={newMessage}
          onChange={(e) => {
            setNewMessage(e.target.value)
            handleTyping()
          }}
          onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Type a message..."
        />
        <button onClick={sendMessage}>Send</button>
      </div>
    </div>
  )
}

Live Notifications

Notification System
'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<Notification[]>([])
  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 (
    <div>
      <button className="relative">
        🔔
        {unreadCount > 0 && (
          <span className="absolute -top-1 -right-1 bg-red-500 text-white rounded-full w-5 h-5 text-xs">
            {unreadCount}
          </span>
        )}
      </button>

      <div>
        {notifications.map((notification) => (
          <div
            key={notification.id}
            className={notification.read ? 'opacity-50' : ''}
            onClick={() => markAsRead(notification.id)}
          >
            <h4>{notification.title}</h4>
            <p>{notification.message}</p>
          </div>
        ))}
      </div>
    </div>
  )
}

Live Dashboard Updates

Real-Time Analytics
'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<Stats>({
    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 (
    <div className="grid grid-cols-3 gap-4">
      <div className="p-4 bg-white rounded shadow">
        <h3>Total Users</h3>
        <p className="text-3xl font-bold">{stats.total_users}</p>
      </div>
      <div className="p-4 bg-white rounded shadow">
        <h3>Active Sessions</h3>
        <p className="text-3xl font-bold">{stats.active_sessions}</p>
      </div>
      <div className="p-4 bg-white rounded shadow">
        <h3>Revenue Today</h3>
        <p className="text-3xl font-bold">${stats.revenue_today}</p>
      </div>
    </div>
  )
}

Optimistic Updates

Instant UI Updates
'use client'
import { useState } from 'react'
import { createClient } from '@/lib/supabase/client'

export function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([])
  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 <div>{/* Render todos */}</div>
}

Connection Status

Track Connection State
'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 (
      <div className="bg-red-500 text-white p-2 text-center">
        ⚠️ Connection lost. Reconnecting...
      </div>
    )
  }

  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

Comments

Markdown supported