🤔 What is XdoX?
XdoX is a web application that lets you start challenges and log your progress every day. You are also able to show your progress to the world via your unique profile page. These challenges can be anything from 100DaysOfCode to 30DaysOfRust to even 60DaysOfCooking!
It is also my submission for the Hasura x Hashnode Hackathon
❓ What is Hasura?
GraphQL is a query language for APIs with a schema. It comes with multiple features like the ability to query specific fields, do pagination, do aggregation queries, and a lot more.
However, making a GraphQL backend is more complicated than making a simple REST backend and that is where Hasura comes in. Hasura provides us with an easy way to make a GraphQL backend that connects our database with our application without the need to write a single line of code!
Hasura also has a cloud offering with a decent free tier so that we can get started with hosting our GaphQL backend without needing to worry about costs. It is Open Source and Self-Hostable as well.
📚 The Tech Stack
What all technologies did I use for XdoX?
For starters, I used Hasura for my application's backend.
Other than that, I used the following services -
Clerk to add authentication to my application. It also integrated well with Hasura and I was able to secure my backend by using JWT Auth (more on this later on in this article)
Heroku Postgres for my database. It also integrated well with Hasura
Vercel to host my frontend
And, here are the libraries and frameworks I used for the application -
- Next.js for my application's frontend
- TailwindCSS for styling my frontend
- Radix UI for un-styled UI components like Modals and Popovers
- Headless UI for transitions
- Apollo React Client for making GraphQL requests from my frontend. It also takes care of caching.
- Tiptap for the rich-text editor with markdown support that is used to log progress
🧐 How does XdoX work?
It is a simple 3-step process. One signs up for an account using Google or Email and then starts a challenge (e.g. 100DaysOfCode). Then one logs their progress every day.
Next, one can share their unique profile page to the world to show off their progress.
Also, it is not necessary to log your progress every day. The app is built in such a way that lets you be flexible with your challenges. Gone on a vacation? No problem, XdoX won't bug out for not logging your progress.
Securing the backend
Backends have direct access to the database and it is considered a best practice to secure them. I need to use the GraphQL API from my front-end and hence it has to be a public API. However, I must secure it so that only limited unauthorized requests and authorized requests can be made.
As I was using Clerk for user authentication, it didn't take me long to implement this. Clerk integrated with Hasura using JWT templates. Here is the documentation explaining how to implement this.
Here, we create a JWT template from the Clerk dashboard. Here is what mine looks like -
When making a request to the API, we pass in a header called Authorization
with a bearer token as the value. This is verified by Hasura using a signing key (this is set in Hasura via environment variables).
This is the code in the frontend that takes care of passing in the bearer token when making requests -
import {
ApolloClient,
ApolloProvider,
from,
HttpLink,
InMemoryCache,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { useSession } from "@clerk/clerk-react";
import { ReactNode } from "react";
const hasuraGraphqlApi = process.env.NEXT_PUBLIC_HASURA_GRAPHQL_API;
interface IApolloProviderWrapperProps {
children: ReactNode;
}
export const ApolloProviderWrapper = ({
children,
}: IApolloProviderWrapperProps) => {
const { getToken } = useSession();
const authMiddleware = setContext(async (_, { headers }) => {
const token = await getToken({ template: "hasura" });
return {
headers: {
...headers,
authorization: `Bearer ${token}`,
},
};
});
const httpLink = new HttpLink({
uri: hasuraGraphqlApi,
});
const apolloClient = new ApolloClient({
link: from([authMiddleware, httpLink]),
cache: new InMemoryCache(),
});
return <ApolloProvider client={apolloClient}>{children}</ApolloProvider>;
};
We simply get a bearer token by using the getToken
function made available to us by the Clerk React SDK and pass it in the Authorization
header.
Now, if the bearer token is valid, the X-Hasura-User-Id
header is added to the request that contains the user id of the user who is making the request. The headers for the user
role are passed in as well. Note that this is taken care of on Hasura's side.
I am also making some unauthenticated requests with a viewer
role. This has been set as the unauthorized role in my Hasura instance and is used in the public user profile pages. Here is the code that take cares of making an unauthenticated requests -
import {
ApolloClient,
ApolloProvider,
from,
HttpLink,
InMemoryCache,
} from "@apollo/client";
import { setContext } from "@apollo/client/link/context";
import { ReactNode } from "react";
const hasuraGraphqlApi = process.env.NEXT_PUBLIC_HASURA_GRAPHQL_API;
interface IApolloProviderWrapperProps {
children: ReactNode;
}
export const UnauthenticatedApolloProviderWrapper = ({
children,
}: IApolloProviderWrapperProps) => {
const authMiddleware = setContext(async (_, { headers }) => {
return {
headers: {
...headers,
"X-Hasura-User-Role": "viewer",
},
};
});
const httpLink = new HttpLink({
uri: hasuraGraphqlApi,
});
const apolloClient = new ApolloClient({
link: from([authMiddleware, httpLink]),
cache: new InMemoryCache(),
});
return <ApolloProvider client={apolloClient}>{children}</ApolloProvider>;
};
Setting row-level permissions for the data
Although the API is now secured, no data is accessible by default. We need to set up permissions and this will also let us limit the data one can access. For example, we will let a user only access their own user data and only access private challenges they have created.
Thankfully, Hasura again makes doing this extremely easy. Let us look at an example -
Here, I have setup insert permissions for the user
role in such a way that one can insert rows only where the user_id
column is equal to the user id of the user making the request (this was passed in as a header).
I am also allowing the user
to only update specific columns. Here, the id
column is auto-generated with the gen_random_uuid()
PostgreSQL function. The created_at
and updated_at
fields are also being taken care of by the backend.
I am also adding a column preset for the user_id
column that will be equal to the X-Hasura-User-Id
header. Now, that is crazy powerful!
Similarly, I have set permissions for update, select and delete for the user
role where I check that the user_id
column matches the X-Hasura-User-Id
header.
For the viewer
role, I have set it up this way -
Here, the viewer is only able to select rows from the database (that is, only read data). I have additionally added a check that makes sure that the challenge is public.
👓 What I learned from this Hackathon
Although I have used GraphQL in the past, my experience was quite limited. Also, I had never built a GraphQL backend, I was just using public GraphQL APIs. I had also never used Hasura before, nor did I ever use a SQL database for any production project.
This hackathon gave me a chance to explore the backend side of GraphQL through Hasura and understand the deeper concepts. I also had a great time using a PostgreSQL database, learning more about relational data. It is crazy powerful!
✨ Conclusion
Over the past month, I have worked on XdoX and have been exploring and learning a LOT of new things. I'm quite excited to see how XdoX does in the real world!
Bye, and have a nice day 😁🤞
Top comments (0)