Step-by-Step Guide to Implementing Presence Channels with Pusher 3 and Next.js 15 for Chat Apps
Real-time chat apps rely on presence channels to track online users, sync member states, and enable interactive features. This guide walks through integrating Pusher 3 presence channels with Next.js 15 (App Router) to build a functional chat app with user presence tracking.
Prerequisites
- Node.js 18+ installed
- Pusher account (sign up for free at pusher.com)
- Basic knowledge of Next.js 15 App Router and React
- Next.js 15 project initialized (run
npx create-next-app@15to start)
Step 1: Set Up Pusher 3 Account and App
Log into your Pusher dashboard, create a new app, select "React" as the front-end tech and "Node.js" as the back-end tech. Note your app_id, key, secret, and cluster from the App Keys section. Enable presence channels in the app settings (toggle "Presence" under Channel Types).
Step 2: Install Dependencies
Install required packages in your Next.js project:
npm install pusher@3 pusher-js@3 next-auth@5 @prisma/client@5 prisma@5
We use Pusher 3 server/client SDKs, NextAuth v5 for user authentication (required for presence channel user identification), and Prisma for user data persistence.
Step 3: Configure Environment Variables
Create a .env.local file in your project root with the following variables:
PUSHER_APP_ID=your_app_id
PUSHER_KEY=your_key
PUSHER_SECRET=your_secret
PUSHER_CLUSTER=your_cluster
DATABASE_URL="your_postgres_database_url"
NEXTAUTH_SECRET=your_random_secret
NEXTAUTH_URL=http://localhost:3000
Replace placeholder values with your actual Pusher credentials and database URL. Generate a NextAuth secret with openssl rand -base64 32.
Step 4: Set Up Authentication with NextAuth v5
Initialize Prisma: run npx prisma init, update the schema with a User model, then run npx prisma migrate dev. Configure NextAuth to use Prisma adapter for user sessions. Create a auth.ts file in the root:
import NextAuth from "next-auth"
import PrismaAdapter from "@auth/prisma-adapter"
import { PrismaClient } from "@prisma/client"
const prisma = new PrismaClient()
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
// Add your auth providers (e.g., Credentials, Google) here
],
})
Create a route handler at app/api/auth/[...nextauth]/route.ts to expose NextAuth endpoints.
Step 5: Create Pusher Server Instance
Create a Pusher server instance in a shared module (e.g., lib/pusher.ts):
import Pusher from "pusher"
export const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID!,
key: process.env.PUSHER_KEY!,
secret: process.env.PUSHER_SECRET!,
cluster: process.env.PUSHER_CLUSTER!,
useTLS: true,
})
Step 6: Implement Presence Channel Authentication Endpoint
Presence channels require server-side authentication to validate users and attach user data. Create a POST endpoint at app/api/pusher/auth/route.ts:
import { auth } from "@/auth"
import { pusher } from "@/lib/pusher"
import { NextResponse } from "next/server"
export async function POST(request: Request) {
const session = await auth()
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
}
const formData = await request.formData()
const socketId = formData.get("socket_id") as string
const channel = formData.get("channel_name") as string
// Validate channel is a presence channel
if (!channel.startsWith("presence-")) {
return NextResponse.json({ error: "Invalid channel" }, { status: 400 })
}
const authResponse = pusher.authorizeChannel(socketId, channel, {
user_id: session.user.id,
user_info: {
name: session.user.name,
email: session.user.email,
image: session.user.image,
},
})
return NextResponse.json(authResponse)
}
Step 7: Initialize Pusher Client in Next.js
Create a Pusher client instance in lib/pusher-client.ts:
import Pusher from "pusher-js"
export const pusherClient = new Pusher(process.env.NEXT_PUBLIC_PUSHER_KEY!, {
cluster: process.env.NEXT_PUBLIC_PUSHER_CLUSTER!,
authEndpoint: "/api/pusher/auth",
auth: {
headers: {
"Cookie": document.cookie,
},
},
})
Add Pusher public variables to .env.local:
NEXT_PUBLIC_PUSHER_KEY=your_key
NEXT_PUBLIC_PUSHER_CLUSTER=your_cluster
Step 8: Build Chat UI with Presence Tracking
Create a chat page at app/chat/page.tsx that subscribes to a presence channel, tracks online members, and sends/receives messages:
"use client"
import { useEffect, useState } from "react"
import { pusherClient } from "@/lib/pusher-client"
import { auth } from "@/auth"
import { useSession } from "next-auth/react"
type Message = { sender: string; content: string; timestamp: number }
type Member = { id: string; name: string; image?: string }
export default function ChatPage() {
const { data: session } = useSession()
const [messages, setMessages] = useState([])
const [members, setMembers] = useState([])
const [input, setInput] = useState("")
useEffect(() => {
if (!session?.user) return
const channel = pusherClient.subscribe("presence-chat")
// Handle new messages
channel.bind("new-message", (data: Message) => {
setMessages((prev) => [...prev, data])
})
// Handle presence events
channel.bind("pusher:subscription_succeeded", (membersData: any) => {
const initialMembers = []
membersData.each((member: any) => {
initialMembers.push({
id: member.id,
name: member.info.name,
image: member.info.image,
})
})
setMembers(initialMembers)
})
channel.bind("pusher:member_added", (member: any) => {
setMembers((prev) => [
...prev,
{ id: member.id, name: member.info.name, image: member.info.image },
])
})
channel.bind("pusher:member_removed", (member: any) => {
setMembers((prev) => prev.filter((m) => m.id !== member.id))
})
return () => {
pusherClient.unsubscribe("presence-chat")
}
}, [session])
const sendMessage = async () => {
if (!input.trim() || !session?.user) return
const message: Message = {
sender: session.user.name!,
content: input,
timestamp: Date.now(),
}
// Send message via API route to trigger Pusher event
await fetch("/api/messages", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(message),
})
setInput("")
}
if (!session) return Please sign in to access chat.
return (
Chat Room
{/* Online Members Sidebar */}
Online ({members.length})
{members.map((member) => (
{member.image && (
)}
{member.name}
))}
{/* Chat Area */}
{messages.map((msg, i) => (
{msg.sender}:
{msg.content}
{new Date(msg.timestamp).toLocaleTimeString()}
))}
setInput(e.target.value)}
onKeyPress={(e) => e.key === "Enter" && sendMessage()}
className="flex-1 border p-2 rounded"
placeholder="Type a message..."
/>
Send
)
}
Step 9: Create Message Trigger Endpoint
Create a POST endpoint at app/api/messages/route.ts to trigger Pusher events when a message is sent:
import { pusher } from "@/lib/pusher"
import { NextResponse } from "next/server"
export async function POST(request: Request) {
const body = await request.json()
const { sender, content, timestamp } = body
await pusher.trigger("presence-chat", "new-message", {
sender,
content,
timestamp,
})
return NextResponse.json({ success: true })
}
Step 10: Test the Implementation
Start your Next.js app with npm run dev, open multiple browser tabs, sign in with different users, and navigate to /chat. You should see online members update in real time, and messages sent in one tab appear in all others.
Conclusion
You now have a functional chat app with Pusher 3 presence channels and Next.js 15, with real-time message syncing and online user tracking. Extend this by adding private messages, read receipts, or typing indicators using Pusher's event system.
Top comments (0)