Create your course for free with zero transaction fee →
Back to tutorials

Build a real-time chat app with Next.js and Nhost

May 10, 2022 · 25 min read

Build a real-time chat app with Next.js and Nhost

Overview

In this tutorial, we are going to learn how to use Nhost, Hasura and Next.js to build a real-time chat application. In a couple of steps, you will set up a powerful, scalable real-time GraphQL backend with Nhost and connect it to your Next.js application. You will also learn how to easily set up authentication with Nhost and authorization with Hasura.

The final version of the project's code can be found on Github.

You can also preview the example live here.

This tutorial will cover:

  • Setting up and configuring Nhost as a serverless backend to provide a PostgreSQL database and a GraphQL API instantly to our Next.js app
  • Using the Nhost JavaScript SDK for authenticating users with OAuth providers and protecting our app
  • Setting up authorization with Hasura so that users can only run operations on data that they should be allowed to
  • Using GraphQL and the Apollo client to query and mutate our data and get real-time updates using subscriptions

Clone the project

To quickstart this tutorial, I've created a Github repository with two branches.

Github repository for the project

The main branch contains the complete source code of the real-time chat app that you can use as a reference. And the start branch is the starting point you'll use to get started quickly with this tutorial. It contains the Next.js project already pre-configured with TypeScript, Tailwind CSS and all the necessary React components that we'll use throughout the tutorial.

So, let's get going and start by cloning the start branch:

git clone -b start https://github.com/AlterClassIO/nextjs-nhost-chat.git

Then navigate into the cloned directory, install the dependencies and start the Next.js development server:

cd nextjs-nhost-chat yarn install yarn dev

You can now check if everything is working properly by opening http://localhost:3000 from your browser.

Real-time chat app starting point
Tasks

Set up your Nhost backend

Since we already have the base UI for our application, we can get started with setting up our Nhost backend.

If you don't know Nhost already, it is a serverless backend for web and mobile applications.

Nhost backend services architecture
Nhost backend services architecture

It provides a suite of backend services that you can use out-of-the-box with your apps:

The best of all is that you don't need to manage any backend infrastructure. In other words, it is the perfect solution for the Next.js real-time chat application we are building for this tutorial.

1. Create your Nhost project

First things first, we need to create a new Nhost project. So, if you haven't already, create an account on Nhost.io.

Then, log in to your Nhost dashboard and click the Create your first app button.

Nhost - Create your first app
Nhost - Create your first app

Next, give your new Nhost app a name, select a geographic region for your Nhost services and click Create App.

Nhost - Create a new app
Nhost - Create a new app

After a few seconds (yes, that's really fast!) you should get a PostgreSQL database, a GraphQL API for your data, file storage, and more, already set up.

Make sure to copy your Nhost backend URL as we are doing to need it within our Next.js project.

Nhost - Backend services up and running!
Nhost - Backend services up and running!

2. Connect Nhost with Next.js

Now that we have our new Nhost app up and running, let's go back to our code editor.

To work with Nhost from within our Next.js app, we'll use the Next.js SDK provided by Nhost. It's a wrapper around the Nhost React SDK which gives us a way to interact with our Nhost backend using React hooks.

You can install the Nhost Next.js SDK with:

yarn add @nhost/react @nhost/nextjs

Next, open your _app.tsx file as we'll now configure Nhost inside our app.

The Nhost Next.js SDK comes with a React provider named NhostNextProvider that makes the authentication state and all the provided React hooks available in our application.

So, import it at the top of your file and wrap your tsx with this provider component:

pages/_app.tsx
import '../styles/globals.css' import type { AppProps } from 'next/app' import { Toaster } from 'react-hot-toast' import { NhostNextProvider } from '@nhost/nextjs' function MyApp({ Component, pageProps }: AppProps) { return ( <NhostNextProvider> <Component {...pageProps} /> <Toaster /> </NhostNextProvider> ) } export default MyApp

Now, we need to instantiate a Nhost client and pass it to this provider to interact with our Nhost backend.

For that, create a new file named nhost.js inside a new folder called lib/, and paste the following code:

lib/nhost.ts
import { NhostClient } from '@nhost/nextjs' const nhost = new NhostClient({ backendUrl: process.env.NEXT_PUBLIC_NHOST_BACKEND_URL || '', }) export { nhost }

This code uses the NhostClient exported from the Nhost Next.js SDK to instantiate a new Nhost client.

Note that we must provide a Nhost backend URL to create this Nhost instance and link it to our Nhost backend. That's precisely the URL we copied earlier in the previous section.

Here, I'm using an environment variable, NEXT_PUBLIC_NHOST_BACKEND_URL, to store that URL.

So, make sure to create that variable too inside a .env.local file at the root of your project and paste your Nhost backend URL as the value:

.env.local
NEXT_PUBLIC_NHOST_BACKEND_URL="https://..."

Finally, go back to your _app.tsx, import your Nhost client from lib/nhost, and pass it to the <NhostNextProvider> component as a prop, along with the nhostSession from the page props.

pages/_app.tsx
import { NhostNextProvider } from '@nhost/nextjs' import { nhost } from '../lib/nhost' function MyApp({ Component, pageProps }: AppProps) { return ( <NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}> {/* ... */} </NhostNextProvider> ) }

