Skip to main content

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

PackageWeekly DownloadsTrend
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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.