Real-time updates make apps feel alive — live notifications, chat messages, collaborative editing. Here are two approaches: Pusher (managed) and Socket.io (self-hosted).
Option 1: Pusher (Managed)
Pusher handles WebSocket infrastructure for you. Great for serverless deployments.
Install
pnpm add pusher pusher-js
Server-Side Client
// lib/pusher-server.ts
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,
});
Client-Side Hook
// hooks/use-pusher.ts
"use client";
import { useEffect, useRef } from "react";
import PusherClient from "pusher-js";
let pusherInstance: PusherClient | null = null;
function getPusher() {
if (!pusherInstance) {
pusherInstance = new PusherClient(process.env.NEXT_PUBLIC_PUSHER_KEY!, {
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
});
}
return pusherInstance;
}
export function usePusherChannel(
channelName: string,
eventName: string,
callback: (data: unknown) => void
) {
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
const pusher = getPusher();
const channel = pusher.subscribe(channelName);
channel.bind(eventName, (data: unknown) => {
callbackRef.current(data);
});
return () => {
channel.unbind(eventName);
pusher.unsubscribe(channelName);
};
}, [channelName, eventName]);
}
Trigger Events from API Routes
// app/api/messages/route.ts
import { NextRequest, NextResponse } from "next/server";
import { pusher } from "@/lib/pusher-server";
import { z } from "zod";
const MessageSchema = z.object({
channelId: z.string(),
content: z.string().min(1).max(2000),
userId: z.string(),
});
export async function POST(request: NextRequest) {
const body = await request.json();
const parsed = MessageSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Invalid message" }, { status: 400 });
}
const { channelId, content, userId } = parsed.data;
const message = {
id: crypto.randomUUID(),
content,
userId,
createdAt: new Date().toISOString(),
};
// Save to database
// await db.insert(messages).values(message);
// Broadcast to all connected clients
await pusher.trigger(`channel-${channelId}`, "new-message", message);
return NextResponse.json(message, { status: 201 });
}
Listen in Components
"use client";
import { useState, useCallback } from "react";
import { usePusherChannel } from "@/hooks/use-pusher";
interface Message {
id: string;
content: string;
userId: string;
createdAt: string;
}
export function LiveChat({ channelId }: { channelId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
usePusherChannel(
`channel-${channelId}`,
"new-message",
useCallback((data: unknown) => {
setMessages((prev) => [...prev, data as Message]);
}, [])
);
return (
<div className="border rounded-lg">
<div className="h-80 overflow-y-auto p-4 space-y-3">
{messages.map((msg) => (
<div key={msg.id} className="text-sm">
<span className="font-medium">{msg.userId}: </span>
{msg.content}
</div>
))}
</div>
<ChatInput channelId={channelId} />
</div>
);
}
Option 2: Socket.io (Self-Hosted)
For full control, use Socket.io with a custom server.
Install
pnpm add socket.io socket.io-client
Socket.io Server
// server/socket.ts
import { createServer } from "http";
import { Server } from "socket.io";
const httpServer = createServer();
const io = new Server(httpServer, {
cors: {
origin: process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
methods: ["GET", "POST"],
},
});
// Track connected users
const connectedUsers = new Map<string, { socketId: string; userId: string }>();
io.on("connection", (socket) => {
console.log("Client connected:", socket.id);
// User authentication
socket.on("authenticate", (userId: string) => {
connectedUsers.set(socket.id, { socketId: socket.id, userId });
io.emit("users:online", Array.from(connectedUsers.values()));
});
// Join a room
socket.on("room:join", (roomId: string) => {
socket.join(roomId);
socket.to(roomId).emit("room:user-joined", {
socketId: socket.id,
userId: connectedUsers.get(socket.id)?.userId,
});
});
// Leave a room
socket.on("room:leave", (roomId: string) => {
socket.leave(roomId);
socket.to(roomId).emit("room:user-left", {
socketId: socket.id,
});
});
// Send message to room
socket.on(
"message:send",
(data: { roomId: string; content: string }) => {
const user = connectedUsers.get(socket.id);
if (!user) return;
const message = {
id: crypto.randomUUID(),
content: data.content,
userId: user.userId,
createdAt: new Date().toISOString(),
};
io.to(data.roomId).emit("message:new", message);
}
);
// Typing indicator
socket.on("typing:start", (roomId: string) => {
const user = connectedUsers.get(socket.id);
if (!user) return;
socket.to(roomId).emit("typing:user", { userId: user.userId });
});
socket.on("typing:stop", (roomId: string) => {
const user = connectedUsers.get(socket.id);
if (!user) return;
socket.to(roomId).emit("typing:clear", { userId: user.userId });
});
socket.on("disconnect", () => {
connectedUsers.delete(socket.id);
io.emit("users:online", Array.from(connectedUsers.values()));
});
});
httpServer.listen(3001, () => {
console.log("Socket.io server running on port 3001");
});
Client Hook
// hooks/use-socket.ts
"use client";
import { useEffect, useRef, useState } from "react";
import { io, Socket } from "socket.io-client";
let socket: Socket | null = null;
function getSocket() {
if (!socket) {
socket = io(process.env.NEXT_PUBLIC_SOCKET_URL ?? "http://localhost:3001");
}
return socket;
}
export function useSocket() {
const [connected, setConnected] = useState(false);
const socketRef = useRef(getSocket());
useEffect(() => {
const s = socketRef.current;
s.on("connect", () => setConnected(true));
s.on("disconnect", () => setConnected(false));
return () => {
s.off("connect");
s.off("disconnect");
};
}, []);
return { socket: socketRef.current, connected };
}
export function useSocketEvent<T>(event: string, callback: (data: T) => void) {
const callbackRef = useRef(callback);
callbackRef.current = callback;
const { socket } = useSocket();
useEffect(() => {
function handler(data: T) {
callbackRef.current(data);
}
socket.on(event, handler);
return () => {
socket.off(event, handler);
};
}, [socket, event]);
}
Typing Indicator Component
"use client";
import { useEffect, useRef, useState } from "react";
import { useSocket, useSocketEvent } from "@/hooks/use-socket";
export function TypingIndicator({ roomId }: { roomId: string }) {
const [typingUsers, setTypingUsers] = useState<string[]>([]);
const { socket } = useSocket();
const timeoutsRef = useRef(new Map<string, NodeJS.Timeout>());
useSocketEvent<{ userId: string }>("typing:user", ({ userId }) => {
setTypingUsers((prev) => (prev.includes(userId) ? prev : [...prev, userId]));
// Clear after 3 seconds
const existing = timeoutsRef.current.get(userId);
if (existing) clearTimeout(existing);
timeoutsRef.current.set(
userId,
setTimeout(() => {
setTypingUsers((prev) => prev.filter((id) => id !== userId));
timeoutsRef.current.delete(userId);
}, 3000)
);
});
useSocketEvent<{ userId: string }>("typing:clear", ({ userId }) => {
setTypingUsers((prev) => prev.filter((id) => id !== userId));
const existing = timeoutsRef.current.get(userId);
if (existing) {
clearTimeout(existing);
timeoutsRef.current.delete(userId);
}
});
if (typingUsers.length === 0) return null;
const text =
typingUsers.length === 1
? `${typingUsers[0]} is typing...`
: typingUsers.length === 2
? `${typingUsers[0]} and ${typingUsers[1]} are typing...`
: `${typingUsers.length} people are typing...`;
return (
<p className="text-xs text-muted-foreground animate-pulse px-4 py-1">
{text}
</p>
);
}
Which to Choose?
| Feature | Pusher | Socket.io |
|---|---|---|
| Setup complexity | Low | Medium |
| Serverless compatible | Yes | No (needs server) |
| Cost | Pay per message | Server hosting |
| Scalability | Managed | DIY |
| Latency | ~50ms | ~10ms |
| Full control | Limited | Full |
- Use Pusher for serverless deployments, simple real-time features, and when you want managed infrastructure.
- Use Socket.io when you need full control, lower latency, complex real-time logic, or want to avoid per-message costs.
Need Real-Time Features?
We build real-time applications with WebSocket, live collaboration, and instant notifications. Contact us to add real-time capabilities to your app.