In this article, I'll walk you through how I built a customer support application with live chatting, in-app, and email notification features using Next.js, Novu, Appwrite, and EmailJS.
Appwrite Cloud handles the authentication, database, and file storage aspects, Novu handles in-app notifications, and EmailJS for email messaging.
Upon completion, you'll be able to build advanced applications that require a chat feature with Appwrite and add email and in-app notifications to your apps via EmailJS and Novu.
💡 PS: This tutorial assumes you have a basic knowledge of React or Next.js
Application Workflow
Before we start coding, let me summarise how the application works. The application does the following:
- allows customers to create support tickets,
- authenticates staff and ensures that no one other than the staff can log in to the application,
- notifies the staff when a customer creates a support ticket,
- allows both the staff and customers to settle disputes via the live-chatting feature powered by Appwrite Cloud, and
- sends a confirmation email to the customers when they open a ticket and also when a staff sends them a message regarding the open ticket.
Brief Demo
The UI Design Process
Here, I'll walk you through creating the required pages for the web application.
First, you need to create a home page where customers can create support tickets, and staff can also navigate to the login page via the page.
Next, create the staff login page that authenticates visitors to ensure they are staff before allowing them access to the dashboard.
You also need a dashboard page that shows all the tickets based on their status. Each support ticket must be clickable and redirects the staff to its details page.
Next, create a page that displays all the data related to a particular ticket. Staff can update the status of a support ticket and download its attachment; if available.
Then, create a chat page where a customer support staff and a customer can communicate in real time about an issue.
The page should not require authentication but an access code sent to the customer's email. The chat URL will be similar to this https://firm-support.vercel.app/chat/<ticket_ID>
.
Finally, you need an admin page that enables you to add or remove staff from the application. With this, you can have more than one user manage the support tickets or respond to customers' queries.
Adding Appwrite to a Next.js application
Appwrite is a powerful open-source backend platform that enables you to create secured and scalable (web and mobile) applications. With Appwrite, you don't need to worry about the backend resources of your application because with Appwrite, you - "Build Fast. Scale Big. All in One Place."
Instead of setting up your backend server on your computer, Appwrite Cloud enables you to focus on developing your applications; while, it manages crucial backend functionalities such as user authentication, database management, file storage, and more.
Installation and configuration
To add Appwrite to a Next.js app, follow the steps below:
First of all, create a Next.js project by running the code below.
npx create-next-app customer-support-app
Visit Appwrite's website and create a new account.
Create a new organisation and project. Each project contains all the resources you need to build a fully functional application.
Next, you need to choose where and how you want to use Appwrite, either as a web or mobile SDK or you need to integrate it with your (existing) server.
Since we are building a Next.js app with Appwrite Cloud, select Web App from the SDK Platform menu and register a new app under the project.
From the image below, I provided a name for the application and used an asterisk as the hostname. After deploying the application on Vercel, you can change the hostname to the URL provided by Vercel.
Install the Appwrite Node.js SDK into your Next.js project as done below.
npm install appwrite
Create a .env.local
and appwrite.js
file at the root of your project.
touch appwrite.js .env.local
Copy the code below into the appwrite.js
file.
import { Client, Account, Databases, Storage } from "appwrite";
const client = new Client();
client
.setEndpoint("https://cloud.appwrite.io/v1")
.setProject(process.env.NEXT_PUBLIC_PROJECT_ID);
export const account = new Account(client);
export const db = new Databases(client);
export const storage = new Storage(client);
The code snippet above enables us to access and interact with the authentication, database, and file storage features provided by Appwrite.
Copy the code below into the .env.local
file.
NEXT_PUBLIC_PROJECT_ID=<your_project_id>
NEXT_PUBLIC_DB_ID=<your_database_id>
NEXT_PUBLIC_TICKETS_COLLECTION_ID=<your_collection_id>
NEXT_PUBLIC_USERS_COLLECTION_ID=<your_collection_id>
NEXT_PUBLIC_BUCKET_ID=<your_bucket_id>
The code snippet above contains environment variables containing all the private keys needed for interacting with Appwrite Cloud.
On your project dashboard, click the Project ID
button to copy your project's id and paste it into the .env.local
variable.
Setting up Authentication with Appwrite Cloud
Since you are using the Email and Password authentication method, you don't need to add any configurations on Appwrite Cloud before using the service because it has been configured by default.
However, let's add an extra layer of security to the project by updating the default Session length.
Select Auth
from the sidebar menu and switch to the Security
tab.
Scroll down to the Session Length section and change it from 365 days
to 1 hour
. Users will need to be re-authenticated after an hour of using the application, and, in case a user doesn't log out of our application, they are logged out automatically after an hour.
Click Update
to add the new setting, and you're ready to go.🚀
Setting up the Appwrite Database
Here, you'll learn how to set up the database on Appwrite Cloud.
Select Database
from the sidebar menu to create a new database.
Next, you need to create two database collections, one for the support tickets and the other for the staff within the application.
You may be wondering why we need another collection for the users. What about the users saved on the Appwrite Auth?
The reason is that you can not get all the users or delete a user using Appwrite Cloud. It is only possible when you use the Node.js SDK.
However, since we need to view all the users (staff) and add or delete whenever necessary from the Admin page of the application; therefore, you need to create a users
database containing similar information as the Auth section.
Create two database collections for the support tickets and users, and copy their IDs into the env.local
file.
Select the users
collection and create three required attributes, as shown below.
Finally, click on the Settings menu under the users
and update the permission to allow only users to create, read, and delete users from the collection.
Setting up Appwrite Storage
Select Storage
from the sidebar menu and create a new bucket for images attached to each support ticket. Users will be able to upload screenshots of the issues they are facing when creating a support ticket.
Copy the bucket ID and paste it into the .env.local
file.
Communicating with Appwrite: Authenticating users
Unlike conventional applications, this application does not have a sign-up page because it is exclusive to staff. Therefore, you can create the initial account via your dashboard on Appwrite Cloud.
In this section, you'll learn how to set up the authentication process for the application.
You can create a utils
folder containing the functions and import them into the required components.
mkdir utils
cd utils
touch functions.js
Add the following imports into the file to enable us to interact with the backend features. We'll make use of them in the upcoming sections.
import { account, db, storage } from "./appwrite";
import { ID } from "appwrite";
Logging into the application
Remember, we have Appwrite Auth and the users
collection on the database storing users' details. Therefore to log users into the application, you need to authenticate the user using Appwrite Auth, then filter the users
collection to verify if the user's details exist before granting access to the application.
//👇🏻 filters the users' list
const checkUserFromList = async (email, router) => {
try {
const response = await db.listDocuments(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_USERS_COLLECTION_ID
);
const users = response.documents;
const result = users.filter((user) => user.email === email);
//👉🏻 USER OBJECT ==> console.log(result[0])
if (result.length > 0) {
successMessage("Welcome back 🎉");
router.push("/staff/dashboard");
} else {
errorMessage("Unauthorized...Contact Management.");
}
} catch (error) {
errorMessage("An error occurred 😪");
console.error(error);
}
};
//👇🏻 authenticates the user
export const logIn = async (email, password, router) => {
try {
//👇🏻 Appwrite login method
await account.createEmailSession(email, password);
//👇🏻 calls the filter function
await checkUserFromList(email, router);
} catch (error) {
console.log(error);
errorMessage("Invalid credentials ❌");
}
};
The code snippet above accepts the user's email and password from the form field, authenticates the user using Appwrite Auth, then checks if the user is on the staff list before granting permission to the application.
Logging out
Logging users out still work the conventional way. Appwrite also provides an account.deleteSession()
method that enables users to log out of an ongoing session.
export const logOut = async (router) => {
try {
await account.deleteSession("current");
router.push("/");
successMessage("See ya later 🎉");
} catch (error) {
console.log(error);
errorMessage("Encountered an error 😪");
}
};
Protecting pages from unauthenticated users
To do this, you can store the user's information object to a state after logging in or use Appwrite's account.get()
method.
Using the account.get()
method:
export const checkAuthStatus = async (setUser, setLoading, router) => {
try {
const response = await account.get();
setUser(response);
setLoading(false);
} catch (err) {
router.push("/");
console.error(err);
}
};
The code snippet above gets all the information related to the currently signed-in users. It checks if the user is active and returns the object containing all the user's details.
You can execute the function on page load for routes containing protected data, such as the Dashboard, Ticket Details, and Admin routes.
Admin page: adding and removing staff
In this section, I'll guide you through how to add, get, and delete staff from the users' collection. We are technically building the backend functionality of the Admin page.
Adding new staff
The code snippet below accepts the user's name, email, and password when you submit the form, create an account on Appwrite Auth, and save the email and the name in the users
database.
//👇🏻 generates random string as ID
const generateID = () => Math.random().toString(36).substring(2, 24);
export const addUser = async (name, email, password) => {
try {
//👇🏻 create a new acct on Appwrite Auth
await account.create(generateID(), email, password, name);
//👇🏻 adds the user's details to the users database
await db.createDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_USERS_COLLECTION_ID,
ID.unique(),
{ user_id: generateID(), name, email }
);
successMessage("User added successfully 🎉");
} catch (error) {
console.log(error);
}
};
Getting the staff list
The code snippet accesses the document on Appwrite Cloud and returns all the data in the users
collection.
export const getUsers = async (setUsers) => {
try {
const response = await db.listDocuments(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_USERS_COLLECTION_ID
);
setUsers(response.documents);
} catch (error) {
console.log(error);
}
};
Removing staff
To do this, you need to pass the ID of the selected staff into the function when you click the Remove button.
export const deleteUser = async (id) => {
try {
await db.deleteDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_USERS_COLLECTION_ID,
id
);
successMessage("User removed 🎉"); // Success
} catch (error) {
console.log(error); // Failure
errorMessage("Encountered an error 😪");
}
};
💡 The user's credentials are still available in Appwrite Auth, but since we verify users' credentials against the
users
collection when they log in, this method is still valid.
Support ticket: data structure, database and storage
In this section, you'll learn how to create the backend (database) for the customer support application.
Creating a support ticket
When a customer creates a support ticket, you have to retrieve all the details from the form and save them to the database. Therefore, create a function that accepts all the ticket's details and ensures it works perfectly whether or not the customer uploads a screenshot of the problem they are facing.
export const sendTicket = async (name, email, subject, message, attachment) => {
if (attachment !== null) {
//👇🏻 Customer attached an image
console.log({ name, email, subject, message, attachment });
} else {
//👇🏻 No attachment
console.log({ name, email, subject, message });
}
};
To add the ticket's information to the tickets
collection on Appwrite Cloud, add the following attributes to the collection.
Name | Required? | Type |
---|---|---|
name | yes | string |
yes | string | |
subject | yes | string |
status | yes | string |
content | yes | string |
access_code | yes | string |
attachment_url | no | string |
messages | string[] |
Switch to the Settings tab and update the permission on the tickets
collection as shown below.
Finally, update the sendTicket
function to add the event details to Appwrite Cloud.
export const sendTicket = async (name, email, subject, message, attachment) => {
const createTicket = async (file_url = "https://google.com") => {
try {
const response = await db.createDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
ID.unique(),
{
name,
email,
subject,
content: message,
status: "open",
messages: [
JSON.stringify({
id: generateID(),
content: message,
admin: false,
name: "Customer",
}),
],
attachment_url: file_url,
access_code: generateID(),
}
);
//👇🏻 send notification to the customer
console.log("RESPONSE >>>", response);
successMessage("Ticket created 🎉");
} catch (error) {
errorMessage("Encountered saving ticket ❌");
}
};
if (attachment !== null) {
try {
const response = await storage.createFile(
process.env.NEXT_PUBLIC_BUCKET_ID,
ID.unique(),
attachment
);
const file_url = `https://cloud.appwrite.io/v1/storage/buckets/${process.env.NEXT_PUBLIC_BUCKET_ID}/files/${response.$id}/view?project=${process.env.NEXT_PUBLIC_PROJECT_ID}&mode=admin`;
//👇🏻 creates ticket with its image
createTicket(file_url);
} catch (error) {
errorMessage("Error uploading the image ❌");
}
} else {
//👇🏻 creates ticket even without an image (screenshot)
await createTicket();
}
};
- From the code snippet above:
- The nested function,
createTicket
accepts all the ticket's attributes and creates a new document on the Appwrite Cloud. Since uploading an attachment is optional, theflier_url
attribute has a default URL value. - The
messages
array attribute creates a new structure for the live chatting feature. It converts the content and the customer information into a JSON string and adds it to themessages
array. - The if and else code block checks if the customer uploaded an image. If true, the code uploads the image to the Cloud Storage, retrieves its URL, and passes it into the
createTicket
function. - Otherwise, the
createTicket
function uses the default value (https://google.com
) as the attachment URL.
- The nested function,
Getting the support tickets from Appwrite Cloud
On the Dashboard page, you need to retrieve all the support tickets. The function below retrieves and group them based on their status.
export const getTickets = async (
setOpenTickets,
setInProgressTickets,
setCompletedTickets
) => {
try {
const response = await db.listDocuments(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID
);
const tickets = response.documents;
const openTickets = tickets.filter((ticket) => ticket.status === "open");
const inProgressTickets = tickets.filter(
(ticket) => ticket.status === "in-progress"
);
const completedTickets = tickets.filter(
(ticket) => ticket.status === "completed"
);
setCompletedTickets(completedTickets);
setOpenTickets(openTickets);
setInProgressTickets(inProgressTickets);
} catch (error) {
console.log(error); // Failure
}
};
Getting the ticket details
When you click on each support ticket on the Dashboard, it needs to redirect you to another page containing all information related to the support ticket.
Therefore, you need to create a ticket/[id].js
file that retrieves the ticket's details via server-side rendering using the id
from the page route, as done below.
export async function getServerSideProps(context) {
let ticketObject = {};
try {
const response = await db.getDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
context.query.id
);
ticketObject = response;
} catch (err) {
ticketObject = {};
}
return {
props: { ticketObject },
};
}
Updating the ticket's status
On the details page, you can update the status of a support ticket using the HTML select tag and accept three types of values - "open"
, "in-progress"
, and "completed"
. You can update the status using the function below:
export const updateTicketStatus = async (id, status) => {
try {
await db.updateDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
id,
{ status }
);
successMessage("Status updated, refresh page 🎉");
} catch (error) {
console.log(error); // Failure
errorMessage("Encountered an error ❌");
}
};
Live chatting feature with Next.js and Appwrite
In this section, I'll walk you through adding the chat feature to the application. To do this, create a chat page similar to the image below.
This page should require an access code on page load but doesn't require authentication to access the page. Run the code snippet below when a user sends a message.
export const sendMessage = async (text, docId) => {
//👇🏻 get the ticket ID
const doc = await db.getDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
docId
);
try {
//👇🏻 gets the user's object (admin)
const user = await account.get();
const result = await db.updateDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
docId,
{
messages: [
...doc.messages,
JSON.stringify({
id: generateID(),
content: text,
admin: true,
name: user.name,
}),
],
}
);
//👇🏻 message was added successfully
if (result.$id) {
successMessage("Message Sent! ✅");
//👉🏻 email the customer with access code and chat URL
} else {
errorMessage("Error! Try resending your message❌");
}
} catch (error) {
//👇🏻 means the user is a customer
const result = await db.updateDocument(
process.env.NEXT_PUBLIC_DB_ID,
process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID,
docId,
{
messages: [
...doc.messages,
JSON.stringify({
id: generateID(),
content: text,
admin: false,
name: "Customer",
}),
],
}
);
if (result.$id) {
successMessage("Message Sent! ✅");
//👉🏻 notify staff via notifications
} else {
errorMessage("Error! Try resending your message❌");
}
}
};
The code snippet above checks if the user is a staff or a customer before adding the message to the messages
array.
Real-time messaging
To display the chat messages immediately you send them.
Create an event listener on the tickets
collection and update the messages
array with the changes, as done below.
//👉🏻 "client" is from your appwrite.js file
useEffect(() => {
const unsubscribe = client.subscribe(
`databases.${process.env.NEXT_PUBLIC_DB_ID}.collections.${process.env.NEXT_PUBLIC_TICKETS_COLLECTION_ID}.documents`,
(data) => {
const messages = data.payload.messages;
setMessages(messages.map(parseJSON));
}
);
return () => {
unsubscribe();
};
}, []);
You can learn more about real-time listeners in Appwrite.
Auto-scroll feature
Add an empty div
below the chat messages element.
<div className='chat__container'>
//👇🏻 chat messages element
{/* {messages.map((message) => (
<div>{message}</div>
))} */}
<div ref={lastMessageRef} />
</div>
Create a reference to the div
element and shift the mouse focus when there is a new message, as done below.
const lastMessageRef = useRef(null);
useEffect(() => {
// 👇️ scroll to bottom every time messages change
lastMessageRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messages]);
Adding in-app and email notifications with Novu and EmailJS
Here, you'll learn how to add in-app notifications with Novu and send emails using EmailJS.
Recall that we need the in-app notification when a customer sends a message via the chat page and when they create a new ticket.
The email notification is needed when the customer support staff sends a message and when a customer creates a new ticket.
Setting up in-app notifications with Novu in Next.js
Novu is the first open-source notification infrastructure that manages all forms of communications. In this article, we'll make use of its in-app notification feature.
Install Novu Node.js SDK and its Notification center by running the code below.
npm install @novu/node @novu/notification-center
Run npx novu init
to create and access your dashboard.
Add your Novu Subscriber ID, App ID, and API Key into the .env.local
file.
NEXT_PUBLIC_NOVU_SUBSCRIBER_ID=<subscriber_ID>
NEXT_PUBLIC_NOVU_APP_ID=<your_app_ID>
NEXT_PUBLIC_NOVU_API_KEY=<your_api_key>
Create two notification template on your Novu dashboard and edit its content for the two cases - when a customer creates a ticket and sends a chat message. Add the notification functions to your Next.js app, as shown below.
const { Novu } = require("@novu/node");
const novu = new Novu(process.env.NEXT_PUBLIC_NOVU_API_KEY);
export default async function handler(req, res) {
const { status, title, username } = req.body;
const response = await novu
.trigger("<template_name>", {
to: {
subscriberId: process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID,
},
payload: {
status,
title,
username,
},
})
.catch((err) => console.error(err));
res.status(200).json(response.data);
}
Finally, create the notification bell to display the notifications within your application and add to the Nav component within your application.
import {
NovuProvider,
PopoverNotificationCenter,
NotificationBell,
} from "@novu/notification-center";
function Novu() {
function onNotificationClick(message) {
if (message?.cta?.data?.url) {
window.location.href = message.cta.data.url;
}
}
return (
<NovuProvider
subscriberId={process.env.NEXT_PUBLIC_NOVU_SUBSCRIBER_ID}
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID}
>
<PopoverNotificationCenter
onNotificationClick={onNotificationClick}
colorScheme='light'
>
{({ unseenCount }) => <NotificationBell unseenCount={unseenCount} />}
</PopoverNotificationCenter>
</NovuProvider>
);
}
export default Novu;
Sending email notifications with EmailJS
Here, you need to send an email to the customer when they create a support ticket and when a staff sends them a message. To do this follow the steps below.
Install EmailJS by running the code below.
npm install @emailjs/browser
Configure your EmailJS account and copy your credentials into the .env.local
file.
NEXT_PUBLIC_EMAIL_SERVICE_ID=<your_service_id>
NEXT_PUBLIC_EMAIL_API_KEY=<your_api_key>
NEXT_PUBLIC_TICKET_CREATION_ID=<template_id>
NEXT_PUBLIC_NEW_MESSAGE_ID=<template_id>
Create the templates for both cases and send your customers the email notifications.
The Wrap Up
The source code for the application is available here. Feel free to check it out
Open to work🙂
Did you enjoy this article or need an experienced React Technical Writer & Developer for a remote, full-time, or contract-based role? Feel free to contact me.
GitHub || LinkedIn || Twitter
Top comments (11)
Nice ❤️ It
😍😍
Looks amazing, thanks for the detailed guide!
You're welcome! 😎
This is really good. Awesome guide!
Thank you. 😊
David ❤️❤️❤️
❤️❤️❤️
thinks ,very detailed! 😁
You're welcome! 😎
Hi, Nice Tutorial , would like to test it, can you please share Admin User Name , Password for Online Demo