Introduction
Real-time features like chat, notifications, and live updates can differentiate your product. But implementing real-time can be expensive. This guide shows you how to add real-time features on a budget.
Options Comparison
# Real-time solutions
solutions:
- name: "Supabase Realtime"
type: "Built-in with database"
complexity: "Very Low"
pricing: "Free (with Supabase)"
best_for: "Using Supabase already"
- name: "Pusher"
type: "Managed WebSocket"
complexity: "Low"
pricing: "$0-50/month"
best_for: "Any stack"
- name: "Socket.io"
type: "Self-hosted"
complexity: "Medium"
pricing: "Hosting costs"
best_for: "Full control needed"
- name: "Ably"
type: "Managed WebSocket"
complexity: "Low"
pricing: "$0-100/month"
best_for: "Enterprise features"
Supabase Realtime
Quick Start
// Subscribe to database changes
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
// Subscribe to table changes
const channel = supabase
.channel('custom-insert-channel')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
},
(payload) => {
console.log('New message:', payload.new)
}
)
.subscribe()
Chat Implementation
// components/ChatRoom.tsx
'use client'
import { useEffect, useState } from 'react'
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
interface Message {
id: string
content: string
user_id: string
created_at: string
}
export function ChatRoom({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([])
const [newMessage, setNewMessage] = useState('')
useEffect(() => {
// Load initial messages
loadMessages()
// Subscribe to new messages
const channel = supabase
.channel(`room-${roomId}`)
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `room_id=eq.${roomId}`,
},
(payload) => {
setMessages((prev) => [...prev, payload.new as Message])
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [roomId])
async function loadMessages() {
const { data } = await supabase
.from('messages')
.select('*')
.eq('room_id', roomId)
.order('created_at', { ascending: true })
if (data) setMessages(data)
}
async function sendMessage(e: React.FormEvent) {
e.preventDefault()
const { error } = await supabase.from('messages').insert({
room_id: roomId,
content: newMessage,
user_id: (await supabase.auth.getUser()).data.user?.id,
})
if (!error) setNewMessage('')
}
return (
<div>
<div className="h-96 overflow-y-auto">
{messages.map((msg) => (
<div key={msg.id}>{msg.content}</div>
))}
</div>
<form onSubmit={sendMessage}>
<input
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Type a message..."
/>
<button type="submit">Send</button>
</form>
</div>
)
}
Pusher
Setup
# Install Pusher
npm install pusher pusher-js
// 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.NEXT_PUBLIC_PUSHER_CLUSTER!,
useTLS: true,
})
export const pusherClient = new PusherClient(
process.env.NEXT_PUBLIC_PUSHER_KEY!,
{
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
}
)
Server: Trigger Events
// api/chat/send-message/route.ts
import { pusherServer } from '@/lib/pusher'
import { NextResponse } from 'next/server'
export async function POST(req: Request) {
const { message, roomId } = await req.json()
// Save to database
await saveMessage({ message, roomId })
// Trigger real-time update
await pusherServer.trigger(`chat-${roomId}`, 'new-message', {
message,
})
return NextResponse.json({ success: true })
}
Client: Subscribe
// components/Chat.tsx
'use client'
import { useEffect, useState } from 'react'
import { pusherClient } from '@/lib/pusher'
export function ChatComponent({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<string[]>([])
useEffect(() => {
const channel = pusherClient.subscribe(`chat-${roomId}`)
channel.bind('new-message', (data: { message: string }) => {
setMessages((prev) => [...prev, data.message])
})
return () => {
pusherClient.unsubscribe(`chat-${roomId}`)
}
}, [roomId])
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg}</div>
))}
</div>
)
}
Pricing Comparison
# Real-time pricing (2025)
supabase:
free: "50K monthly active users"
pro: "$25/month with 200K MAU"
pusher:
free: "100K messages/day, 50 connections"
free_tier: "Chat, Presence features"
sandbox: "$0/month"
hobby: "$25/month"
socket.io:
hosting: "Your server costs"
# Can be free on existing server
ably:
free: "6M messages/month, 100 peak connections"
starter: "$50/month"
Key Takeaways
- Supabase Realtime - Free if using Supabase
- Pusher - Best for any stack
- Socket.io - Full control, requires hosting
Comments