How to Add Real-Time Features to Your App (2026)
Real-time features — live notifications, collaborative editing, chat, presence — make apps feel alive. Here's how to add them to your Next.js app without building complex WebSocket infrastructure.
The Three Approaches
1. Managed Real-Time (Easiest)
Use Supabase, Liveblocks, or Pusher. They handle the infrastructure. You just subscribe to channels.
2. Server-Sent Events (Simple)
One-way communication (server → client). Good for notifications and live updates.
3. WebSockets (Full Control)
Two-way communication. Build your own real-time infrastructure. More work but maximum flexibility.
Recommendation: Start with Supabase Realtime. You're probably already using Postgres.
Option 1: Supabase Realtime (Best for Most Apps)
Setup (5 minutes)
npm install @supabase/supabase-js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
Real-Time Database Changes
'use client'
import { useEffect, useState } from 'react'
export function LiveTodoList() {
const [todos, setTodos] = useState([])
useEffect(() => {
// Initial fetch
const fetchTodos = async () => {
const { data } = await supabase.from('todos').select('*')
setTodos(data || [])
}
fetchTodos()
// Subscribe to changes
const channel = supabase
.channel('todos')
.on('postgres_changes',
{ event: '*', schema: 'public', table: 'todos' },
(payload) => {
if (payload.eventType === 'INSERT') {
setTodos(prev => [...prev, payload.new])
} else if (payload.eventType === 'DELETE') {
setTodos(prev => prev.filter(t => t.id !== payload.old.id))
} else if (payload.eventType === 'UPDATE') {
setTodos(prev => prev.map(t => t.id === payload.new.id ? payload.new : t))
}
}
)
.subscribe()
return () => { supabase.removeChannel(channel) }
}, [])
return (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
)
}
Every database change is broadcast to subscribed clients instantly.
Presence (Who's Online)
const channel = supabase.channel('room-1')
// Track presence
await channel.subscribe(async (status) => {
if (status === 'SUBSCRIBED') {
await channel.track({
user_id: userId,
online_at: new Date().toISOString(),
})
}
})
// Listen for presence changes
channel.on('presence', { event: 'sync' }, () => {
const state = channel.presenceState()
console.log('Online users:', Object.keys(state))
})
Broadcast (Custom Events)
// Send a message
await channel.send({
type: 'broadcast',
event: 'cursor-move',
payload: { x: 100, y: 200, user: userId },
})
// Receive messages
channel.on('broadcast', { event: 'cursor-move' }, (payload) => {
updateCursor(payload.x, payload.y)
})
Option 2: Server-Sent Events (SSE)
Good for one-way updates (server → client): live notifications, progress updates, streaming responses.
API Route
// app/api/events/route.ts
export async function GET(req: Request) {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
// Send initial connection message
controller.enqueue(encoder.encode('data: Connected\n\n'))
// Send updates every 5 seconds
const interval = setInterval(() => {
const data = { time: new Date().toISOString(), count: Math.random() }
controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\n\n`))
}, 5000)
// Cleanup
req.signal.addEventListener('abort', () => {
clearInterval(interval)
controller.close()
})
},
})
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
}
Client Component
'use client'
import { useEffect, useState } from 'react'
export function LiveUpdates() {
const [data, setData] = useState(null)
useEffect(() => {
const eventSource = new EventSource('/api/events')
eventSource.onmessage = (event) => {
try {
const parsed = JSON.parse(event.data)
setData(parsed)
} catch (e) {
// Initial "Connected" message
}
}
return () => eventSource.close()
}, [])
return <div>Latest: {JSON.stringify(data)}</div>
}
Option 3: WebSockets with Pusher
Setup
npm install pusher pusher-js
// lib/pusher.ts
import PusherServer from 'pusher'
import PusherClient from 'pusher-js'
export const pusherServer = new PusherServer({
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!,
})
export const pusherClient = new PusherClient(
process.env.NEXT_PUBLIC_PUSHER_KEY!,
{ cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER! }
)
Trigger Events (Server)
// app/api/send-notification/route.ts
import { pusherServer } from '@/lib/pusher'
export async function POST(req: Request) {
const { userId, message } = await req.json()
await pusherServer.trigger(`user-${userId}`, 'notification', {
message,
timestamp: new Date().toISOString(),
})
return Response.json({ success: true })
}
Subscribe (Client)
'use client'
import { pusherClient } from '@/lib/pusher'
import { useEffect, useState } from 'react'
export function Notifications({ userId }) {
const [notifications, setNotifications] = useState([])
useEffect(() => {
const channel = pusherClient.subscribe(`user-${userId}`)
channel.bind('notification', (data) => {
setNotifications(prev => [...prev, data])
})
return () => {
channel.unbind_all()
channel.unsubscribe()
}
}, [userId])
return (
<ul>
{notifications.map((n, i) => <li key={i}>{n.message}</li>)}
</ul>
)
}
Common Use Cases
Live Notifications
Use Supabase Broadcast or Pusher. Send notifications to specific users.
Collaborative Editing
Use Liveblocks or Yjs. Purpose-built for collaborative features.
Chat
Use Supabase Realtime or build with Pusher. Store messages in database, broadcast new messages.
Presence ("Who's Online")
Use Supabase Presence or Pusher presence channels.
Live Dashboard
Use SSE or Supabase Realtime. Stream metrics and updates.
Pricing Comparison
| Supabase | Pusher | Liveblocks | |
|---|---|---|---|
| Free tier | 500MB, 50K MAU | 200K messages/day | 100 MAU |
| Paid | $25/mo | $49/mo | $99/mo |
FAQ
WebSockets vs Server-Sent Events?
SSE for one-way (server → client) updates. WebSockets for two-way communication.
Can I use Supabase Realtime without Supabase database?
No. Realtime requires Supabase Postgres. For non-Supabase projects, use Pusher or build WebSockets.
How do I handle reconnection?
Supabase and Pusher handle reconnection automatically. For custom WebSockets, implement exponential backoff reconnection logic.
Bottom Line
Use Supabase Realtime if you're using Supabase. Use Pusher for everything else. Use SSE for simple one-way updates. Don't build custom WebSocket infrastructure unless you have specific needs that managed services don't cover.