Use when you have a written implementation plan to execute in a separate session with review checkpoints
npx skills add soilmass/vibe-coding-plugin --skill "real-time"
Install specific skill from multi-skill repository
# Description
>
# SKILL.md
name: real-time
description: >
Real-time patterns — Server-Sent Events (SSE), WebSocket via Pusher/Ably, polling fallback, progress streaming for long tasks
allowed-tools: Read, Grep, Glob
Real-Time
Purpose
Real-time communication patterns for Next.js 15. Covers SSE with route handlers, WebSocket
via hosted services, and polling fallback. The ONE skill for live updates.
When to Use
- Streaming progress for long-running tasks
- Live notifications or chat features
- Real-time dashboards or feeds
- Server-to-client push updates
When NOT to Use
- Static data fetching →
nextjs-data - Form submissions →
react-server-actions - Periodic data refresh →
cachingwith revalidation
Pattern
Server-Sent Events (SSE) route handler
// app/api/events/route.ts
import { NextRequest } from "next/server";
export async function GET(request: NextRequest) {
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 connection event
send({ type: "connected" });
// Example: stream progress
for (let i = 0; i <= 100; i += 10) {
send({ type: "progress", value: i });
await new Promise((r) => setTimeout(r, 500));
}
send({ type: "complete" });
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
});
}
EventSource client component
"use client";
import { useEffect, useState } from "react";
export function ProgressStream({ taskId }: { taskId: string }) {
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState<"connecting" | "streaming" | "done">("connecting");
useEffect(() => {
const source = new EventSource(`/api/events?taskId=${taskId}`);
source.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "progress") {
setProgress(data.value);
setStatus("streaming");
}
if (data.type === "complete") {
setStatus("done");
source.close();
}
};
source.onerror = () => {
// Reconnect with backoff
source.close();
setTimeout(() => setStatus("connecting"), 3000);
};
return () => source.close();
}, [taskId]);
return <progress value={progress} max={100} />;
}
Pusher (WebSocket) integration
// lib/pusher.ts (server)
import "server-only";
import Pusher from "pusher";
export const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID!,
key: process.env.NEXT_PUBLIC_PUSHER_KEY!,
secret: process.env.PUSHER_SECRET!,
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
useTLS: true,
});
// Trigger from Server Action
export async function notifyUser(userId: string, event: string, data: unknown) {
await pusher.trigger(`user-${userId}`, event, data);
}
SSE auth strategy
// EventSource does NOT support custom headers.
// Workaround: pass a short-lived token via URL query parameter.
// ⚠️ Security: use a dedicated SSE token, not the session token.
// Server: generate SSE token
export async function generateSseToken(userId: string) {
const token = crypto.randomUUID();
await redis.set(`sse:${token}`, userId, { ex: 60 }); // 60s expiry
return token;
}
// Server: validate in SSE route
export async function GET(request: NextRequest) {
const token = request.nextUrl.searchParams.get("token");
if (!token) return new Response("Unauthorized", { status: 401 });
const userId = await redis.get(`sse:${token}`);
if (!userId) return new Response("Unauthorized", { status: 401 });
await redis.del(`sse:${token}`); // Single use
// ... create SSE stream scoped to userId
}
// Client: fetch token then connect
const token = await getSseToken(); // Server Action
const source = new EventSource(`/api/events?token=${token}`);
Anti-pattern
// WRONG: WebSockets on Vercel Edge (not supported)
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 }); // Fails on serverless!
// WRONG: aggressive polling without backoff
setInterval(() => fetch("/api/status"), 100); // 10 req/sec = abuse
Vercel and similar serverless platforms don't support raw WebSocket servers.
Use hosted services (Pusher, Ably) or SSE instead. Always use exponential
backoff for polling.
Common Mistakes
- Using raw WebSockets on serverless platforms — use SSE or Pusher
- No reconnection logic for EventSource — connections drop
- Aggressive polling without backoff — wastes resources
- Not closing EventSource on component unmount — memory leak
- Missing
Cache-Control: no-cacheon SSE responses
Checklist
- [ ] SSE route handler sets proper headers (text/event-stream, no-cache)
- [ ] EventSource cleaned up on unmount
- [ ] Reconnection with exponential backoff on errors
- [ ] WebSocket via hosted service (Pusher/Ably) not raw
ws - [ ] Polling uses reasonable interval with backoff
Composes With
api-routes— SSE endpoints are route handlersreact-client-components— EventSource runs in client componentsperformance— avoid aggressive polling that wastes bandwidthstate-management— real-time data updates client statelogging— log connection lifecycle eventserror-handling— handle connection failures and reconnection
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.