Building a Real-Time Chat App in Express.js Using Soketi (Pusher-Compatible WebSockets)
A Step-by-Step Guide to Implementing Real-Time Communication through Pusher Channels
Building a Real-Time Chat App in Express.js Using Soketi (Pusher-Compatible WebSockets)
A Step-by-Step Guide to Implementing Real-Time Communication through Pusher Channels
Real-time applications — such as chat apps, live dashboards, presence systems, or collaborative tools — depend heavily on WebSockets. Soketi provides a blazing-fast, open-source alternative to Pusher and fully supports the Pusher Channels protocol, making it easy to integrate into any Node.js project. In this story, you’ll learn how to integrate real-time broadcasting with Soketi inside an Express.js application and build a fully working chat window with authentication, channels, and real-time message broadcasting.
Non members can read full-story here
1. Install Dependencies
Before getting started, install the required packages:
npm install pusher
npm install -g @soketi/soketi #soketi required Node js v18
2. Creating the Broadcasting Layer
We’ll implement a simple broadcasting system similar to Laravel Echo’s philosophy — using a broadcaster class, channel authorization handlers, and a unified BroadcastManager. Create following files in your project.
app/BroadcastManager.js
const PusherBroadcaster = require("./broadcasters/PusherBroadcaster");
const Pusher = require("pusher");
class BroadcastManager{
static driver;
static connection(){
if(!this.driver){
if(process.env.BROADCAST_DRIVER == 'pusher'){
//console.log("here create driver");
const pusher = new Pusher({
appId: "app-id",
key: "app-key",
secret: "app-secret",
host : '127.0.0.1',
port : 6001,
scheme : 'http',
encrypted : true,
//useTLS : false,
});
this.driver = new PusherBroadcaster(pusher);
}
}else{
// console.log("return default driver");
}
return this.driver;
}
}
module.exports = BroadcastManager;
app/broadcasters/Broadcaster.js
const channels = require("../channels");
class Broadcaster{
constructor(){
this.channels = channels;
}
normalizeChannelName(channel){
const prefixes = ['private-encrypted-', 'private-', 'presence-'];
for (const prefix of prefixes) {
if (channel.startsWith(prefix)) {
return channel.substring(prefix.length);
}
}
return channel;
}
isGuardedChannel=(channel) =>{
const prefixes = ['private-', 'presence-'];
return prefixes.some(prefix => channel.startsWith(prefix));
}
channelNameMatchesPattern = (channel, pattern) => {
// Replace placeholders in the pattern (e.g., {id}) with a wildcard *
const transformedPattern = pattern.replace(/\{(.*?)\}/g, '*');
// Check if the channel matches the transformed pattern
const regex = new RegExp(`^${transformedPattern}$`);
return regex.test(channel);
};
extractParameters = (callback) =>{
if (callback instanceof Function) {
const paramNames = callback.toString().match(/\(([^)]*)\)/)[1].split(',').map(param => param.trim());
return paramNames;
}
// not tested below
// } else if (isString(callback)) {
// return this.extractParametersFromClass(callback);
// }
throw new Error('Invalid callback type');
}
extractChannelKeys = (pattern, channel) => {
// Convert the pattern to a regular expression with named capturing groups
const regexPattern = pattern.replace(/\{(.*?)\}/g, '(?<$1>[^\\.]+)');
// Create the regular expression
const regex = new RegExp(`^${regexPattern}$`);
// Match the channel against the pattern
const match = channel.match(regex);
if (!match) {
return null;
}
// Return the captured keys
return match.groups;
};
}
module.exports = Broadcaster;
app/broadcasters/PusherBroadcaster.js
const Broadcaster = require("./Broadcaster");
class PusherBroadcaster extends Broadcaster{
pusher;
constructor(pusher){
super();
this.pusher = pusher;
}
async broadcast(channels, payload){
const response = await this.pusher.trigger(channels, payload.event, payload.data, {socket_id: payload.socket});
console.log(response);
if(typeof response == 'object' && response.status >= 200 && response.status <= 299 || response === true){
return;
}
throw new Error(response.body ? `Pusher Error : ${response.body}`:'Failed to connect to Pusher.');
}
async auth(req, res){
req.user = {id:1,name:"Alex"};
let channelName = this.normalizeChannelName(req.body.channel_name);
if(this.isGuardedChannel(req.body.channel_name) && !req.user){
return res.status(403).json({error:"Not authorizied"});
}
for(let pattern in this.channels){
if (!this.channelNameMatchesPattern(channelName, pattern)) {
continue;
}
const callback = this.channels[pattern];
const callbackParameters = this.extractParameters(callback);
let channelKeys = this.extractChannelKeys(pattern, channelName);
const args = callbackParameters.map(paramName => {
if(paramName == "user"){
return req.user;
}
if(!channelKeys){
return null;
}
return channelKeys[paramName] || null;
});
let channelResult = await callback(...args);
if(channelResult && channelResult !== false){
let response = this.validAuthenticationResponse(req, channelResult);
return res.status(200).json(response);
}
}
return res.status(403).json({error:"Can't access the channel"})
}
validAuthenticationResponse(req, result){
if(req.body.channel_name.startsWith('private')) {
console.log("in private channel");
return this.decodePusherResponse(req, this.pusher.authorizeChannel(req.body.socket_id, req.body.channel_name));
}
let channelName = this.normalizeChannelName(req.body.channel_name);
return this.decodePusherResponse(
req,
this.pusher.authorizeChannel(req.body.socket_id, req.body.channel_name, {user_id: req.user.id, user_info: result})
)
}
decodePusherResponse(req, pusherRes){
//console.log(pusherRes);
if(!req.body.callback){
return pusherRes;
}
}
}
module.exports = PusherBroadcaster;
app/channels.js
Define channels and authorization:
module.exports = {
"conversation.{id}": async(user, id)=>{
return true;
}
}
3. Create routes
var express = require("express");
var router = express.Router();
const BroadcastManager = require("../../app/broadcasting/BroadcastManager");
router.post("/broadcasting/auth", async function (req, res, next) {
console.log(req.body);
BroadcastManager.connection();
const broadcaster = BroadcastManager.connection();
return broadcaster.auth(req, res);
});
router.post("/chat", async function (req, res, next) {
const broadcaster = BroadcastManager.connection();
let a = await broadcaster.broadcast([req.body.conversation_channel], {
event: "App\\Events\\Chat\\MessageRecived",
data: {username:req.body.username, message:req.body.message},
socket: req.headers["x-socket-id"],// include socket id if you want to broadcast to others, else omit it to broadcast to all
});
console.log(a);
return res.json({});
});
module.exports = router;
4. Frontend Chat UI (HTML + Alpine.js + Echo + Tailwind)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple Chat</title>
<!-- Tailwind CSS CDN -->
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pusher/8.4.0/pusher.min.js" integrity="sha512-p3rR75Is6DCK1r2D8mdxLQhe4IWVDSTUBdxqs0Veum0hHDSY+sH9M6U6Cesr1umlxbiEK9w/3IhXFlZcWT1AoA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<!-- Alpine.js CDN -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
<!-- Axios CDN -->
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
</head>
<body class="bg-gray-100 min-h-screen flex items-center justify-center">
<div x-data="chatApp()" class="w-full max-w-lg bg-white shadow-lg rounded p-6">
<!-- Join Chat Screen -->
<div x-show="!joined" class="space-y-4">
<h1 class="text-2xl font-bold text-center">Join Chat</h1>
<!-- Name Input -->
<div>
<input type="text"
x-model="username"
:class="{'border-red-500': nameError}"
class="w-full border rounded px-3 py-2"
placeholder="Enter your name">
<p x-show="nameError" class="text-red-600 text-sm mt-1">
Name is required.
</p>
</div>
<button @click="joinChat"
class="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700">
Join Chat
</button>
</div>
<!-- Chat Screen -->
<div x-show="joined" x-transition class="flex flex-col h-[500px]">
<h2 class="text-xl font-bold mb-3">Chat Room</h2>
<!-- Message Area -->
<div class="flex-1 border rounded p-3 overflow-y-auto space-y-2" id="chat-box">
<template x-for="msg in messages">
<div class="p-2 rounded bg-gray-200" >
<span x-text="msg.username"></span>:
<span x-text="msg.message"></span>
</div>
</template>
</div>
<!-- Input -->
<div class="mt-4 flex gap-2">
<input type="text" x-model="input"
@keydown.enter="sendMessage"
class="flex-1 border rounded px-3 py-2"
placeholder="Type a message...">
<button @click="sendMessage"
class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
Send
</button>
</div>
</div>
</div>
<script type="module">
import Echo from 'https://cdnjs.cloudflare.com/ajax/libs/laravel-echo/2.2.4/echo.js'
window.Echo = new Echo({
broadcaster: 'pusher',
key: 'app-key',
wsHost: "127.0.0.1",
wsPort: 6001,
wssPort: 6001,
forceTLS: false,
encrypted: true,
disableStats: true,
enabledTransports: ['ws', 'wss'],
cluster: "mt1"
});
</script>
<script>
function chatApp() {
return {
joined: false,
input: "",
messages: [],
username: "",
nameError: false,
joinChat() {
if (!this.username.trim()) {
this.nameError = true;
return;
}
// Add your join click handler here
this.nameError = false;
this.joined = true;
window.Echo.private('conversation.1')
.listen('Chat.MessageRecived', (e) => {
this.messages.push(e);
})
},
async sendMessage() {
if (!this.input.trim()) return;
const userMessage = this.input;
this.messages.push({username: "You", message: userMessage});
this.input = "";
try {
const res = await axios.post("/chat", { username:this.username,message: userMessage,conversation_channel: "private-conversation.1"});
} catch (err) {
}
}
}
}
</script>
</body>
</html>
Now run your application server and soketi server using soketi start . Visit you application server address . you will see following output.

메타데이터
- post_id
- 185acb2ca5f3
- slug
- building-a-real-time-chat-app-in-express-js-using-soketi-pusher-compatible-websockets-185acb2ca5f3
- url
- https://medium.com/@anuj_jaryal/building-a-real-time-chat-app-in-express-js-using-soketi-pusher-compatible-websockets-185acb2ca5f3
- canonical_url
- https://medium.com/@anuj_jaryal/building-a-real-time-chat-app-in-express-js-using-soketi-pusher-compatible-websockets-185acb2ca5f3
- author_url
- https://medium.com/@anuj_jaryal
- status
- ok
- fetched_at
- 2026-07-14 15:40:45