Build a full-stack app with Next.js and Supabase
Tags: nextjs, webdev
Published: 2022-08-25 13:43:45

**Written by [Taofiq Aiyelabegan](https://blog.logrocket.com/author/taofiqaiyelabegan/)✏️**

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](#introduction-next-js-supabase)
*   [Why should we use Supabase?](#why-use-supabase)
*   [Initiating our project with Next.js](#initiating-project-next-js)
*   [Setting up a Supabase project and creating a database table](#setting-up-supabase-project-creating-database-table)
*   [Connecting Next.js with a Supabase database](#connecting-next-js-supabase-database)
*   [Configuring our app’s UI](#configuring-app-ui)
*   [Implementing user authentication](#implementing-user-authentication)
*   [Implementing workout functionalities](#implementing-workout-functionalities)
    *   [Fetching all workouts](#fetching-all-workouts)
    *   [Creating a new workout](#creating-new-workout)
    *   [Updating a workout](#updating-workout)
    *   [Deleting a workout](#deleting-workout)
*   [Deploying to Vercel](#deploying-vercel)

## Introduction to Next.js and Supabase <a name="introduction-next-js-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? <a name="why-use-supabase">

[Supabase is a serverless, open-source alternative to Firebase](https://blog.logrocket.com/exploring-supabase-the-open-source-firebase-alternative/) 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.users` table 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 <a name="initiating-project-next-js">

To initiate our project in the terminal with the Next.js template, we will run the following command:

```bash
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:

```bash
yarn add @supabase/supabase-js
```

or

```bash
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](http://localhost:3000). You should see a page like this:

![Welcome Screen](https://blog.logrocket.com/wp-content/uploads/2022/08/welcome-screen.png)

## Setting up a Supabase project and creating a database table <a name="setting-up-supabase-project-creating-database-table">

To set up a Supabase project, visit [app.supabase.com](https://app.supabase.com/) to sign in to the app dashboard using your GitHub account.

![Supabase Dashboard](https://blog.logrocket.com/wp-content/uploads/2022/08/supabase-dashboard.png)

Once you log in, you can create your organization and set up a new project within it by clicking **All Projects**.

![All Projects And Organization Screen](https://blog.logrocket.com/wp-content/uploads/2022/08/all-projects-organization-screen.png)

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.

![Create A New Project](https://blog.logrocket.com/wp-content/uploads/2022/08/create-new-project.png)

Once the project has been created, you should see a dashboard like this:

![Workout Next Supabase Dashboard](https://blog.logrocket.com/wp-content/uploads/2022/08/workout-next-supabase-dashboard.png)

For this tutorial, I already created a project named `workout-next-supabase.` 

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.

```sql
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);
```

![Execute Your Query](https://blog.logrocket.com/wp-content/uploads/2022/08/execute-query.png)

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:

*   `user_id`
*   `user_email`
*   `id`
*   `title`
*   `loads`
*   `reps`
*   `Date stamp`

![Table Editor](https://blog.logrocket.com/wp-content/uploads/2022/08/table-editor.png)

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 <a name="connecting-next-js-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:

![Url Api Setup](https://blog.logrocket.com/wp-content/uploads/2022/08/url-api-setup.png)

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 `NEXT_PUBLIC`. 

Now, let’s create a `.env.local` file in the root of our project and include our URL and keys in the file.

```plaintext
.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 `.env.local` in your `gitignore` file 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:

```javascript
// 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 (`supabaseKey`). 

Now, we can call and use the Supabase client anywhere in our project!

## Configuring our app’s UI <a name="configuring-app-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 `Navbar.js` and `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.

```javascript
// _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](https://gist.github.com/Taofiqq/5fe986309bf2bdbf7eb7edd4249960cd) to see what these two components look like alongside the styles I used. 

Now, our homepage should look like this:

![Adrenargy Home Screen](https://blog.logrocket.com/wp-content/uploads/2022/08/andrenargy-home-screen.png)

## Implementing user authentication <a name="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.

```javascript
// _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.

![Confirm Signup Email](https://blog.logrocket.com/wp-content/uploads/2022/08/confirm-signup-email.png)

Now, when we click the **Login** button, we should be redirected to the user page to this page:

![Login Page](https://blog.logrocket.com/wp-content/uploads/2022/08/login-page.png)

Now, we can click on the **Sign up** button and enter an email.

![Sign Up Page](https://blog.logrocket.com/wp-content/uploads/2022/08/sign-up-page.png)

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:

![Welcome Screen With No Workouts Yet](https://blog.logrocket.com/wp-content/uploads/2022/08/welcome-screen-no-workouts-yet.png)

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 <a name="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 `index.js` file:

```javascript
// /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="/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="/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. 

The `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. 

In 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](https://date-fns.org/). We will see how our cards look when we start creating them in the next section.

```javascript
// 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 <a name="creating-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.js` and `Create.module.css` files in the `pages` and `styles` folder respectively, and implement some logic and styling.

```javascript
// /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("/");
   } 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. 

The `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.

![Create New Project](https://blog.logrocket.com/wp-content/uploads/2022/08/create-new-workout.png)

![Workout Successfully Created](https://blog.logrocket.com/wp-content/uploads/2022/08/workout-successfully-created.png)

![Dashboard With Workouts Dumbell Press](https://blog.logrocket.com/wp-content/uploads/2022/08/dashboard-workouts-dumbell-press.png)

### Updating a workout <a name="updating-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:

```javascript
// /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("/");
 };
 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”:

![Edit Workout Dumbell Press](https://blog.logrocket.com/wp-content/uploads/2022/08/edit-workout-dumbell-press.png)

![Edit Workout Successful](https://blog.logrocket.com/wp-content/uploads/2022/08/edit-workout-successful.png)

![Edit Workout With Arm Curl](https://blog.logrocket.com/wp-content/uploads/2022/08/edit-workout-arm-curl.png)

### Deleting a workout <a name="deleting-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 `.delete()`function. This `.eq('id', id)` specifies the `id` of the row to be deleted on the table.

```javascript
 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);
   }
 };
```

The `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:

```javascript
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 <a name="deploying-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_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY`) in the **Environment Variable** field and click **Deploy** to deploy your app to production.

![Deploy To Vercel](https://blog.logrocket.com/wp-content/uploads/2022/08/deploy-vercel.png)

And there we have it!

## Conclusion

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](https://github.com/Taofiqq/nextjs-supabase-app) and the [live Vercel link here](https://nextjs-supabase-app.vercel.app/). If you’d like to read more on [Supabase](https://supabase.com/docs/) and Next.js, you can check out their docs.

---

## [LogRocket](https://logrocket.com/signup): Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try [LogRocket](https://logrocket.com/signup). 

[![LogRocket signup](https://blog.logrocket.com/wp-content/uploads/2017/03/1d0cd-1s_rmyo6nbrasp-xtvbaxfg.png)](https://logrocket.com/signup)

[LogRocket](https://logrocket.com/signup) 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](https://logrocket.com/signup).