agentsclimarketplace

Backend realtime

Skill kensaurus/cursor-kenji/skills/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

Install
npx -y skills add kensaurus/cursor-kenji --skill backend-realtime

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

  1. Check for existing real-time setup:
cat package.json | grep -i "socket\|realtime\|pusher\|ably"
rg "supabase.*channel|useSubscription|WebSocket" --type ts --type tsx
  1. Check for existing patterns:
rg "on\('INSERT'\|on\('UPDATE'\|subscribe\(" --type ts
ls -la src/hooks/use*Realtime* src/lib/realtime* 2>/dev/null
  1. 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:

  1. Connection handling → Reconnects on disconnect, shows status
  2. Error handling → Graceful degradation when real-time unavailable
  3. Memory leaks → All subscriptions cleaned up on unmount
  4. Duplicate subscriptions → No multiple listeners for same event
  5. Optimistic updates → UI feels instant, handles conflicts
  6. Mobile → Works on spotty connections, battery efficient

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.