Backend realtime
🦖Curated Cursor AI agent skills, slash commands, MCP configs, subagents & rules for full-stack dev — React 19, Next.js 15, Supabase, Tailwind v4, TypeScript
npx -y skills add kensaurus/cursor-kenji --skill backend-realtimeAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Implement real-time features using WebSockets, Supabase Realtime, Server-Sent Events, and live data. Use when user wants "real-time", "live updates", "WebSocket", "notifications", "chat", "collaborative", "presence", "live data", or "instant sync".
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
8.4 KB, as published. Nobody here has run it
Real-time Features Skill
Implement live, collaborative features using WebSockets, Supabase Realtime, and Server-Sent Events.
Code examples assume a Next.js App Router + Supabase stack (
@/lib/supabase/client,'use client'). Adapt import paths and row types to the detected stack.
CRITICAL: Check Existing First
Before implementing ANY real-time feature, verify:
- Check for existing real-time setup:
cat package.json | grep -i "socket\|realtime\|pusher\|ably"
rg "supabase.*channel|useSubscription|WebSocket" --type ts --type tsx
- Check for existing patterns:
rg "on\('INSERT'\|on\('UPDATE'\|subscribe\(" --type ts
ls -la src/hooks/use*Realtime* src/lib/realtime* 2>/dev/null
- Check Supabase config:
rg "createClient|supabaseUrl" --type ts -l
cat .env* | grep -i SUPABASE
Why: Real-time connections are stateful. Don't create duplicate subscriptions.
Supabase Realtime
Subscribe to Database Changes
The canonical useRealtimeMessages hook (initial fetch + INSERT/DELETE
subscription with unmount cleanup) is in
references/patterns.md.
Presence (Online Users)
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@/lib/supabase/client'
import type { RealtimePresenceState } from '@supabase/supabase-js'
interface UserPresence {
id: string
name: string
avatar: string
online_at: string
}
export function usePresence(roomId: string, currentUser: UserPresence) {
const [onlineUsers, setOnlineUsers] = useState<UserPresence[]>([])
const supabase = createClient()
useEffect(() => {
const channel = supabase.channel(`presence:${roomId}`)
channel
.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState<UserPresence>()
const users = Object.values(state).flat()
setOnlineUsers(users)
})
.on('presence', { event: 'join' }, ({ newPresences }) => {
console.log('User joined:', newPresences)
})
.on('presence', { event: 'leave' }, ({ leftPresences }) => {
console.log('User left:', leftPresences)
})
.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track(currentUser)
}
})
return () => {
channel.untrack()
supabase.removeChannel(channel)
}
}, [roomId, currentUser, supabase])
return onlineUsers
}
Broadcast (Custom Events)
'use client'
import { useEffect, useCallback } from 'react'
import { createClient } from '@/lib/supabase/client'
export function useBroadcast(channelName: string) {
const supabase = createClient()
const channelRef = useRef<ReturnType<typeof supabase.channel> | null>(null)
useEffect(() => {
channelRef.current = supabase.channel(channelName)
channelRef.current
.on('broadcast', { event: 'cursor-move' }, ({ payload }) => {
// Handle cursor position updates from others
console.log('Cursor moved:', payload)
})
.on('broadcast', { event: 'typing' }, ({ payload }) => {
// Handle typing indicators
console.log('User typing:', payload)
})
.subscribe()
return () => {
if (channelRef.current) {
supabase.removeChannel(channelRef.current)
}
}
}, [channelName, supabase])
const broadcast = useCallback((event: string, payload: unknown) => {
channelRef.current?.send({
type: 'broadcast',
event,
payload,
})
}, [])
return { broadcast }
}
// Usage: Collaborative cursors
function CollaborativeCanvas() {
const { broadcast } = useBroadcast('canvas:123')
const handleMouseMove = (e: React.MouseEvent) => {
broadcast('cursor-move', {
userId: currentUser.id,
x: e.clientX,
y: e.clientY,
})
}
}
TanStack Query + Real-time
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { useEffect } from 'react'
export function useRealtimeQuery<T>(
queryKey: string[],
queryFn: () => Promise<T>,
table: string,
filter?: string
) {
const queryClient = useQueryClient()
const supabase = createClient()
const query = useQuery({
queryKey,
queryFn,
})
useEffect(() => {
const channel = supabase
.channel(`${table}-changes`)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table,
filter,
},
() => {
// Invalidate and refetch on any change
queryClient.invalidateQueries({ queryKey })
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [table, filter, queryKey, queryClient, supabase])
return query
}
// Usage
function ProductList() {
const { data: products } = useRealtimeQuery(
['products'],
() => fetchProducts(),
'products'
)
}
Optimistic Updates
'use client'
import { useOptimistic, useTransition } from 'react'
import { addMessage } from '@/app/actions'
function Chat({ initialMessages }: { initialMessages: Message[] }) {
const [isPending, startTransition] = useTransition()
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
initialMessages,
(state, newMessage: Message) => [...state, newMessage]
)
async function handleSubmit(formData: FormData) {
const content = formData.get('content') as string
// Optimistic update - instant UI feedback
const optimisticMessage: Message = {
id: crypto.randomUUID(),
content,
created_at: new Date().toISOString(),
user_id: currentUser.id,
pending: true,
}
startTransition(async () => {
addOptimisticMessage(optimisticMessage)
await addMessage(content) // Server Action
})
}
return (
<div>
{optimisticMessages.map((msg) => (
<div key={msg.id} className={msg.pending ? 'opacity-50' : ''}>
{msg.content}
</div>
))}
<form action={handleSubmit}>
<input name="content" />
<button type="submit" disabled={isPending}>Send</button>
</form>
</div>
)
}
Server-Sent Events (SSE)
// app/api/events/route.ts
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const send = (data: unknown) => {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
)
}
// Send initial data
send({ type: 'connected', timestamp: Date.now() })
// Subscribe to changes (e.g., from database)
const interval = setInterval(() => {
send({ type: 'heartbeat', timestamp: Date.now() })
}, 30000)
// Cleanup on close
// Note: In production, use proper cleanup mechanism
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
}
// Client hook
function useSSE(url: string) {
const [data, setData] = useState(null)
useEffect(() => {
const eventSource = new EventSource(url)
eventSource.onmessage = (event) => {
setData(JSON.parse(event.data))
}
eventSource.onerror = () => {
eventSource.close()
// Implement reconnection logic
}
return () => eventSource.close()
}, [url])
return data
}
Typing Indicators
function useTypingIndicator(channelName: string, userId: string) {
const [typingUsers, setTypingUsers] = useState<string[]>([])
const supabase = createClient()
const timeoutRef = useRef<NodeJS.Timeout>()
useEffect(() => {
const channel = supabase.channel(channelName)
channel
.on('broadcast', { event: 'typing' }, ({ payload }) => {
if (payload.userId !== userId) {
setTypingUsers((prev) =>
prev.includes(payload.userId) ? prev : [...prev, payload.userId]
)
// Remove after 3 seconds of no activity
setTimeout(() => {
setTypingUsers((prev) =>
prev.filter((id) => id !== payload.userId)
)
}, 3000)
}
})
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [channelName, userId, supabase])
const sendTyping = useCallback(() => {
clearTimeout(timeoutRef.current)
supabase.channel(channelName).send({
type: 'broadcast',
event: 'typing',
payload: { userId },
})
}, [channelName, userId, supabase])
return { typingUsers, sendTyping }
}
Validation
After implementing real-time features:
- Connection handling → Reconnects on disconnect, shows status
- Error handling → Graceful degradation when real-time unavailable
- Memory leaks → All subscriptions cleaned up on unmount
- Duplicate subscriptions → No multiple listeners for same event
- Optimistic updates → UI feels instant, handles conflicts
- Mobile → Works on spotty connections, battery efficient