Best npm Packages for Real-Time Features 2026: Socket.io vs PartyKit vs Yjs
·PkgPulse Team
TL;DR
Socket.io is still the easiest WebSocket solution for Node.js servers. PartyKit is the 2026 edge-native option (Cloudflare Durable Objects — no server management). Yjs is essential for collaborative editing (conflict-free replicated data types). For simple chat/notifications: Socket.io or Ably. For Google Docs-like collaborative editing: Yjs + PartyKit or y-websocket.
Key Takeaways
- Socket.io: 9M downloads/week, rooms, namespaces, fallback transports, Node.js native
- PartyKit: Cloudflare Durable Objects-based, persistent WebSocket state, serverless
- Yjs: CRDT library for collaborative editing (text, drawings, rich content)
- Ably: Managed WebSocket service, guaranteed delivery, pub/sub at scale
- 2026 pattern: PartyKit + Yjs for collaborative apps; Socket.io for chat/events
Downloads
| Package | Weekly Downloads | Trend |
|---|---|---|
socket.io | ~9M | → Stable |
yjs | ~3M | ↑ Growing |
partykit | ~50K | ↑ Fast growing |
ably | ~1M | ↑ Growing |
Socket.io: The Standard WebSocket
npm install socket.io socket.io-client
// server/socket.ts — Socket.io with Express:
import { Server } from 'socket.io';
import { createServer } from 'http';
import { app } from './app';
const httpServer = createServer(app);
const io = new Server(httpServer, {
cors: { origin: process.env.FRONTEND_URL, credentials: true },
transports: ['websocket', 'polling'], // Fallback to polling
});
// Middleware for auth:
io.use(async (socket, next) => {
const token = socket.handshake.auth.token;
const user = await verifyToken(token);
if (!user) return next(new Error('Unauthorized'));
socket.data.userId = user.id;
next();
});
io.on('connection', (socket) => {
const userId = socket.data.userId;
// Join user's rooms:
socket.join(`user:${userId}`);
socket.on('join-channel', (channelId: string) => {
socket.join(`channel:${channelId}`);
io.to(`channel:${channelId}`).emit('user-joined', { userId, channelId });
});
socket.on('send-message', async ({ channelId, content }: { channelId: string; content: string }) => {
// Save to DB:
const message = await db.message.create({
data: { channelId, senderId: userId, content },
include: { sender: { select: { id: true, name: true } } },
});
// Broadcast to channel:
io.to(`channel:${channelId}`).emit('new-message', message);
});
socket.on('typing', ({ channelId }: { channelId: string }) => {
socket.to(`channel:${channelId}`).emit('user-typing', { userId });
});
socket.on('disconnect', () => {
io.to(`user:${userId}`).emit('user-offline', { userId });
});
});
httpServer.listen(3001);
// React client with Socket.io:
import { io, Socket } from 'socket.io-client';
import { useEffect, useRef, useState } from 'react';
function useChatSocket(channelId: string, token: string) {
const socketRef = useRef<Socket | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [onlineUsers, setOnlineUsers] = useState<string[]>([]);
useEffect(() => {
const socket = io(process.env.NEXT_PUBLIC_WS_URL!, {
auth: { token },
transports: ['websocket'],
});
socket.emit('join-channel', channelId);
socket.on('new-message', (message: Message) => {
setMessages(prev => [...prev, message]);
});
socket.on('user-joined', ({ userId }: { userId: string }) => {
setOnlineUsers(prev => [...new Set([...prev, userId])]);
});
socketRef.current = socket;
return () => { socket.disconnect(); };
}, [channelId, token]);
const sendMessage = (content: string) => {
socketRef.current?.emit('send-message', { channelId, content });
};
return { messages, sendMessage, onlineUsers };
}
PartyKit: Edge-Native WebSockets
npm install partykit
// party/chatroom.ts — PartyKit server (runs on Cloudflare):
import type * as Party from 'partykit/server';
interface Message {
type: 'message' | 'join' | 'leave';
content?: string;
user: string;
timestamp: string;
}
export default class ChatRoom implements Party.Server {
messages: Message[] = [];
constructor(readonly room: Party.Room) {}
// Called when WebSocket connects:
onConnect(conn: Party.Connection, ctx: Party.ConnectionContext) {
const url = new URL(ctx.request.url);
const user = url.searchParams.get('user') ?? 'Anonymous';
// Send message history to new connection:
conn.send(JSON.stringify({ type: 'history', messages: this.messages }));
// Broadcast join:
const joinMsg: Message = { type: 'join', user, timestamp: new Date().toISOString() };
this.room.broadcast(JSON.stringify(joinMsg), [conn.id]);
}
// Called when message received:
onMessage(message: string, sender: Party.Connection) {
const { content, user } = JSON.parse(message);
const msg: Message = { type: 'message', content, user, timestamp: new Date().toISOString() };
// Store last 50 messages in Durable Object storage:
this.messages = [...this.messages.slice(-49), msg];
// Broadcast to everyone in room:
this.room.broadcast(JSON.stringify(msg));
}
onClose(conn: Party.Connection) {
// Handle disconnect
}
}
// Client with PartySocket:
import PartySocket from 'partysocket';
const socket = new PartySocket({
host: 'my-project.myusername.partykit.dev',
room: `channel-${channelId}`,
query: { user: currentUser.name },
});
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'history') setMessages(data.messages);
if (data.type === 'message') setMessages(prev => [...prev, data]);
};
socket.send(JSON.stringify({ content: 'Hello!', user: currentUser.name }));
Yjs: Collaborative Editing (CRDT)
npm install yjs y-websocket @tiptap/extension-collaboration
// Tiptap collaborative editor with Yjs:
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Collaboration from '@tiptap/extension-collaboration';
import CollaborationCursor from '@tiptap/extension-collaboration-cursor';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
function CollaborativeEditor({ documentId }: { documentId: string }) {
const ydoc = useMemo(() => new Y.Doc(), [documentId]);
const provider = useMemo(() => {
return new WebsocketProvider(
'wss://your-y-websocket-server.com',
`document-${documentId}`,
ydoc
);
}, [documentId]);
const editor = useEditor({
extensions: [
StarterKit.configure({ history: false }),
Collaboration.configure({ document: ydoc }),
CollaborationCursor.configure({
provider,
user: { name: currentUser.name, color: '#2563eb' },
}),
],
});
useEffect(() => {
return () => { provider.destroy(); ydoc.destroy(); };
}, []);
return <EditorContent editor={editor} />;
}
# Self-host y-websocket server:
npx y-websocket-server --host localhost --port 1234
# Or with PartyKit (managed Durable Objects for Yjs):
# npm install y-partykit
Decision Guide
Use Socket.io if:
→ Chat, notifications, live updates
→ Node.js server (Express, Fastify, Hono)
→ Need rooms, namespaces, reconnection handling
→ Most common use case
Use PartyKit if:
→ Serverless / Cloudflare Workers deployment
→ Need persistent WebSocket state (Durable Objects)
→ Building multiplayer games or collaborative tools
→ Don't want to manage WebSocket servers
Use Yjs if:
→ Collaborative text editing (Google Docs-like)
→ Shared drawing canvases
→ Any conflict-free multi-user data
→ Combine with Socket.io/PartyKit for transport
Use Ably if:
→ Need managed service with guaranteed delivery
→ 6M messages/month free tier
→ Fan-out to thousands of connections
→ Need pub/sub without managing infrastructure
Compare Socket.io, Ably, and real-time libraries on PkgPulse.