Featured
💻
VerifiedSimplesilver
ui-builder
Expert guide for building beautiful, responsive UI with React, Tailwind CSS, Shadcn/ui, and modern design patterns. Use when creating components, layouts, animations, or styling.
meta
0v1.0.0
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.
---
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<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:**
```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<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
```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<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
```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<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
```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<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
```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<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
```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<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
```typescript
'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
```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 (
<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
Expert guide for building beautiful, responsive UI with React, Tailwind CSS, Shadcn/ui, and modern design patterns. Use when creating components, layouts, animations, or styling.
Expert guide for Supabase integration - database schemas, RLS policies, auth, Edge Functions, and real-time subscriptions. Use when working with Supabase backend features.
Expert guide for React state management with Zustand, Context, and modern patterns. Use when managing global state, forms, complex UI state, or optimizing re-renders.