DEV Community

Atlas Whoff
Atlas Whoff

Posted on

WebSocket Real-Time Features in Next.js: Chat, Notifications, and Live Updates

WebSocket Real-Time Features in Next.js: Chat, Notifications, and Live Updates

Adding real-time to a Next.js app is trickier than in a traditional Node server.
Here are the patterns that work in production.

Option 1: Pusher / Ably (Managed, Easiest)

For most apps, use a managed WebSocket service:

npm install pusher pusher-js
Enter fullscreen mode Exit fullscreen mode
// lib/pusher.ts
import Pusher from 'pusher'
import PusherClient from 'pusher-js'

export const pusherServer = new Pusher({
  appId: process.env.PUSHER_APP_ID!,
  key: process.env.NEXT_PUBLIC_PUSHER_KEY!,
  secret: process.env.PUSHER_SECRET!,
  cluster: process.env.PUSHER_CLUSTER!,
  useTLS: true,
})

export const pusherClient = new PusherClient(
  process.env.NEXT_PUBLIC_PUSHER_KEY!,
  { cluster: process.env.PUSHER_CLUSTER! }
)
Enter fullscreen mode Exit fullscreen mode

Trigger from server action:

async function sendMessage(channelId: string, text: string) {
  const message = await db.message.create({
    data: { channelId, text, userId: session.user.id }
  })

  // Push to all subscribers
  await pusherServer.trigger(
    `channel-${channelId}`,
    'new-message',
    message
  )

  return message
}
Enter fullscreen mode Exit fullscreen mode

Subscribe in client component:

'use client'

function ChatRoom({ channelId }: { channelId: string }) {
  const [messages, setMessages] = useState<Message[]>([])

  useEffect(() => {
    const channel = pusherClient.subscribe(`channel-${channelId}`)

    channel.bind('new-message', (message: Message) => {
      setMessages(prev => [...prev, message])
    })

    return () => pusherClient.unsubscribe(`channel-${channelId}`)
  }, [channelId])

  return <MessageList messages={messages} />
}
Enter fullscreen mode Exit fullscreen mode

Option 2: Server-Sent Events (One-Way, Built-In)

For notifications and live updates (no bidirectional needed):

// app/api/events/route.ts
export async function GET(request: Request) {
  const session = await getServerSession()
  if (!session) return new Response('Unauthorized', { status: 401 })

  const stream = new ReadableStream({
    start(controller) {
      const encoder = new TextEncoder()

      const send = (data: unknown) => {
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
        )
      }

      // Send initial connection
      send({ type: 'connected' })

      // Poll for new events every 2 seconds
      let lastCheck = Date.now()
      const interval = setInterval(async () => {
        const events = await db.notification.findMany({
          where: {
            userId: session.user.id,
            createdAt: { gt: new Date(lastCheck) },
          },
        })
        lastCheck = Date.now()
        if (events.length > 0) send({ type: 'notifications', events })
      }, 2000)

      request.signal.addEventListener('abort', () => {
        clearInterval(interval)
        controller.close()
      })
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  })
}
Enter fullscreen mode Exit fullscreen mode

Client:

function useNotifications() {
  const [notifications, setNotifications] = useState<Notification[]>([])

  useEffect(() => {
    const eventSource = new EventSource('/api/events')

    eventSource.onmessage = (e) => {
      const data = JSON.parse(e.data)
      if (data.type === 'notifications') {
        setNotifications(prev => [...prev, ...data.events])
      }
    }

    return () => eventSource.close()
  }, [])

  return notifications
}
Enter fullscreen mode Exit fullscreen mode

Option 3: Socket.io with Custom Server

For complex bidirectional communication, run Socket.io alongside Next.js:

// server.ts
import { createServer } from 'http'
import { Server } from 'socket.io'
import next from 'next'

const app = next({ dev: process.env.NODE_ENV !== 'production' })
const handler = app.getRequestHandler()

app.prepare().then(() => {
  const httpServer = createServer(handler)
  const io = new Server(httpServer)

  io.on('connection', (socket) => {
    socket.on('join-room', (roomId: string) => {
      socket.join(roomId)
    })

    socket.on('send-message', async ({ roomId, text }) => {
      const message = await saveMessage(roomId, text)
      io.to(roomId).emit('new-message', message)
    })
  })

  httpServer.listen(3000)
})
Enter fullscreen mode Exit fullscreen mode

Choosing the Right Approach

Approach Best For Complexity
Pusher/Ably Most apps, quick setup Low
SSE Notifications, live feeds Low
Socket.io Complex bidirectional High
Native WebSocket Maximum control High

My recommendation: Pusher for 80% of use cases. SSE if you only need server-to-client updates.


The AI SaaS Starter Kit includes Pusher integration and SSE notification patterns pre-configured. $99 one-time.

Top comments (0)