When it comes to building and choosing frameworks for your next full-stack application, combining Next.js with Supabase is one of the best options to work with in my opinion.
Supabase is an open source Firebase alternative with a lot of powerful tools, including seamless authentication. As a developer, this is key to building a successful full-stack application.
Alongside authentication, Supabase comes with other features, such as a Postgres database, real-time subscriptions, and object storage. I believe that Supabase is one of the easiest backend-as-a-services to get started or integrate with.
In this article, we will learn how to build a full-stack app using Next.js and Supabase. We’ll talk about how to set up a Supabase project, configure the UI, and implement authentication and functionalities.
The concept of this app is for users to track and create workout activities based on specified parameters, edit these activities if there are any mistakes or necessary changes, and delete them if needed. Let’s get started!
Introduction to Next.js and Supabase
Next.js is one of the easiest and most popular ways to build production-ready React applications. Over recent years, Next.js has experienced significant exponential growth and many companies have adopted it to build their applications.
Why should we use Supabase?
Supabase is a serverless, open-source alternative to Firebase built on top of the PostgreSQL database. It provides all the backend services needed to create a full-stack application.
As a user, you can manage your database from the Supabase interface, ranging from creating tables and relationships to writing your SQL queries and real-time engine on top of PostgreSQL.
Supabase comes with really cool features that make your full-stack application development even easier. Some of these features are:
- Row-level security (RLS) – Supabase comes with the PostgreSQL RLS feature that allows you to restrict rows in your database tables. When you create policies, you create them directly with SQL
- Real-time database – Supabase has an update feature on the PostgreSQL database that can be used to listen to real-time changes
- Supabase UI – Supabase has an open-source user interface component library to create applications quickly and efficiently
- User authentication – Supabase creates an
auth.userstable as soon as you create your database. When you create an application, Supabase will also assign a user and ID as soon as you register on the app that can be referenced within the database. For log in methods, there are different ways you can authenticate users such as email, password, magic links, Google, GitHub, and more
- Edge functions – Edge functions are TypeScript functions distributed globally at the edge, close to users. They can be used to perform functions such as integrating with third parties or listening for WebHooks
Initiating our project with Next.js
To initiate our project in the terminal with the Next.js template, we will run the following command:
npx create-next-app nextjs-supabase
nextjs-supabase is our app’s folder name where we’ll encompass the Next.js app template.
We’ll need to install the Supabase client package to connect to our Next.js app later. We can do so by running either of the following commands:
yarn add @supabase/supabase-js
npm i @supabase/supabase-js
Once the app has finished setting up, open the folder in your favorite code editor. Now, we can remove the basic template in our
/pages/index.js file and replace it with an
h1 heading saying “Welcome to Workout App.”
After that’s done, run the command
yarn dev in the terminal to start up your app at http://localhost:3000. You should see a page like this:
Setting up a Supabase project and creating a database table
To set up a Supabase project, visit app.supabase.com to sign in to the app dashboard using your GitHub account.
More great articles from LogRocket:
Once you log in, you can create your organization and set up a new project within it by clicking All Projects.
Click on New Project and give your project a name and database password. Click the Create a new project button; it will take a couple of minutes for your project to be up and running.
Once the project has been created, you should see a dashboard like this:
For this tutorial, I already created a project named
Now, let’s create our database table by clicking on the SQL Editor icon on our dashboard and clicking New Query. Enter the SQL query below in the editor and click RUN to execute the query.
CREATE TABLE workouts ( id bigint generated by default as identity primary key, user_id uuid references auth.users not null, user_email text, title text, loads text, reps text, inserted_at timestamp with time zone default timezone('utc'::text, now()) not null ); alter table workouts enable row level security; create policy "Individuals can create workouts." on workouts for insert with check (auth.uid() = user_id); create policy "Individuals can update their own workouts." on workouts for update using (auth.uid() = user_id); create policy "Individuals can delete their own workouts." on workouts for delete using (auth.uid() = user_id); create policy "Workouts are public." on workouts for select using (true);
This will create the workout table we’ll use to build our CRUD application.
Alongside creating a table, row-level permissions will be enabled to ensure that only authorized users can create, update, or delete the details of their workouts.
To check out how the workout table looks, we can click the Table Editor icon on the dashboard to see the workout table we just created.
For this application, we will have seven columns:
Once our table and columns are set, the next step is to connect our Supabase database with our Next.js frontend application!
Connecting Next.js with a Supabase database
To connect Supabase with our Next.js app, we will need our Project URL and Anon Key. Both of these can be found on our database dashboard. To get these two keys, click on the gear icon to go to Settings and then click API. You’ll see these two keys show up like this:
Of course, we don’t want to expose these values publicly on the browser or our repository since it’s sensitive information. To our advantage, Next.js provides inbuilt support for environment variables that allow us to create a
.env.local file in the root of our project. This will load our environment variables and expose them to the browser by prefixing it with
Now, let’s create a
.env.local file in the root of our project and include our URL and keys in the file.
.env.local NEXT_PUBLIC_SUPABASE_URL= // paste your project url here NEXT_PUBLIC_SUPABASE_ANON_KEY= // paste your supabase anon key here
N.B., Don’t forget to include
gitignorefile to prevent it from being pushed to the GitHub repo (and available for everyone to see) when deploying.
Now let’s create our Supabase client file by creating a file called
supabase.js at the root of our project. Inside the
supabase.js file, we will write the following code:
// supabase.js import createClient from "@supabase/supabase-js"; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL; const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY; export const supabase = createClient(supabaseUrl, supabaseKey);
Here, we are importing a
createClient function from Supabase and creating a variable called
supabase. We call the
createClient function and then pass in our parameters: URL (
supabaseUrl) and Anon Key (
Now, we can call and use the Supabase client anywhere in our project!
Configuring our app’s UI
First, we need to configure our app to look the way we want it to. We’ll have a navigation bar with the project name, and Login and Signup options when the app is first loaded. When a user signs up and logs in, we will display the navbar to have Home, Logout, and Create Workout buttons.
There will also be a footer on every page on the website.
To do this, we will create a
component folder that’ll house the
Footer.js files. Then, inside
_app.js, we will wrap our
pages component with the
Navbar and the
Footer components so they are displayed on every page of the app.
// _app.js import Footer from "../components/Footer"; import Navbar from "../components/Navbar"; import "../styles/globals.css"; function MyApp( Component, pageProps ) return ( <div> <Navbar/> <Component ...pageProps/> <Footer /> </div> ); export default MyApp;
I created a GitHub gist here to see what these two components look like alongside the styles I used.
Now, our homepage should look like this:
Implementing user authentication
To implement user authentication, we will initialize the user state in our
_app.js file and create a
validateUser function to check and validate a user. We’ll then set the user state to the session object that is returned.
// _app.js import useState, useEffect from "react"; import Footer from "../components/Footer"; import Navbar from "../components/Navbar"; import "../styles/globals.css"; import supabase from "../utils/supabase"; function MyApp( Component, pageProps ) const [session, setSession] = useState(null); useEffect(() => setSession(supabase.auth.session()); supabase.auth.onAuthStateChange((_event, session) => setSession(session); ); , ); return ( <div> <Navbar session=session /> <Component ...pageProps session=session /> <Footer /> </div> ); export default MyApp;
When a user loads the homepage of our app, we want to display a button to tell them to either log in or sign up. When the Login button is clicked, it should redirect the user to a page where the user can enter their email and password. If they are an existing user and the login details are valid, they will be redirected to the home page.
If the user has invalid credentials, an alert message will display to tell the user about the issue. They’ll be shown a sign up option instead.
When the user signs up, a confirmation email will be sent to the email they entered. they’ll need to confirm their email by clicking on the link in the body of the email.
Now, when we click the Login button, we should be redirected to the user page to this page:
Now, we can click on the Sign up button and enter an email.
Once we click this, an email will be sent to confirm the email address. Upon confirming, it will log us in and we should see a page like this:
Notice that if we have not signed in, we are unable to see our activity dashboard, see a button to create a new workout, or log out. This was the authentication mentioned initially that’s provided to us by Supabase!
Implementing workout functionalities
Now, we’ll dive into creating a user’s ability to create, modify, and delete their workouts.
Fetching all workouts
We’ll need to fetch all the workouts we’ll be creating and render them on the homepage. We will do this inside the
// /pages/index.js import Head from "next/head"; import Link from "next/link"; import useEffect, useState from "react"; import styles from "../styles/Home.module.css"; import supabase from "../utils/supabase"; import WorkoutCard from "../components/WorkoutCard"; export default function Home( session ) const [workouts, setWorkouts] = useState(); const [loading, setLoading] = useState(true); useEffect(() => fetchWorkouts(); , ); const fetchWorkouts = async () => const user = supabase.auth.user(); try setLoading(true); const data, error = await supabase .from("workouts") .select("*") .eq("user_id", user?.id); if (error) throw error; setWorkouts(data); catch (error) alert(error.message); finally setLoading(false); ; if (loading) return <div className=styles.loading>Fetching Workouts...</div>; return ( <div className=styles.container> <Head> <title>Nextjs x Supabase</title> <meta name="description" content="Generated by create next app" /> <link rel="icon" href="https://blog.logrocket.com/favicon.ico" /> </Head> <div className=styles.home> !session?.user ? ( <div> <p> Welcome to Adrenargy. Kindly log in to your account or sign in for a demo </p> </div> ) : ( <div> <p className=styles.workoutHeading> Hello <span className=styles.email>session.user.email</span>, Welcome to your dashboard </p> workouts?.length === 0 ? ( <div className=styles.noWorkout> <p>You have no workouts yet</p> <Link href="http://blog.logrocket.com/create"> <button className=styles.button> " " Create a New Workout </button> </Link> </div> ) : ( <div> <p className=styles.workoutHeading>Here are your workouts</p> <WorkoutCard data=workouts/> </div> ) </div> ) </div> </div> );
In this component, we are destructuring the
session object we passed from the
page props in the
_app.js file and using that to validate authorized users. If there are no users, the dashboard will not be displayed. If there is a user logged in, the dashboard of workouts will appear. And if there are no workouts created, a text saying “You have no workout yet” and a button to create a new one will appear.
To render our created workouts, we have two states:
workouts, an empty array, and a
loading state that takes in a boolean value of
true. We are using
useEffect to fetch the workouts data from the database when the page is loaded.
fetchWorkouts function is used to call the Supabase instance to return all the data from the workout tables in our database using the
select method. The .
eq() filter method is used to filter out and return only the data with the user id matching the current logged in user. Then,
setWorkouts is set to the data sent from the database, and
setLoading is set back to
false once we fetch our data.
If the data is still being fetched, the page should display “Fetching Workouts…” and if the request made to our database returns the array of our workouts, we want to map through the array and render the
WorkoutCard component, we are rendering the workout title, load, reps, and the date and time it was created. The time created is being formatted using the
date-fns library that you can check out here. We will see how our cards look when we start creating them in the next section.
// Workoutcard.js import Link from "next/link"; import styles from "../styles/WorkoutCard.module.css"; import BsTrash from "react-icons/bs"; import FiEdit from "react-icons/fi"; import formatDistanceToNow from "date-fns/"; const WorkoutCard = ( data ) => return ( <div className=styles.workoutContainer> data?.map((item) => ( <div key=item.id className=styles.container> <p className=styles.title> " " Title: "" item.title </p> <p className=styles.load> " " Load(kg): " " item.loads </p> <p className=styles.reps>Reps:item.reps</p> <p className=styles.time> created:" " formatDistanceToNow(new Date(item.inserted_at), addSuffix: true, ) </p> </div> )) </div> ); ; export default WorkoutCard;
Creating a new workout
Now that we’ve logged in, our dashboard is fresh and clean. To implement the ability to create a new workout, we will add
Create.module.css files in the
styles folder respectively, and implement some logic and styling.
// /pages/create.js import supabase from "../utils/supabase"; import useState from "react"; import styles from "../styles/Create.module.css"; import useRouter from "next/router"; const Create = () => const initialState = title: "", loads: "", reps: "", ; const router = useRouter(); const [workoutData, setWorkoutData] = useState(initialState); const title, loads, reps = workoutData; const handleChange = (e) => setWorkoutData( ...workoutData, [e.target.name]: e.target.value ); ; const createWorkout = async () => try const user = supabase.auth.user(); const data, error = await supabase .from("workouts") .insert([ title, loads, reps, user_id: user?.id, , ]) .single(); if (error) throw error; alert("Workout created successfully"); setWorkoutData(initialState); router.push("https://blog.logrocket.com/"); catch (error) alert(error.message); ; return ( <> <div className=styles.container> <div className=styles.form> <p className=styles.title>Create a New Workout</p> <label className=styles.label>Title:</label> <input type="text" name="title" value=title onChange=handleChange className=styles.input placeholder="Enter a title" /> <label className=styles.label>Load (kg):</label> <input type="text" name="loads" value=loads onChange=handleChange className=styles.input placeholder="Enter weight load" /> <label className=styles.label>Reps:</label> <input type="text" name="reps" value=reps onChange=handleChange className=styles.input placeholder="Enter number of reps" /> <button className=styles.button onClick=createWorkout> Create Workout </button> </div> </div> </> ); ; export default Create;
Here, the basic UI scope is that we will have a form to create a new workout. The form will consist of three fields (title, load, and reps) as we specified when creating our database.
An initial state object is defined to handle all these fields that were passed to the
workoutsData state. The
onChange function is used to handle the input field changes.
createWorkout function uses the Supabase client instance to create a new workout using the initial state fields we defined and insert it into the database table.
Finally, we have an alert toast that informs us when our new workout has been created.
Then, we set the form data back to the initial empty string state once our workout has been created. After that, we’re using the
router.push method to navigate the user back to the homepage.
Updating a workout
To update a workout, we will create a folder called
edit within our
pages folder that’ll hold our
[id].js file. We’ll create an edit link icon on our workout component card that links to this page. When the cards are rendered on the homepage, we can click on this edit icon and it will take us to the edit page of that particular card.
We will then fetch the details of the needed workout card to be updated from our workouts table by its
id and the authorized owner of the card. Then, we’ll create a
updateWorkout function to update our workout card details:
// /pages/edit/[id].js import useRouter from "next/router"; import useEffect, useState from "react"; import styles from "../../styles/Edit.module.css"; import supabase from "../../utils/supabase"; const Edit = () => const [workout, setWorkout] = useState(""); const router = useRouter(); const id = router.query; useEffect(() => const user = supabase.auth.user(); const getWorkout = async () => const data = await supabase .from("workouts") .select("*") .eq("user_id", user?.id) .filter("id", "eq", id) .single(); setWorkout(data); ; getWorkout(); , [id]); const handleOnChange = (e) => setWorkout( ...workout, [e.target.name]: e.target.value, ); ; const title, loads, reps = workout; const updateWorkout = async () => const user = supabase.auth.user(); const data = await supabase .from("workouts") .update( title, loads, reps, ) .eq("id", id) .eq("user_id", user?.id); alert("Workout updated successfully"); router.push("https://blog.logrocket.com/"); ; return ( <div className=styles.container> <div className=styles.formContainer> <h1 className=styles.title>Edit Workout</h1> <label className=styles.label> Title:</label> <input type="text" name="title" value=workout.title onChange=handleOnChange className=styles.updateInput /> <label className=styles.label> Load (kg):</label> <input type="text" name="loads" value=workout.loads onChange=handleOnChange className=styles.updateInput /> <label className=styles.label> Reps:</label> <input type="text" name="reps" value=workout.reps onChange=handleOnChange className=styles.updateInput /> <button onClick=updateWorkout className=styles.updateButton> Update Workout </button> </div> </div> ); ; export default Edit;
First, we create a state to store the workout card details that’ll be fetched from our table. Then, we extract the
id of that card using the
useRouter hook. The
getWorkout function calls the Supabase client instance to filter the
id of that workout card and returns the data (title, loads, and reps).
Once the workout card details have been returned, we can create our
updateWorkout function to modify the details using the
.update()function. Once the workout has been updated by the user and the Update workout button is clicked, an alert message is sent and the user will be redirected back to the homepage.
Let’s see how it works.
Click on the edit icon to go to the edit page. We’ll be renaming the title from “Dumbell Press” to “Arm Curl”:
Deleting a workout
To delete a workout on each card, we will create the
handleDelete function that’ll take in the
id as an argument. We’ll call the Supabase instance to delete a workout card using the
.eq('id', id) specifies the
id of the row to be deleted on the table.
const handleDelete = async (id) => try const user = supabase.auth.user(); const data, error = await supabase .from("workouts") .delete() .eq("id", id) .eq("user_id", user?.id); fetchWorkouts(); alert("Workout deleted successfully"); catch (error) alert(error.message); ;
eq('user_id', user?.id) is used to check if the card that is being deleted belongs to that particular user. The function will be passed to the
WorkoutCard component in the
index.js file and destructured for usage in the component itself as follows:
const WorkoutCard = ( data, handleDelete ) => return ( <div className=styles.workoutContainer> data?.map((item) => ( <div key=item.id className=styles.container> <p className=styles.title> " " Title: "" item.title </p> <p className=styles.load> " " Load(kg): " " item.loads </p> <p className=styles.reps>Reps:item.reps</p> <p className=styles.time> created:" " formatDistanceToNow(new Date(item.inserted_at), addSuffix: true, ) </p> <div className=styles.buttons> <Link href=`/edit/$item.id`> <a className=styles.edit> <FiEdit /> </a> </Link> <button onClick=() => handleDelete(item.id) className=styles.delete > <BsTrash /> </button> </div> </div> )) </div> ); ;
An alert toast will be displayed once the card has been deleted successfully and the user will be redirected to the homepage.
Deploying to Vercel
Now, we have to deploy our application to Vercel so anybody on the Internet can use it!
To deploy to Vercel, you must first push your code to your repository, log in to your Vercel dashboard, click on Create New Project, and click the repository to which you just pushed your code.
Enter the environment variables we created earlier alongside their values (
NEXT_PUBLIC_SUPABASE_ANON_KEY) in the Environment Variable field and click Deploy to deploy your app to production.
And there we have it!
Thank you for reading! I hope this tutorial gives you the required knowledge needed to create a full-stack application using Next.js and Supabase.
You can customize the styling to your use case, as this tutorial majorly focuses on the logic of creating a full-stack application.
You can find the full repository of this project here and the live Vercel link here. If you’d like to read more on Supabase and Next.js, you can check out their docs.
LogRocket: Full visibility into production Next.js apps
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your Next.js apps — start monitoring for free.