Real-time Chat Application Using Strapi, Next, Socket.io, and PostgreSQL

Goal

Prerequisites

Setting up PostgreSQL

  • On the left navigation bar, click on Servers and click on PostgreSQL 14.
  • Right-click on Databases, hover over Create and click on Database.
  • You can name the database anything you desire, but, in this tutorial, the name of the database is chatapp. Once you’re done naming the database, hit save.

Connecting PostgreSQL with Strapi

Custom Setup

mkdir strapified-chat
cd strapified-chat
code .
npm init --y
npx create-strapi-app chatapp

Accessing Strapi Admin

cd chatapp
npm run build
npm run develop

Quickstart

  • Create a folder for this project. You can decide to name it anything you want but in this tutorial, its name is strapified-chat.
  • Open this folder in your terminal and run the create-strapi-app command below. This will create a Strapi app named chatapp:
npx create-strapi-app chatapp --quickstart

Configuring Strapi

  • Once the installation is complete, open the folder named strapified-chat in a code editor.
  • Click on chatapp then select database.js from the config folder.
  • Replace the following code into the database.js file to configure the PostgreSQL database.
module.exports = ({ env }) => ({
connection: {
client: "postgres",
connection: {
host: env("DATABASE_HOST", "127.0.0.1"),
port: env.int("DATABASE_PORT", 5432),
database: env("DATABASE_NAME", "chatapp"),//Name of database
user: env("DATABASE_USERNAME", "postgres"),//Default username
password: env("DATABASE_PASSWORD", "12345678"),//Password to your PostgreSQL database
ssl: env.bool("DATABASE_SSL", false),
},
},
});

Setting up NextJS

  • Run the create-next app command below to move out from the Strapi folder and spin up the next application in a new folder.
cd ..
npx create-next-app next-chat
  • Follow the prompt to install create-next-app.

Email Authentication using Nodemailer

cd next-chat
npm i nodemailer

Configuring Nodemailer

  • Open the mail.js file and add the following code to configure nodemailer.
export default function (req, res) {
const nodemailer = require("nodemailer");
const transporter = nodemailer.createTransport({
port: 465,
host: "smtp.gmail.com",
secure: "true",
auth: {
user: "examaple@gmail.com",//Replace with your email address
pass: "example",// Replace with the password to your email.
},
});
const mailData = {
from: "Chat API",
to: req.body.email,
subject: `Verify your email`,
text: req.body.message,
};
transporter.sendMail(mailData, function (err, info) {
if (err)
return res.status(500).json({ message: `an error occurred ${err}` });
res.status(200).json({ message: info }); de
});
}

Setting up the Login Form

  • Open up the index.js file in the pages folder and replace the code in it with the following code.
import styles from "../styles/Home.module.css";
export default function Home() {
return (
<div className={styles.container}>
<form className={styles.main}>
<h1>Login</h1>
<label htmlFor="name">Email: </label>
<input type="email" id="name" />
<br />
<input type="submit" />
</form>
</div>
);
}

Getting the Email from the User

  1. Import the useState dependency from react in the index.js file. import { useState } from “react”;
  2. Add an onChange event that will listen for any change in the input and store it in a state variable.
export default function Home() {
const [email, setEmail] = useState("");
const [user, setUser] = useState("");
return (
<div className={styles.container}>
<form className={styles.main}>
<h1>Login</h1>
<label htmlFor="user">Username: </label>
<input
type="text"
id="user"
value={user}
onChange={(e) => setUser(e.target.value)} // Getting the inputs
/>
<br />
<label htmlFor="name">Email: </label>
<input
type="email"
id="name"
onChange={(e) => setEmail(e.target.value)} // Getting the inputs
/>
<br />
<input type="submit" />
</form>
</div>
);
}
  1. Now, create a function that will get the email when the user clicks submit and sends a POST to api/mail.
export default function Home() {
const [email, setEmail] = useState("");
const [user, setUser] = useState("");
const handlesubmit = (e) => {
e.preventDefault();
let message = "Testing, Testing..... It works🙂";
let data = {
email, // User's email
message,
};
fetch("/api/mail", {
method: "POST", // POST request to /api//mail
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}).then(async (res) => {
if (res.status === 200) {
console.log(await res.json());
} else {
console.log(await res.json());
}
});
setEmail("");
setUser("");
};
return (
<div className={styles.container}>
<form className={styles.main}>
<h1>Login</h1>
<label htmlFor="user">Username: </label>
<input
type="text"
id="user"
value={user}
onChange={(e) => setUser(e.target.value)} // Getting the inputs
/>{" "}
<br />
<label htmlFor="name">Email: </label>
<input
type="email"
id="name"
value={email}
onChange={(e) => setEmail(e.target.value)} // Getting the inputs
/>
<br />
<input type="submit" onClick={handlesubmit} /> // Handling Submit
</form>
</div>
);
}

