DEV Community

ANKUSH CHOUDHARY JOHAL
ANKUSH CHOUDHARY JOHAL

Posted on • Originally published at johal.in

Step-by-Step Guide to Implementing Presence Channels with Pusher 3 and Next.js 15 for Chat Apps

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@15 to 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  ],
})
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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,
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Add Pusher public variables to .env.local:

NEXT_PUBLIC_PUSHER_KEY=your_key
NEXT_PUBLIC_PUSHER_CLUSTER=your_cluster
Enter fullscreen mode Exit fullscreen mode

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





  )
}
Enter fullscreen mode Exit fullscreen mode

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 })
}
Enter fullscreen mode Exit fullscreen mode

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)