Ian McPhail
In this tutorial we will use Clerk with Hasura to build a full-stack Next.js app with a database and GraphQL API, all without having to write any backend code.
Hasura provides a GraphQL engine that can help you build real-time APIs and ship modern apps faster. Although Hasura itself does not handle authentication, you can bring your own auth server and integrate it with Hasura via JWTs. This is where Clerk fits in.
In this tutorial we will use Clerk with Hasura to build a full-stack Next.js app with a database and GraphQL API, all without having to write any backend code.
If you would like to skip ahead and see the completed codebase, browse to the repo here.
This tutorial makes the following assumptions:
npm
(or yarn
if you prefer) for package managementcreate-next-app
)We’re going to start off with creating a new project from the Clerk dashboard. I’m going to name this application “More Cookies Please” (you’ll see why shortly) and leave the default options selected for authentication strategy and turn on social login with Google.
The next thing we need to do is navigate to JWT Templates from the left menu and create a template based on Hasura.
The next thing we need to do is navigate to JWT Templates from the left menu and create a template based on Hasura.
Click the “Apply changes” button and navigate back to the Home dashboard. Here we’re going to copy the Frontend API key.
That’s all we need to do in the Clerk dashboard.
Now it’s time to clone the clerk-hasura-starter repo from GitHub. Name this project directory more-cookies-please
to match the Clerk instance name.
1git clone https://github.com/clerkinc/clerk-hasura-starter.git more-cookies-please
Once you have the repository downloaded to your computer, run the following commands to change into the directory, install the package dependencies, and copy the .env.local.sample
so we can set a couple environment variables:
1cd more-cookies-please/2npm install3cp .env.local.sample .env.local
Add in the Frontend API key from Clerk that was copied earlier. We’re also going to set the GraphQL endpoint even though it hasn’t been created yet.
1NEXT_PUBLIC_CLERK_FRONTEND_API=<YOUR_FRONTEND_API>2NEXT_PUBLIC_HASURA_GRAPHQL_API=http://localhost:8080/v1/graphql
Note: CLERK_API_KEY
is available in the sample but isn’t needed for this tutorial.
Once those environment variables are set, start up the application with npm run dev
Open your web browser to http://localhost:3000 and you should see the starter application homepage, which prompts you to sign up. So now let’s sign up for an account.
Click the Sign up button and you will be redirected to a Sign Up form generated for your application with Clerk components. I’m going to choose Sign up with Google since it's the fastest (and doesn’t require a new password).
Once you’ve signed up and logged in, you should see the following screen:
Now it’s time to customize this application. We’re going to install a little package I created based on the excellent react-rewards library and inspired by Cookie Clicker (thank @bsinthewild for reminding me of this).
1npm install react-cookie-clicker
Note: ⚠️ You will see some warnings about conflicting peer dependencies due to a mismatch of React versions, but it’s safe to proceed.
We’re going to create a new file called MoreCookies.js
inside of the components/
folder.
Because this library does not support server-side rendering (SSR), we need to make use of the dynamic imports from Next.js or we’ll get some nasty errors.
1import dynamic from "next/dynamic";2const CookieClicker = dynamic(() => import("react-cookie-clicker"), {3ssr: false4});56const MoreCookies = () => {7return (8<div style={{ marginTop: 150 }}>9<CookieClicker />10<h2>Click the cookie</h2>11</div>12);13};1415export default MoreCookies;
Now let’s add the component to pages/index.js
:
1import styles from "../styles/Home.module.css";2import Link from "next/link";3import { SignedIn, SignedOut } from "@clerk/nextjs";4import MoreCookies from "../components/MoreCookies";56const SignupLink = () => (7<Link href="/sign-up">8<a className={styles.cardContent}>9<img src="/icons/user-plus.svg" />10<div>11<h3>Sign up for an account</h3>12<p>13Sign up and sign in to explore all the features provided by Clerk14out-of-the-box15</p>16</div>17<div className={styles.arrow}>18<img src="/icons/arrow-right.svg" />19</div>20</a>21</Link>22);2324const Main = () => (25<main className={styles.main}>26<SignedOut>27<h1 className={styles.title}>Welcome to your new app</h1>28<p className={styles.description}>29Sign up for an account to get started30</p>31<div className={styles.cards}>32<div className={styles.card}>33<SignupLink />34</div>35</div>36</SignedOut>37<SignedIn>38<MoreCookies />39</SignedIn>40</main>41);
The ClerkFeatures
component can be removed, but the Footer
and Home
components should remain untouched.
You can see here we are making use of the SignedIn
and SignedOut
components from Clerk. We can also finally see the big cookie button!
Go ahead and click it for a nice reward. You’ve earned it. 🍪
Clicking the cookie is a lot of fun, but what is even more fun? Keeping count of all those clicks!
This is where the Hasura GraphQL integration comes in.
There are two different ways we can connect to Hasura: we can use Hasura Cloud or Hasura Core running from a Docker container. We’re going to do the latter for this tutorial, but you can see instructions for connecting with Hasura Cloud in our integration documentation.
The starter repo already contains the docker-compose.yml
file we need.
The only part we need to update here is the JWT secret. Uncomment the line for HASURA_GRAPHQL_JWT_SECRET
and add in the value for your Clerk Frontend API. The JWT URL path points to the JSON Web Key Set (JWKS) endpoint we have set up for your application.
1HASURA_GRAPHQL_JWT_SECRET: '{"jwk_url":"https://<YOUR_FRONTEND_API>/.well-known/jwks.json"}'.
Note: Make sure the https:// protocol is included in the URL in front of the Frontend API.
Once the JWT secret is set, run the following command:
1docker compose up -d
This will spin up Docker services for GraphQL engine as well as a Postgres database.
You can confirm that the services are running correctly with the following:
1docker compose ps
If all is good, you should see output similar to:
1COMMAND SERVICE STATUS PORTS2"graphql-engine serve" graphql-engine running 0.0.0.0:8080->8080/tcp3"docker-entrypoint.s…" postgres running 5432/tcp
Head to http://localhost:8080/console to open the Hasura console.
Hasura has already done the work of setting up a GraphQL endpoint for us and also provided the GraphiQL integrated development environment (IDE) to explore the API.
It’s time to set up the database to keep the score count.
Navigate to the Data page and fill out the form to Connect Existing Database. (Remember we have the Postgres one running from Docker Compose?)
Name the database default
(or something more clever) and input the database URL copied from the docker-compose.yml
file. Click connect and the data source will be added.
The next step is to create the database table named scoreboard
and add two fields:
user_id
is a Text field that will contain the user ID from Clerkcount
is an Integer field that will keep track of the click countSet the user_id
as the Primary Key for the table. Then click the Add Table button.
The next thing we need to do is set permissions for the “user” role. Click on the Permissions tab and enter user
as a new role. Then we need to set the basic CRUD (Create, Read, Update, Delete) operations on the table.
For Insert permissions, set the following values:
count
checkeduser_id
from session variable X-Hasura-User-Id
With the Clerk integration, the user ID will be set as the session variable and Hasura will then set that as the user_id
column when the request is made.
For Select permissions, set the following values:
{"user_id":{"_eq":"X-Hasura-User-Id"}}
count
and user_id
checkedThe custom check ensures only the current authenticated user can read their own count. If the user ID from the session variable matches the one from the table, the user is granted read permission to every column in their database row.
For Update permissions, set the following values:
{"user_id":{"_eq":"X-Hasura-User-Id"}}
count
checkedHaving the same custom check prevents another authenticated user from updating someone else’s count.
We can skip over Delete permissions since we aren’t implementing a mechanism to delete user records from the scoreboard.
Your final permissions access chart should look like the following:
Now it’s time to make authenticated requests from the codebase to Hasura.
If you take a look at hooks/index.js
, you can see the useQuery
hook that has been set up. It makes use of graphql-request, a minimal GraphQL client, with the useSWR hook to perform query requests to the GraphQL endpoint.
1import { request } from "graphql-request";2import { useAuth } from "@clerk/nextjs";3import useSWR from "swr";45export const useQuery = (query, variables, blockRequest) => {6if (!query) {7throw Error("No query provided to `useQuery`");8}9const { getToken } = useAuth()10const endpoint = process.env.NEXT_PUBLIC_HASURA_GRAPHQL_API;11const fetcher = async () =>12request(endpoint, query, variables, {13authorization: `Bearer ${await getToken({ template: "hasura" })}`14});1516return useSWR(query, blockRequest ? () => {} : fetcher);17};
What we’re doing is reading the custom JWT (from the template we named hasura
) from the session object provided by Clerk. Note that the call to getToken
is asynchronous and returns a Promise that needs to be resolved before accessing the value.
We pass the custom fetcher function, which accepts a GraphQL query and optional variables, to useSWR
. The blockRequest
parameter is something we’ll make use of later to prevent certain calls from happening.
Let’s try this out to make sure we can get some data. Open up components/MoreCookies.js
and import the useQuery
hook and log the data
to the console:
1import dynamic from "next/dynamic";2import { useQuery } from "../hooks";3const CookieClicker = dynamic(() => import("react-cookie-clicker"), {4ssr: false5});67const MoreCookies = () => {8const { data } = useQuery(`query { scoreboard { count } }`);9console.log("data >>", data);1011return (12<div style={{ marginTop: 150 }}>13<CookieClicker />14<h2>Click the cookie</h2>15</div>16);17};1819export default MoreCookies;
If all went well, you should see the following:
1data >> undefined2data >> {scoreboard: Array(0)}
The data is undefined
at first but then gets populated. scoreboard
is an empty Array because we haven’t recorded any click counts yet. So let’s do that now.
In order to make the GraphQL mutation we’re going to create another custom hook in hooks/index.js
:
1export const useCountMutation = (count, data) => {2const prevCount = data?.scoreboard[0]?.count ?? 0;3const blockRequest = count < 1 || prevCount === count;45// Block mutation if count is less than 1 or equal to previous value6return useQuery(7`mutation {8insert_scoreboard_one(9object: { count: ${count} },10on_conflict: { constraint: scoreboard_pkey, update_columns: count }) {11count12user_id13}14}`,15null,16blockRequest17);18};
This hook accepts the count
as the first parameter and the data
object containing previous data as the second parameter. It makes use of the useQuery
hook to apply the insert_scoreboard_one
insert mutation as an upsert. Instead of adding multiple rows to the database table, the on_conflict
argument sets a constraint on the primary key (user_id
) and if it already exists, only the count
column will be updated. Both count
and user_id
values are returned from a successful mutation.
If we replace the useQuery
with useCountMutation
in the MoreCookies
component, we can give us some credit for those clicks we’ve already made. (I set mine at 10
but you can be more or less generous.)
1import dynamic from "next/dynamic";2import { useCountMutation } from "../hooks";3const CookieClicker = dynamic(() => import("react-cookie-clicker"), {4ssr: false5});67const MoreCookies = () => {8const { data } = useCountMutation(10);9console.log("data >>", data);1011return (12<div style={{ marginTop: 150 }}>13<CookieClicker />14<h2>Click the cookie</h2>15</div>16);17};1819export default MoreCookies;
If you look at the browser console, you should now see something like:
1data >> {insert_scoreboard_one: {count: 10, user_id: 'user_29IqLFGiidcpkwqplE1F8C8EnD1'}}
You can confirm that this made it into the Postgres database by going to the Data tab in the Hasura Console and clicking into scoreboard and Browse Rows:
Success! The count (fake or not) has made it into the database and is associated with the authenticated user.
So now that everything is working as intended, we can go ahead and connect it all together. We’ll add one more custom hook that performs both the initial useQuery
and the useCountMutation
. This is going to need both useState
and useEffect
from React so make sure you import those.
1export const useScoreboard = () => {2const [count, setCount] = useState(0);3const { data } = useQuery(`query {4scoreboard { count, user_id }5}`);6const increment = () => {7setCount(count + 1);8};910// Perform mutation on count11useCountMutation(count, data);1213useEffect(() => {14if (!count && data?.scoreboard[0]) {15// Set initial count from database16setCount(data.scoreboard[0].count);17}18}, [count, data]);1920return [count, increment];21};
It returns the current count
as well as an increment
function, both of which we can pass as props to the <CookieClicker />
component.
1import dynamic from "next/dynamic";2import { useScoreboard } from "../hooks";3const CookieClicker = dynamic(() => import("react-cookie-clicker"), {4ssr: false5});67const MoreCookies = () => {8const [count, increment] = useScoreboard();910return (11<div style={{ marginTop: 150 }}>12<CookieClicker count={count} onClick={increment} />13<h2>Click the cookie</h2>14<p>Current count: {count}</p>15</div>16);17};1819export default MoreCookies;
By setting the count
and onClick
props on CookieClicker
, it will now reward you with cookies based on the number of times the button is clicked. Keep clicking for more cookies!
Hope you had fun building this. You now have a complete Cookie Clicker app built using Clerk, Hasura, and Next.js — with no backend code required! To take it even further, you could implement an actual scoreboard that keeps track of all the cookie clicks from multiple users. Then deploy this app to production, share it with your friends, and see how much idle time they have on their hands. 😆
If you enjoyed this tutorial or have any questions, feel free to reach out to me (@devchampian) on Twitter, follow @ClerkDev, or join our support Discord channel. Happy coding!
Start completely free for up to 10,000 monthly active users and up to 100 monthly active orgs. No credit card required.
Learn more about our transparent per-user costs to estimate how much your company could save by implementing Clerk.
The latest news and updates from Clerk, sent to your inbox.