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.
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.
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.
It provides a suite of backend services that you can use out-of-the-box with your apps:
- A PostgreSQL database
- An instant GraphQL API from Hasura
- Authentication with Hasura Auth
- Storage with Hasura Storage
- Serverless Functions
- The Nhost CLI for local development
- The Nhost SDK for JavaScript, React, and Next.js
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.
Next, give your new Nhost app a name, select a geographic region for your Nhost services and click Create 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.
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:
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:
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:
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.
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.
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.
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.
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
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.
Then, generate a new client secret for your OAuth app.
Copy your GitHub Client ID and Secret, paste them into your Nhost Github settings page, and click on Confirm 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:
import { useProviderLink } from '@nhost/nextjs' const Login = () => { const { github } = useProviderLink() //... }
Next, pass the github
login URL to the link tag inside your JSX:
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:
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.
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:
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:
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.
And once successfully logged in, you should see the (empty) chat app.
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.
Then, paste your admin secret and click Enter on the next screen.
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 defaultgen_random_uuid()
),text
(type Text),authorId
(type UUID),createdAt
(type Timestamp and defaultnow()
)
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.
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.
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.
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:
Let's start with the insert
permissions:
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.
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.
Great! Now, add the permissions for the user
role on all the other operations,
as shown below.
- The select operation:
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:
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:
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.
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:
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.
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.
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:
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.
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:
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.
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.
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.
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:
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:
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:
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:
import { useUserData } from '@nhost/nextjs' const Message = () => { const user = useUserData() const isAuthor = author?.id === user?.id //... }
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!
Guest Authors
Write for the AlterClass blog to empower the developer community and grow your brand.
Join usStay up to date
Subscribe to the newsletter to stay up to date with tutorials, courses and much more!