Token Authentication using JWT

Adding JWT to the Application

  1. Open up your terminal in the next-chat folder and install the JSON Web Token dependency.
npm i jsonwebtoken
const handlesubmit = (e) => {
e.preventDefault();
const id = Math.trunc(Math.random() * 1000000); //Generating a random number and taking the first 10 numbers.
let account = { // Creating the payload
id
};
const SECRET = "this is a secret"; //JWT secret
const token = jwt.sign(account, SECRET);// Creates the Token
console.log(token);
};

Setting up Strapi

  1. Ensure that your Strapi server is up and running.
cd chatapp
npm run develop

Permissions in Strapi

  1. To allow requests, click on Settings on the side-nav bar and click on Roles under USERS & PERMISSIONS PLUGIN.

Storing the User’s Credentials

  1. Make sure your NextJS application is running.
cd next-chat
npm run dev
const handlesubmit = (e) => {
e.preventDefault();
let message = "Testing, Testing..... It works🙂";
const id = Math.trunc(Math.random() * 1000000); //Generating a random number and taking the first 10 numbers.
let data = {
email, // User's email
message,
};
let account = {
id
};
const SECRET = "this is a secret";
const token = jwt.sign(account, SECRET);
let strapiData = { // Parameters for Strapi
data: {
id,
username: user,
email,
token,
},
};
fetch("http://localhost:1337/api/accounts", {
method: "POST",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify(strapiData), // Adding the parameters to the body.
}).then(async (res) => {
console.log(await res.json()); // Outputs the result
});

// The Rest of the code
};

Authenticating the User

  1. Open up your index.js file in the pages folder and add the following code to send the link to the user’s email in the handlesubmit function.
const handlesubmit = (e) => {
e.preventDefault();
const id = Math.trunc(Math.random() * 1000000); //Generating a random number and taking the first 10 numbers.
console.log(id);
let account = { // JWT payload
id,
};
const SECRET = "this is a secret";
const token = jwt.sign(account, SECRET);
let message = `http://localhost:3000/chat/${token}`; // Adding the token to the message sent to the user.
let data = {
email, // User's email
message,
};
fetch("/api/mail", {
method: "POST", // POST request to /api//mail
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
}).then(async (res) => {
if (res.status === 200) {
console.log(await res.json());
} else {
console.log(await res.json());
}
});
setEmail("");
setUser("");
};

Verifying the Token

  1. Create a folder in the pages folder called chat and create a file in the chat folder named token.js
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import jwt from "jsonwebtoken";
export default function Chat() {
const router = useRouter();
const SECRET = "this is a secret"; // JWT Secret
const [done, setDone] = useState("");
const token = router.query.token; // Getting the token from the URL
useEffect(() => {
if (!router.isReady) return console.log("Loading... Please wait"); // Checking if the token has been fetched from the URL.
try {
const data = jwt.verify(token, SECRET); // Verifying the token using the secret
console.log(data); // Logging out the Payload.
setDone("done"); // Granting access to the chat page
} catch (error) {
router.push("/"); // Redirecting the user to the home page if an error occured
}
}, [token]); // Listens for a change in token
return (
<div>
{done !== "done" ? ( // Waiting for access to be granted
<h1>Verifying token..... Please wait</h1>
) : (
<h1> Group Chat</h1>
)}
</div>
);
}
  • Add the following code to the token.js file to get the id stored in the token.
export default function Chat() {
// Rest of the code
useEffect(() => {
//Rest of the code
try {
const payload = jwt.verify(token, SECRET); // Verifying the token using the secret
async function fetchData() {
const data = await fetch(
`http://localhost:1337/api/accounts/${payload.id}`
);
const account = await data.json(); // Getting the user's data
console.log(account);
}
fetchData();
setDone("done"); // granting access to the chat page
} catch (error) {
router.push("/"); // redirecting the user to the home page if an error occured
}
}, [token]); // Listens for a change in token
// Rest of the code
}
  • After getting the user’s data from strapi, add the following code to compare the tokens.
// Rest of code

useEffect(() => {
// Rest of the code.

async function fetchData() {
const data = await fetch(
`http://localhost:1337/api/accounts/${payload.id}`
);
const account = await data.json();
if (token !== account.data.attributes.token) return router.push("/"); // Verifying if the user exist in Strapi
return;
}
fetchData();
// Rest of the code
}, [token])

// Rest of the code

Sending Messages

  1. Run npm install to install all the dependencies for this chat application. Once the installation is complete, run npm run dev to start up the chat application.

Setting up Socket.io

Backend

  1. Create a new collection named message in Content-Type Builder and add 2 fields, a user field and a message field.
