(Flexibble) Full Stack Next.js application built using Grafbase and NextAuth.js

About Flexibble
The main idea behind Flexibble is to build a platform for developers to showcase their remarkable work in the community, but also to uncover a wellspring of inspiration from fellow projects and get hired.
Content
In this blog post, I will try to cover the major parts on how to set up Grafbase in your own project, use Authentication with Grafbase and NextAuth.js, and show how Grafbase makes our lives easy after initial setup and spins up all the queries and mutation (which we can explore using Grafbase Pathfinder).
Functionalities of Project
Login & Signup using Google which is provided by NextAuth.js
CRUD (Create, Read, Update, Delete) operations on the project
Functionality to view a particular User's project or profile
Technologies Used
Next.js
Grafbase
NextAuth.js
The thought process of building up the Project (just the Grafbase & NextAuth.js part)
Part-1: Setting up the Grafbase account, database, and the major configurations
Part-2: The whole setup of Authentication with NextAuth.js and Grafbase
Part-3: Creating the main operation functionalities of the project using Grafbase PathFinder
Setting up Grafbase ( Part-1)
Go to Grafbase's official website (https://grafbase.com/) and signup if you don't have an account or log in if you already have one.
Initially, you will see an empty dashboard with no projects. That's the first step in setting up the whole thing.
Now install the Grafbase SDK into your project by going into the terminal and typing the command
npm i @grafbase/sdk, this will install the Grafbase SDK into the project as a Dev Dependency.Go to your local project and in the terminal use the following command
npx grafbase init --config-format typescriptto set up Grafbase for your project.You will see a new folder being created named
grafbasein your project folder structure.In the
grafbasefolder you will have a.envfile and agrafbase.config.ts file.You will have to open up the
grafbase.config.tsfile and make the configurations there, By default it will have a boilerplate code for configurations, you can modify the code according to your choice.You can refer to these resources to do configurations according to your needs:
---> https://grafbase.com/docs/database
---> https://www.npmjs.com/package/@grafbase/sdkWe can set up all the Models, Schemas, and Authentication in the
grafbase.config.tsfile and Grafbase will take care of everything, and it will even provide suggestions forqueriesandmutationswhich can be used in functionalities. We will see it in the later part of this blog.If we try to condense our whole project and identify the models, then it has two models which are mainly "User" Model to take care of users who signup and the "Project" Model to store information about the projects, here is how my
grafbase.config.tsfile looksLink: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/grafbase/grafbase.config.ts
import { g, auth, config } from "@grafbase/sdk"; // @ts-ignore const User = g .model("User", { name: g.string().length({ min: 2, max: 20 }), email: g.string().unique(), avatarUrl: g.url(), description: g.string().optional(), githubUrl: g.url().optional(), linkedinUrl: g.url().optional(), projects: g .relation(() => Project) .list() .optional(), }) .auth((rules) => { rules.public().read(); }); // @ts-ignore const Project = g .model("Project", { title: g.string().length({ min: 3 }), description: g.string(), image: g.url(), liveSiteUrl: g.url(), githubUrl: g.url(), category: g.string().search(), createdBy: g.relation(() => User), }) .auth((rules) => { rules.public().read(), rules.private().create().delete().update(); }); const jwt = auth.JWT({ issuer: "grafbase", secret: g.env("NEXTAUTH_SECRET"), }); export default config({ schema: g, auth: { providers: [jwt], rules: (rules) => rules.private(), }, });'g' is a schema generator using which we are creating two Models which are "User" and "Project", the fields show the attributes of the models with validators chained along them are self-explanatory.
One thing here we can take note is of the
.relation()which binds the two models:
a)g.relation(() => Project).list().optional()in the "User" Model tells that the "User" Model has a relation with "Project" Model ( that is user creates projects in the DB), which is a list of projects and it is optional as initially, a new user sign's up, projects the user has to show is null.b)
g.relation(() => User)in the "Project" Model simply means that a oroject is binded by the user who creates the project.We will look at the auth part in Part-2
After following the above steps the basic setup is done locally for the Grafbase next, we have to create a new GitHub repository and push the code with the setup done till now to the repository.
Now, go to the Grafbase website, and go to your dashboard. As of now it should be empty. You can a new project by clicking on the create project. After that import the repository from GitHub to Grafbase.
After importing the project from the repository, the project is connected to the Grafbase and you can see it on the dashboard. Click on the connect button which would be visible on the top-right. On clicking it you can see the Grafbase
API_EndpointandAPI_KEYwhich you can save for now on the notepad, we will use these to connect to the database hosted on the cloud when the project is live and hosted.
NextAuth and Grafbase (Part-2)
Authentication is the process that ensures only authorized individuals gain access to sensitive systems and data, shielding against unauthorized entry.
So let's start...
Before we move forward let me show you the layout.tsx file of the project. The whole Authentication part is set up in the Navbar for my project.
Link: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Navbar />
<main>{children}</main>
<Footer />
</body>
</html>
);
}
I am using NextAuth.js (https://next-auth.js.org/) for authentication and the terms used from now on can be related to the library. So from now on if you don't see a familiar term please visit the following links for more information:
---> https://next-auth.js.org/getting-started/introduction
---> https://www.npmjs.com/package/next-authNow install the next-auth library into your project by going into the terminal and typing the command
npm install next-auth.As for the folks who have used Next.js previously know that using
<Navbar/>in thelayout.tsxfile makes it common for all the routes and pages. So in the<Navbar/>we are looking for asessionwhich we get from the auth providers when the user is logged in. To get thatsessionwe call thegetServerSession. If thesessionis not available it means the user is not signed in. If the user is not signed we can show the list of authentication providers (in our case which would be Google )A snippet from
<Navbar/>components showing the above functionalityLink-1: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/components/Navbar.tsx
Link-2: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/lib/session.ts
const session = await getCurrentUser();
<div className="flexCenter gap-4">
{session?.user ? (
<>
<ProfileMenu session={session} />
{console.log(session)}
<Link href="/create-project">Share Work</Link>
</>
) : (
<AuthProviders />
)}
</div>
export async function getCurrentUser() {
const session = (await getServerSession(authOptions)) as SessionInterface;
return session;
}
Here the
<AuthProviders/>component is nothing but a component that just iterates over the list of auth providers that we are providing and shows it to the user.Before we start using the
sessionand functionality we have to set up a few things.a) in the
app/api/auth/[...nextauth]/route.tswe have to write the following:import NextAuth from "next-auth/next"; import { authOptions } from "@/lib/session"; const handler = NextAuth(authOptions); export { handler as GET, handler as POST };Here we are importing
NextAuthand passing in all theauthOptionswhich we create in thesession.tsfile which I will discuss below.b) in the
app/api/auth/token/route.tswe have to write the following:Link:https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/app/api/auth/token/route.ts
import { getToken } from "next-auth/jwt"; import { NextRequest, NextResponse } from "next/server"; const secret = process.env.NEXTAUTH_SECRET; export async function GET(req: NextRequest) { const token = await getToken({ req, secret, raw: true }); return NextResponse.json({ token }, { status: 200 }); }Here we are looking for the token along with the secret to encode and decode in the process of authentication, the
NEXTAUTH_SECRETcan be created by following the steps mentioned here in this link:---> https://next-auth.js.org/configuration/options#nextauth_secret
You can use the following tool to execute the command mentioned in the reference link above:
Now after all the nitty-gritty, we can discuss the important
session.tsfile where all the magic happens.Link: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/lib/session.ts
import { getServerSession } from "next-auth/next"; import { NextAuthOptions, User } from "next-auth"; import GoogleProvider from "next-auth/providers/google"; import jsonwebtoken from "jsonwebtoken"; import { JWT } from "next-auth/jwt"; import { SessionInterface, UserProfile } from "@/commons.types"; import { createUser, getUser } from "./actions"; export const authOptions: NextAuthOptions = { providers: [ GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }), ], jwt: { encode: ({ secret, token }) => { const encodedToken = jsonwebtoken.sign( { ...token, iss: "grafbase", exp: Math.floor(Date.now() / 1000) + 60 * 60, }, secret ); return encodedToken; }, decode: async ({ secret, token }) => { const decodedToken = jsonwebtoken.verify(token!, secret) as JWT; return decodedToken; }, }, theme: { colorScheme: "light", logo: "/logo.png", }, callbacks: { async session({ session }) { const email = session?.user?.email as string; try { const data = (await getUser(email)) as { user?: UserProfile }; console.log("this is the session where new one is created"); console.log(data); const newSession = { ...session, user: { ...session.user, ...data?.user, }, }; return newSession; } catch (error) { console.log("Error retrieving user data", error); return session; } }, async signIn({ user }: { user: User }) { try { // get the user if they exist const userExists = (await getUser(user?.email as string)) as { user?: UserProfile; }; // if don't exist create them if (!userExists.user) { await createUser( user.name as string, user.email as string, user.image as string ); console.log("user successfully created"); } return true; } catch (error: any) { console.log(error); return false; } }, }, }; // asking for the session export async function getCurrentUser() { const session = (await getServerSession(authOptions)) as SessionInterface; return session; }The
getCurrentUser()is created here and passed to the<Navbar/>component to be used. Here as we are using Google Provider, we have to pass inGOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRETfor the Google Provider to work.You can acquire them by going through this guide:
https://www.balbooa.com/gridbox-documentation/how-to-get-google-client-id-and-client-secretThat is for providers, as we are passing GoogleProvider. You can pass in multiple providers. You can learn all about them and how to set them up by looking at these docs:
https://next-auth.js.org/configuration/providers/oauthAfter that, we are working on
jwt, where we are doing theencodeanddecodeprocess. And in the process, we are using a package calledjsonwebtokenyou can install it using the commandnpm install jsonwebtoken.While doing so we are also informing that our
issueris Grafbase.
We connect this to our Grafbase config setup by this snippet.Link: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/grafbase/grafbase.config.ts
const jwt = auth.JWT({ issuer: "grafbase", secret: g.env("NEXTAUTH_SECRET"), });Which can be found above. Here also the
NEXTAUTH_SECRETused is the same as the one created above.It is also advised to include it in the
.envfile inside thegrafbasefolder which is created at the start so that it has access.After
jwtthe most important option is thecallbacksinside which we havesessionandsigIn. This is the samesessionthat is available everywhere. Before we touchsessionslet's cover thesignIn.In the
signInwe can see that, we are following a pattern where if a user exists, get that user from the Grafbase database usinggetUserquery, and if the user doesn't exists create a new user usingcreateUsermutation to the Grafbase database. If an error occurs log it.the
getUserquery andcreateUsermutation would be explained in Part-3 and how Grafbase makes it so easy for usFrom the
sessionprovided by the provider which is Google Provider we can extract the email and from the email, we can query the Grafbase database. If the user exists or not then get the details from the Grafbase database append them to the currentsessionand returnnewsession.This is the whole process of authentication where the major behind-the-scenes work is done by the next-auth, and provides us with an easy abstraction with all the features
The whole auth setup is done.
If you could recall the initial config where we set up some auth using.auth()that's a feature provided by Grafbaase to make our lives easy telling us what part is accessible (public) and what part is not-accessible (private).Link: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/grafbase/grafbase.config.ts
a)
.auth((rules) => { rules.public().read(); });on Users means that the profiles are visible and users can read them if they are not authenticated also.b)
.auth((rules) => { rules.public().read(), rules.private().create().delete().update(); });on Project means that the projects are read-only if not authenticated, and if authenticated then the user can create, delete and update.With just a few lines of code and minimum files, we were able to set up the full authentication for the project using NextAuth.js and Grafbase. The combination of NextAuth.js & Grafbase is very powerful for full-stack projects
Creating Queries & Mutations (Part-3)
As of now, we have done all the hard work of setting up Grafbase on our project and secured our project with authentication using both Grafbase and NextAuth.js. Now it's time to reap the rewards of all that initial setup.
We finally move into the part where we focus on the major functionalities of creating users and performing all the CRUD operations on the projects and also querying both users and projects.
So Let's dive in....
Let's install the
graphql-requestlibrary into your project by going into the terminal and typing the commandnpm i graphql-request
Can read more about it here:
https://www.npmjs.com/package/graphql-requestSo in the folder structure, I am storing all the raw GraphQL queries in the
graphqlfolder and all the functions calling those queries in theactions.tsfileLink-1: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/graphql/index.ts
Link-2: https://github.com/reddymahendra52/Flexibble-Grafbase/blob/main/lib/actions.ts
Here is how the
action.tsfile looksimport { ProjectForm } from "@/commons.types"; import { createProjectMutation, createUserMutation, deleteProjectMutation, getProjectByIdQuery, getProjectsOfUserQuery, getUserQuery, projectsQuery, updateProjectMutation, } from "@/graphql"; import { GraphQLClient } from "graphql-request"; const isProduction = process.env.NODE_ENV === "production"; const apiURL = isProduction ? process.env.NEXT_PUBLIC_GRAFBASE_API_URL || "" : "http://127.0.0.1:4000/graphql"; const apiKey = isProduction ? process.env.NEXT_PUBLIC_GRAFBASE_API_KEY || "" : "letmein"; const serverUrl = isProduction ? process.env.NEXT_PUBLIC_SERVER_URL : "http://localhost:3000"; const client = new GraphQLClient(apiURL); const makeGraphQLRequest = async (query: string, variables = {}) => { try { return await client.request(query, variables); } catch (error) { throw error; } }; export const getUser = (email: string) => { client.setHeader("x-api-key", apiKey); return makeGraphQLRequest(getUserQuery, { email }); }; export const createUser = (name: string, email: string, avatarUrl: string) => { client.setHeader("x-api-key", apiKey); const variables = { input: { name, email, avatarUrl, }, }; return makeGraphQLRequest(createUserMutation, variables); }; export const fetchToken = async () => { try { const response = await fetch(`${serverUrl}/api/auth/token`); return response.json(); } catch (err) { throw err; } }; export const uploadImage = async (imagePath: string) => { try { const response = await fetch(`${serverUrl}/api/upload`, { method: "POST", body: JSON.stringify({ path: imagePath, }), }); return response.json(); } catch (err) { throw err; } }; export const createNewProject = async ( form: ProjectForm, creatorId: string, token: string ) => { const imageUrl = await uploadImage(form.image); if (imageUrl.url) { client.setHeader("Authorization", `Bearer ${token}`); const variables = { input: { ...form, image: imageUrl.url, createdBy: { link: creatorId, }, }, }; return makeGraphQLRequest(createProjectMutation, variables); } }; export const fetchAllProjects = (category?: string, endcursor?: string) => { client.setHeader("x-api-key", apiKey); return makeGraphQLRequest(projectsQuery, { category, endcursor }); }; export const getProjectDetails = (id: string) => { client.setHeader("x-api-key", apiKey); return makeGraphQLRequest(getProjectByIdQuery, { id }); }; export const getUserProjects = (id: string, last?: number) => { client.setHeader("x-api-key", apiKey); return makeGraphQLRequest(getProjectsOfUserQuery, { id, last }); }; export const deleteProject = (id: string, token: string) => { client.setHeader("Authorization", `Bearer ${token}`); return makeGraphQLRequest(deleteProjectMutation, { id }); }; export const updateProject = async ( form: ProjectForm, projectId: string, token: string ) => { // to check if the user updated the image or not // or only updated the information function isBase64DataURL(value: string) { const base64Regex = /^data:image\/[a-z]+;base64,/; return base64Regex.test(value); } let updatedForm = { ...form }; const isUploadingNewImage = isBase64DataURL(form.image); // upload to cloudinary if (isUploadingNewImage) { const imageUrl = await uploadImage(form.image); if (imageUrl.url) { updatedForm = { ...updatedForm, image: imageUrl.url }; } } client.setHeader("Authorization", `Bearer ${token}`); const variables = { id: projectId, input: updatedForm, }; return makeGraphQLRequest(updateProjectMutation, variables); };At the top, we can see I am importing all the raw
querieswhich will be discussed immediately below. Initially, we can see all the environment variables which needs to be used, we make a set of variables to use for production and other to use locally, it is one of the best practices followed in the industry.makeGraphQLRequest()is a generalized function that we will reuse again and again in the below functions, it takes in two parameters which are thequeryand the other one isvariables(the input values which we pass), the query is imported and passed.All the functions are self-explanatory. We can see the function names attributed to what it does and the respected
queryandvariablesbeing passed, they show standard CRUD operations.As I mentioned earlier, you might be thinking what does Grafbase do here? So let me explain:
Grafbase has
Pathfinderwhich can be found locally when you run the following commandnpx grafbase devonhttp://localhost:4000or on the project dashboard.The
Pathfinderfeature takes a look at our config file and based on our schema and models automatically populates all the possible queries and mutations which we can create.Which might look like this as shown below:


As we can see these are probably all the possible mutations and queries we can find, and on clicking on each Query on PathFinder, you can create the document of what you want with filters and lot of other options.
Using this you can very easily write Queries without breaking your head.
Here is an example:

Using Grafbase and with the help of Pathfinder all the queries and mutations are written for the project, which is as follows:
export const createProjectMutation = ` mutation CreateProject($input: ProjectCreateInput!) { projectCreate(input: $input) { project { id title description createdBy { email name } } } } `; export const getUserQuery = ` query GetUser($email: String!){ user(by: {email: $email}){ id name email avatarUrl description githubUrl linkedinUrl } } `; export const createUserMutation = ` mutation CreateUser($input: UserCreateInput!){ userCreate(input: $input){ user{ name email avatarUrl description githubUrl linkedinUrl id } } } `; export const projectsQuery = ` query getProjects() { projectSearch(first:10) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { node { title githubUrl description liveSiteUrl id image category createdBy { id email name avatarUrl } } } } } `; export const getProjectByIdQuery = ` query GetProjectById($id: ID!) { project(by: { id: $id }) { id title description image liveSiteUrl githubUrl category createdBy { id name email avatarUrl } } } `; export const getProjectsOfUserQuery = ` query getUserProjects($id: ID!, $last: Int = 4) { user(by: { id: $id }) { id name email description avatarUrl githubUrl linkedinUrl projects(last: $last) { edges { node { id title image } } } } } `; export const deleteProjectMutation = ` mutation DeleteProject($id: ID!) { projectDelete(by: { id: $id }) { deletedId } } `; export const updateProjectMutation = ` mutation UpdateProject($id: ID!, $input: ProjectUpdateInput!) { projectUpdate(by: { id: $id }, input: $input) { project { id title description createdBy { email name } } } } `;
That's a wrap for this blog post, hope I was able to cover at least a few things regarding the above topic. I tried my best to put into words what I wanted to convey, if the readers can even take away a little bit of knowledge from this blog, I would be more than happy. Thank You!!
Thanks for taking out your time and reading. 😄