We are now all set! That's all it takes to set up and configure Nhost inside your Next.js application.

Tasks

Add authentication to Next.js

1. Enable Github Authentication

Great! The next step is to allow our users to sign-up for our application and protect the chat from being accessible by non-authenticated users.

For authenticating users into our app, we'll use social login providers. For the sake of this tutorial, we'll only set up Github as our OAuth provider, but feel free to enable any other supported social providers if you'd like.

To let users log in with their Github account, we first need to enable the Github login provider from our Nhost dashboard.

Nhost - Enable Github login
Nhost - Enable Github login

Once enabled, copy the OAuth callback URL provided by Nhost as we'll need it for the next step. Please don't close your Nhost dashboard, as we'll come back to it in a moment to finish setting up Github login.

Nhost - Copy the OAuth callback URL
Nhost - Copy the OAuth callback URL

Now, we can create a new Github OAuth app. So, login to your own Github account, go to the Developers settings page, and click on the New OAuth app button

Github - Create new OAuth app
Github - Create new OAuth app

From there, choose a name for your OAuth app, enter http://localhost:3000 for the homepage URL, paste the OAuth callback URL you just copied into the corresponding field, and click Register application.

Github - Register application
Github - Register application

Then, generate a new client secret for your OAuth app.

Github - Generate new client secret
Github - Generate new client secret

Copy your GitHub Client ID and Secret, paste them into your Nhost Github settings page, and click on Confirm Settings.

Nhost - Confirm Github login settings
Nhost - Confirm Github login settings

To enable another login provider for your app, repeat the steps above by creating a new OAuth app from the corresponding provider website, and paste your client ID and secret into Nhost.

2. Authenticate users with the Nhost SDK

Now that Github login is properly configured, we can start implementing the authentication logic inside our Next.js app.

For that, we'll use the useProviderLink hook provided by the Nhost Next.js SDK.

import { useProviderLink } from '@nhost/nextjs';

This React hook accepts as an argument an optional options object to set some information about the user we'd like to sign up to our application, such as his display name or his default role. We won't define it in this tutorial as we'll use the default values provided by Nhost.

Once called, this hook returns an object with the login URLs for all the OAuth providers we've defined within our Nhost dashboard.

const { github } = useProviderLink();

We'll use the useProviderLink hook within our Login component. So, open up the corresponding file from your project, import the hook at the top, and call it inside your component:

components/Login.js
import { useProviderLink } from '@nhost/nextjs' const Login = () => { const { github } = useProviderLink() //... }

Next, pass the github login URL to the link tag inside your JSX:

components/Login.js
const Login = () => { const { github } = useProviderLink() return ( <div className="w-full max-w-md"> <div className="flex flex-col items-center border-opacity-50 px-4 py-8 sm:rounded-xl sm:border sm:px-8 sm:shadow-md"> <Image src={logo} alt="logo" /> <p className="mt-4 text-center">Please sign in to access the chat</p> <div className="mt-8 space-y-4"> <a href={github} className="flex items-center justify-center space-x-2 rounded-md border border-opacity-50 px-6 py-2 hover:bg-gray-50" > <Image src={githubLogo} alt="Github" width={32} height={32} /> <span>Sign in with Github</span> </a> </div> </div> </div> ) }

And that's it! Users can now log into our Next.js application using their Github account.

But before going further into building the rest of our app, we must protect it so that only authenticated users have access to the chat. For that, we'll once again use the Nhost SDK to check whether or not the current user is authenticated.

From your index.tsx file, import the useAuthenticationStatus hook from Nhost, and call it from within the Home component:

pages/index.tsx
import { useAuthenticationStatus } from '@nhost/nextjs' const Home: NextPage = () => { const { isLoading: isLoadingUser, isAuthenticated } = useAuthenticationStatus() //... }

The useAuthenticationStatus hook allows us to check if the current user is authenticated.

If not, we can render the Login component as users must authenticate themselves before being able to access the chat.

Otherwise, if the user is already authenticated, we render the corresponding UI for the chat application.

pages/index.tsx
import { useAuthenticationStatus } from '@nhost/nextjs' import Spinner from '../components/Spinner' import Login from '../components/Login' const Home: NextPage = () => { const { isLoading: isLoadingUser, isAuthenticated } = useAuthenticationStatus() return ( <div> {/* ... */} <main className="flex h-[calc(100vh-3.5rem)] flex-col items-center justify-center"> {isLoadingUser ? ( <Spinner /> ) : !isAuthenticated ? ( <Login /> ) : ( <> <div className="w-full flex-1 overflow-y-auto px-4"> <div className="mx-auto max-w-screen-md"> <div className="mt-8 border-b pb-6 text-center"> <h1 className="text-3xl font-extrabold"> Welcome to <br /> Nhost Chat </h1> <p className="mt-3 text-gray-500"> This is the beginning of this chat. </p> </div> </div> </div> <div className="mx-auto mb-6 w-full max-w-screen-md flex-shrink-0 px-4"> <Form /> </div> </> )} </main> </div> ) }

We can also use the useUserData hook provided by Nhost to retrieve the information about the currently authenticated user, such as his name or email address.

import { useUserData } from '@nhost/nextjs';

That way, we can render the UserMenu component if the user is authenticated to display his name, email address, and picture:

pages/index.tsx
import { useUserData } from '@nhost/nextjs' import UserMenu from '../components/UserMenu' const Home: NextPage = () => { const user = useUserData() return ( <div> <header className="..."> <div className="..."> <Image src={logo} /> {isAuthenticated && user ? <UserMenu {...user} /> : null} </div> </header> {/* ... */} </div> ) }

And finally, we can allow users to logout from the application using the useSignOut hook from the Nhost SDK, and call it from the UserMenu component:

components/UserMenu.tsx
import { useSignOut } from '@nhost/nextjs' const UserMenu = ({ email, displayName, avatarUrl }: UserMenuProps) => { const { signOut } = useSignOut() return ( <Menu as="div" className="..."> {/* ... */} <Transition> <Menu.Items className="..."> {/* ... */} <Menu.Item> <button className="..." onClick={signOut} > <LogoutIcon className="..." /> <span>Logout</span> </button> </Menu.Item> </Menu.Items> </Transition> </Menu> ) }

So now, if you try to access the app, you should see the Login component to log in with your Github account.

Github login component

And once successfully logged in, you should see the (empty) chat app.

Empty chat application
Tasks

Model data and permissions with Hasura

Nhost provides a PostgreSQL database to store our data and uses Hasura to dynamically and instantly generate a GraphQL API for that data.

It is very handy as we can query and mutate data without worrying about setting up an API ourselves or the corresponding server infrastructure.

So, to store and query the messages for our chat, we first need to define the data model for the chat messages. We'll do that from the Hasura console. So, go back to your Nhost dashboard, and from the data tab, copy your admin secret and click Open Hasura.

Nhost - Copy your admin secret and open Hasura
Nhost - Copy your admin secret and open Hasura

Then, paste your admin secret and click Enter on the next screen.

Nhost - Paste your admin secret to enter Hasura
Nhost - Paste your admin secret to enter Hasura

You should now get access to your Hasura console for your current Nhost project.

Now, let's create a new table in our database, named messages. This table will have the following columns:

  • id (type UUID and default gen_random_uuid()),
  • text (type Text),
  • authorId (type UUID),
  • createdAt (type Timestamp and default now())

In the Hasura Console, head over to the data tab section and click on the PostgreSQL database (from the left side navigation) that Nhost provides us. The database name should be default, and the schema name should be public.

Click on the public schema and the Create Table button.

Hasura - Create new table (1)
Hasura - Create new table (1)

Then, enter the values for creating the messages table as mentioned above. Also, specify the id column as the primary key of the table, and link the authorId column to the users.id column using a foreign key to link the users and messages tables together.

Hasura - Create new table (2)
Hasura - Create new table (2)

Once you are done, click on the Add Table button to create the table.

Great! We have created the messages table required for our chat application.

Before moving on with the next section, make sure to create a few messages inside your database from the Hasura console so that we have some data to fetch and display from our Next.js app.

Hasura - Add some data amunally
Hasura - Add some data amunally
Tasks

Query and mutate data with GraphQL

1. Configure permission rules

It's important to know that Hasura has an allow nothing by default policy to ensure that only roles and permissions you define explicitly have access to the GraphQL API and the underlying data.

In other words, right now your users can't retrieve/update any data from the messages table (or any other tables) through the GraphQL API provided by Hasura. That's because we haven't set any permissions for our users in Hasura yet.

Hasura supports role-based access control. So we can create rules for each role, table, and operation (select, insert, update and delete) that can check dynamic session variables, like the user ID.

In our case, we need to add permissions for the user role on the insert, select, update, and delete operations.

So, open the permissions tab for the messages table:

Hasura - Permissions for the messages table
Hasura - Permissions for the messages table

Let's start with the insert permissions:

Hasura - Insert permissions for the user role
Hasura - Insert permissions for the user role

To restrict the users to create new messages only for themselves, specify an _eq condition between the authorId and the X-Hasura-User-ID session variable, which is passed with each request.

Hasura - Insert permissions condition
Hasura - Insert permissions condition

Then, select the columns the users can define through the GraphQL API, set the value for the authorId column to be equal to the X-Hasura-User-ID session variable, and click Save Permissions.

Hasura - Insert permissions columns
Hasura - Insert permissions columns

Great! Now, add the permissions for the user role on all the other operations, as shown below.

  • The select operation:
Hasura - Select permissions for the user role
Hasura - Select permissions for the user role

For this one, we don't need to run any check as any (authenticated) users can read the messages from the chat.

Make sure to toggle all the columns as we'll need them to display the messages properly inside our Next.js app.

  • The update operation:
Hasura - Update permissions for the user role
Hasura - Update permissions for the user role

The update permissions check is the same as the insert one as we want to restrict the users from modifying the messages from other users.

Note that the users can update only the text column.

  • The delete operation:
Hasura - Delete permissions for the user role
Hasura - Delete permissions for the user role

We do the same thing for the delete permissions check. We allow users to delete only their own messages.

Finally, we need to add permissions for the user role on the select operation for the users table. This is necessary to fetch users data from the GraphQL API later.

Hasura - User permissions for the `users` table
Hasura - User permissions for the `users` table
Tasks

2. Set up the GraphQL relationship

Before using the GraphQL API inside our Next.js app, we have one more thing to set up.

Indeed, when we have created the messages table, we have defined a one-to-many relationship between the messages and the users table via a foreign key constraint. That relationship allows a user to have many messages, and each message to belong to only one user.

But to actually access the nested properties of a user from a message via the GraphQL API, we must add the following object relationship:

Hasura - One-to-many relationship
Hasura - One-to-many relationship

Thanks to that relationship, we can now fetch a list of messages with their author like so:

query { messages { id text author { displayName email } } }

We are all set with our database/Hasura configuration. Let's keep going and interact with our data through the generated GraphQL API.

Tasks

2. Read messages

Another great thing about Nhost is that we can connect to the provided GraphQL API with any GraphQL client we'd like. In this tutorial, we'll use the Apollo GraphQL client, but feel free to use any other library.

Did you know that the Nhost SDK even comes with its own GraphQL client? Check it out from the official documentation.

So, start by installing the following dependencies:

yarn add @nhost/react-apollo @apollo/client

Then, add the NhostApolloProvider from @nhost/react-apollo into your _app.tsx file. Make sure this provider is nested into NhostNextProvider, as it will need the Nhost context.

pages/_app.tsx.js
import { NhostApolloProvider } from '@nhost/react-apollo' function MyApp({ Component, pageProps }: AppProps) { return ( <NhostNextProvider nhost={nhost} initial={pageProps.nhostSession}> <NhostApolloProvider nhost={nhost}> <Component {...pageProps} /> <Toaster /> </NhostApolloProvider> </NhostNextProvider> ) }

From there, we can construct the query to retrieve the messages data using GraphQL.

So, create a new file named queries.ts inside the lib/ folder to define the GraphQL query to fetch all the messages.

import { gql } from '@apollo/client'; export const GET_MESSAGES = gql` subscription Messages { messages(order_by: { createdAt: asc }) { id text createdAt author { id avatarUrl displayName } } } `;

Note that we are using the order_by argument to sort the result by the createdAt column in ascending order. We are also retrieving the nested properties of each author from the messages. That's possible thanks to the relationship we defined earlier in Hasura.

Finally, instead of using a simple query operation, we are defining a GraphQL subscription. Subscriptions are useful for notifying our Next.js app in real-time about changes to the back-end data, such as the creation of a new object or updates to an important field.

In our case, it allows us to maintain an active connection to our GraphQL server (via WebSocket) and to receive new messages data as soon as they're available.

So, let's now use that subscription query inside our index.tsx file:

pages/index.tsx
import { useSubscription } from '@apollo/client' import Message, { MessageProps } from '../components/Message' import MessageSkeleton from '../components/MessageSkeleton' import { GET_MESSAGES } from '../lib/queries' const Home: NextPage = () => { const { isLoading: isLoadingUser, isAuthenticated } = useAuthenticationStatus() const user = useUserData() const { loading: isLoadingMessages, error, data, } = useSubscription(GET_MESSAGES, { skip: isLoadingUser || !isAuthenticated, }) let messages = data?.messages ?? [] return ( <div> {/* ... */} <main className="..."> {isLoadingUser ? ( <Spinner /> ) : !isAuthenticated ? ( <Login /> ) : ( <> <div className="..."> <div className="..."> {/* ... */} {isLoadingMessages ? ( <div className="my-6 space-y-4"> {[...new Array(5)].map((_, i) => ( <MessageSkeleton key={i} /> ))} </div> ) : error ? ( <p className="my-6 text-center text-red-500"> Something went wrong. Try to refresh the page. </p> ) : messages.length > 0 ? ( <ol className="my-6 space-y-2"> {messages.map((msg: MessageProps) => ( <li key={msg.id}> <Message {...msg} /> </li> ))} </ol> ) : ( <p className="my-6 text-center text-gray-500"> No messages yet. </p> )} </div> </div> {/* ... */} </> )} </main> </div> ) }

As you can see, we are using the useSubscription hook from Apollo to run the GraphQL subscription we've defined.

Note that we are skipping that query if the user is not authenticated yet, through the skip option.

We then render all the messages retrieved from the subscription to the screen using our Message component.

Fetch messages using a GraphQL subscription
Tasks

3. Create messages

So now that we can read messages from the database in real-time through a GraphQL subscription let's allow users to send new messages to the chat.

The query to do so is the following:

lib/queries.ts
export const CREATE_MESSAGE = gql` mutation CreateMessage($object: messages_insert_input!) { insert_messages_one(object: $object) { id } } `

Make sure to define this GraphQL mutation inside your queries.ts file.

Then, import it into your index.tsx file and pass it to the useMutation Apollo hook.

pages/index.tsx
import { useMutation } from '@apollo/client' import { CREATE_MESSAGE } from '../lib/queries' const Home: NextPage = () => { const [createMessage] = useMutation(CREATE_MESSAGE) //... }

Next, create a new function named createMessageHandler to perform the mutation request when the user submits the form, as shown below.

pages/index.tsx
const Home: NextPage = () => { //... const createMessageHandler = (text: string) => { if (!user) return return createMessage({ variables: { object: { text }, }, }) } return ( <div> {/* ... */} <main className="..."> <div className="..."> <Form onSubmit={createMessageHandler} /> </div> </main> </div> ) }

Go ahead and try sending a new message to the chat from your browser.

If you have followed all the steps, your message should be sent through the GraphQL API and stored in the PostgreSQL database. The message should then appears (almost) instantly on the screen, and without refreshing the page, thanks to the GraphQL subscription we're running.

Create new messages using a GraphQL mutation
Tasks

4. Update and delete messages

One feature users like about chat applications are the ability to edit and delete their messages.

Since we have already configured all the necessary permissions on the Hasura console, this is something we could easily achieve by defining the following GraphQL mutations:

queries.ts
export const UPDATE_MESSAGE = gql` mutation UpdateMessage($id: uuid!, $text: String!) { update_messages(where: { id: { _eq: $id } }, _set: { text: $text }) { returning { id } } } ` export const DELETE_MESSAGE = gql` mutation DeleteMessage($id: uuid!) { delete_messages(where: { id: { _eq: $id } }) { returning { id } } } `

Next, we can import those two mutations inside our index.tsx file, and pass them to two useMutation hooks, like so:

pages/index.tsx
import { GET_MESSAGES, CREATE_MESSAGE, DELETE_MESSAGE, UPDATE_MESSAGE, } from '../lib/queries' const Home: NextPage = () => { const [deleteMessage] = useMutation(DELETE_MESSAGE) const [updateMessage] = useMutation(UPDATE_MESSAGE) }

Then, we can call the corresponding mutation functions returned by Apollo, updateMessage and deleteMessage, from two different handler methods that we pass to the onEdit and onDelete props of the Message component:

pages/index.tsx
const Home: NextPage = () => { //... const deleteMessageHandler = (id: string) => { if (!id) return return deleteMessage({ variables: { id, }, }) } const updateMessageHandler = (id: string, text: string) => { if (!id || !text) return return updateMessage({ variables: { id, text, }, }) } return ( <div> {/* ... */} <main className="..."> {/* ... */} <ol className="..."> {messages.map((msg: MessageProps) => ( <li key={msg.id}> <Message {...msg} onDelete={deleteMessageHandler} onEdit={updateMessageHandler} /> </li> ))} </ol> </main> </div> ) }

Finally, from the Message component, check if the currently authenticated user is the author of the message before rendering the edit and delete buttons:

components/Message.tsx
import { useUserData } from '@nhost/nextjs' const Message = () => { const user = useUserData() const isAuthor = author?.id === user?.id //... }
Edit and delete messages
Tasks

Summary

Congratulations on getting this far 🥳!

That was a long tutorial, but we have achieved so much in just a couple of steps. Indeed, we have set up a Next.js app, a PostgreSQL database, and a GraphQL API. We have also implemented social login authentication, authorization, and real-time messaging. All of that, without managing any server/backend infrastructure, thanks to Nhost and Hasura.

I hope you enjoyed following this tutorial. If yes, feel free to share it, and follow me on Twitter to stay up to date about my upcoming tutorials and courses.

And if you've got any questions, please ask them on Twitter!

Oh, and I encourage you to share the app you built in this tutorial on Twitter. If you do, please mention @Nhostio and @gdangel0, so that we can take a look!

Share

Guest Authors

Write for the AlterClass blog to empower the developer community and grow your brand.

Join us

Stay up to date

Subscribe to the newsletter to stay up to date with tutorials, courses and much more!