"use strict";
module.exports = {
/**
* An asynchronous register function that runs before
* your application is initialized.
*
* This gives you an opportunity to extend code.
*/
register({ strapi }) {},
/**
* An asynchronous bootstrap function that runs before
* your application gets started.
*
* This gives you an opportunity to set up your data model,
* run jobs, or perform some special logic.
*/
bootstrap(/* { strapi } */) {
//strapi.server.httpServer is the new update for Strapi V4
var io = require("socket.io")(strapi.server.httpServer, {
cors: { // cors setup
origin: "http://localhost:3000",
methods: ["GET", "POST"],
allowedHeaders: ["my-custom-header"],
credentials: true,
},
});
io.on("connection", function (socket) { //Listening for a connection from the frontend
socket.on("join", ({ username }) => { // Listening for a join connection
console.log("user connected");
console.log("username is ", username);
if (username) {
socket.join("group"); // Adding the user to the group
socket.emit("welcome", { // Sending a welcome message to the User
user: "bot",
text: `${username}, Welcome to the group chat`,
userData: username,
});
} else {
console.log("An error occurred");
}
});
socket.on("sendMessage", async (data) => { // Listening for a sendMessage connection
let strapiData = { // Generating the message data to be stored in Strapi
data: {
user: data.user,
message: data.message,
},
};
var axios = require("axios");
await axios
.post("http://localhost:1337/api/messages", strapiData)//Storing the messages in Strapi
.then((e) => {
socket.broadcast.to("group").emit("message", {//Sending the message to the group
user: data.username,
text: data.message,
});
})
.catch((e) => console.log("error", e.message));
});
});
},
};

Frontend

  1. Add the following code to the index.js file in next-chat/components to set up socket.io and send messages.
import React, { useEffect, useState } from "react";
import { Input } from "antd";
import "antd/dist/antd.css";
import "font-awesome/css/font-awesome.min.css";
import Header from "./Header";
import Messages from "./Messages";
import List from "./List";
import socket from "socket.io-client";
import {
ChatContainer,
StyledContainer,
ChatBox,
StyledButton,
SendIcon,
} from "../pages/chat/styles";
function ChatRoom({ username, id }) {
const [messages, setMessages] = useState([]);
const [message, setMessage] = useState("");
const [users, setUsers] = useState([]);
const io = socket("http://localhost:1337");//Connecting to Socket.io backend
let welcome;
useEffect(() => {
io.emit("join", { username }, (error) => { //Sending the username to the backend as the user connects.
if (error) return alert(error);
});
io.on("welcome", async (data, error) => {//Getting the welcome message from the backend
let welcomeMessage = {
user: data.user,
message: data.text,
};
welcome = welcomeMessage;
setMessages([welcomeMessage]);//Storing the Welcome Message
await fetch("http://localhost:1337/api/messages")//Fetching all messages from Strapi
.then(async (res) => {
const response = await res.json();
let arr = [welcome];
response.data.map((one, i) => {
arr = [...arr, one.attributes];
setMessages((msgs) => arr);// Storing all Messages in a state variable
});
})
.catch((e) => console.log(e.message));
});
io.on("message", async (data, error) => {//Listening for a message connection
await fetch("http://localhost:1337/api/messages")
.then(async (res) => {
const response = await res.json();
let arr = [welcome];
response.data.map((one, i) => {
arr = [...arr, one.attributes];
setMessages((msgs) => arr);
});
})
.catch((e) => console.log(e.message));
});
}, [username]);
const sendMessage = (message) => {
if (message) {
io.emit("sendMessage", { message, user: username }, (error) => {// Sending the message to the backend
if (error) {
alert(error);
}
});
setMessage("");
} else {
alert("Message can't be empty");
}
};
const handleChange = (e) => {
setMessage(e.target.value);
};
const handleClick = () => {
sendMessage(message);
};
return (
<ChatContainer>
<Header room="Group Chat" />
<StyledContainer>
<List users={users} id={id} usersname={username}>
<ChatBox>
<Messages messages={messages} username={username} />
<Input
type="text"
placeholder="Type your message"
value={message}
onChange={handleChange}
/>
<StyledButton onClick={handleClick}>
<SendIcon>
<i className="fa fa-paper-plane" />
</SendIcon>
</StyledButton>
</ChatBox>
</StyledContainer>
</ChatContainer>
);
}
export default ChatRoom;
import React, { useEffect, useRef } from "react";
import Message from "./Message/";
import styled from "styled-components";
function Messages(props) {
const { messages, username: user } = props;
const messagesEndRef = useRef(null);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });//Scroll to bottom functionality.
};
useEffect(() => {
scrollToBottom();
}, [messages]);
return (
<StyledMessages>
{messages.map((message, i) => (
<div key={i} ref={messagesEndRef}>
<Message message={message} username={user} />
</div>
))}
</StyledMessages>
);
}
export default Messages;
const StyledMessages = styled.div`
padding: 5% 0;
overflow: auto;
flex: auto;
`;

Role-Based Auth

  1. Create an active user collection type in Strapi, add a users field and socketid field, make sure it is set to a unique field and configure the roles permission.
// The rest of Code
socket.on("join", ({ username }) => {
console.log("user connected");
console.log("username is ", username);
if (username) {
socket.join("group");
socket.emit("welcome", {
user: "bot",
text: `${username}, Welcome to the group chat`,
userData: username,
});
let strapiData = {
data: {
users: username,
},
}
await axios
.post("http://localhost:1337/api/active-users", strapiData)//Storing the active users
.then(async (e) => {
socket.emit("roomData", { done: "true" });
})
.catch((e) => {
if (e.message == "Request failed with status code 400") {//Checking if user exists
socket.emit("roomData", { done: "existing" });
}
})

} else {
console.log("e no work");
}
});
//The rest of the code
//The rest of the code
useEffect(() => {
//The rest of the code

io.on("roomData", async (data) => {
await fetch("http://localhost:1337/api/active-users").then(async (e) => {
setUsers(await e.json());//Fetching and storing the users in the users state variable
});
//The rest of the code
}, [username]);

//The rest of the code
import React from "react";
import styled from "styled-components";
import { List as AntdList, Avatar } from "antd";
function List(props) {
const users = props.users.data;//Getting the users
return (
<StyledList>
<ListHeading>Active Users</ListHeading>
<AntdList
itemLayout="horizontal"
dataSource={users}
renderItem={(user) => (
<AntdList.Item>
<AntdList.Item.Meta
avatar={
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
}
title={user.attributes.users}//Displaying the users
/>
<button
style={
user.attributes.users === "Admin" || props.username !== "Admin"
? { display: "none" }
: null
}//Verifying that the user is an Admin
>
Delete
</button>
</AntdList.Item>
)}
/>
</StyledList>
);
}
export default List;
const StyledList = styled(AntdList)`
margin-right: 10px;
flex: 0 0 35%;
padding: 20px;
.ant-list-item-meta-content {
flex-grow: 0;
}
h4 {
font-size: 25px;
}
a {
color: #097ef0;
}
`;
const ListHeading = styled.div`
color: #757591;
font-size: 20px;
font-style: oblique;
border-bottom: 1px solid #757591;
`;

Adding Delete functionality.

  • Every user will always have to reload the page to view active users.
  • When the Admin deletes a user, the page has to be refreshed to view changes.
  • The deleted user will be redirected to the home page
  1. Open up the index.js file in chatapp/src and add the code below to set up the backend.
//The rest of the code
bootstrap(/* { strapi } */) {
//The rest of the code
io.on("connection", function (socket) {
// The rest of the code
socket.on("kick", (data) => {//Listening for a kick event
io.sockets.sockets.forEach((socket) => {
if (socket.id === data.socketid) {
socket.disconnect();//Disconnecting the User
socket.removeAllListeners();
return console.log("kicked", socket.id, data.socketid);
} else {
console.log("Couldn't kick", socket.id, data.socketid);
}
});
});
});
},
};
  1. After setting up the backend, open up your index.js file in next-chat/components/List and add the following code. The code below will get the socket id, emit it to the server and delete the user from the database.
import React from "react";
import styled from "styled-components";
import { List as AntdList, Avatar } from "antd";
import socket from "socket.io-client";
function List(props) {
const users = props.users.data;
const handleClick = async (id, socketid) => {
const io = socket("http://localhost:1337");
await fetch("http://localhost:1337/api/active-users/" + id, {//Gets the id and delete the user from the active users collection.
method: "Delete",
headers: {
"Content-type": "application/json",
},
})
.then(async (e) => {
io.emit("kick", { socketid }, (error) => {//When the
if (error) return alert(error);
});
setTimeout(() => location.reload(), 3000);//Refreshes the page
})
.catch((e) => location.reload());//Refreshes the page if an error occured
};
return (
<StyledList>
<ListHeading>Active Users</ListHeading>
<AntdList
itemLayout="horizontal"
dataSource={users}
renderItem={(user) => (
<AntdList.Item>
<AntdList.Item.Meta
avatar={
<Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" />
}
title={user.attributes.users}
/>
<button
style={
user.attributes.users === "Admin" || props.username !== "Admin"
? { display: "none" }
: null
}
onClick={() => handleClick(user.id, user.attributes.socketid)}//Passing the socketid as parameter to the handleClick function
>
Delete
</button>
</AntdList.Item>
)}
/>
</StyledList>
);
}
export default List;
//The rest of the code

Conclusion

--

--

The open source Headless CMS Front-End Developers love.

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store