NextJS: a React-based framework that lets us render data on the server-side. It helps Google crawl the application which results in SEO benefits.
Supabase: provides authentication, database, and storage that we are going to use in our application.
Chakra UI: is a simple, modular, and accessible component library that will provide us with the building blocks to build the application.
Vercel: will host our application. It scales well, all without any configuration, and deployment is instant.
Why use Supabase?
According to the Supabase documentation, Supabase is "an open source Firebase alternative".
However, Supabase is not completely similar to Firebase. The difference between the two is that Supabase uses Postgres (a relational database) for storing data whereas Firebase uses the NoSQL mechanism to store data.
Personally, I typically use Postgres as my primary database and I have found it to scale well.
Supabase provides the following services:
Real time rest server
Row level security
Now let's understand how it works:
Alright, what's going on here?
As you already know, Supabase uses Postgres as a database – but it also has many other components which provide different services.
One of my favorites is Realtime. Supabase uses the Elixir server to set up a web socket connection to listen for insert, update, and delete the events.
PostgRest directly converts the Postgres database into a Rest API.
GoTrue is an API for managing users and issuing SWT tokens.
Postgres-Meta is a Restful API for managing the Postgres database.
Kong is an API gateway.
Note: All these definitions are taken from the supabase documentation. To read more about how Supabase works, you can visit their documentation.
And with that, we're ready to dive into our project. Here's what we'll cover:
I am going to divide this tutorial into four separate sections. At the start of every section, you will find a Git commit that has the code developed in that section. Also If you want to see the complete code, then it is available in this repository.
How to Configure Supabase Tables, Auth, and Storage
In this section, we'll implement the following functionality:
Create a Supabase project.
Setup authentication for users and policies.
Configure database and policies for users and todos.
To create a Supabase project, visit the following link. Click on the "Start your project" button and login via GitHub (at the time of writing this article, they only support GitHub as an auth provider).
Once you've created your account, click on New project where it will ask for the organization. By default, Supabase will create an organizational account for you with your username. I'll be using the default but you can create your own for this project.
Once the organization is selected, Supabase will ask for the project name, database password, and region.
Fill out this form and click on the Create new project button.
Supabase will start setting up the application. It may take a few minutes to set up.
Under the Project API keys section, you'll see two types of keys:
anon is a public API key and it can be used on the client-side.
service_role is a private API key and you use it on the server-side only. This key can bypass the row-level security and mutate the data.
What is Row Level Security in Supabase?
You might be wondering – what is row-level security and why it is so important?
Well, Supabase has a client library to access data directly from the browser and for that, we use the anon key. As the anon key is on the client, anyone can have access to the key via the network tab.
But there are some cases where we don't want the data to be directly accessed by the browser using the client library.
In these cases, we can set up row-level security, which specifies which data can be accessed using the anon key.
Returning back to the application, once the project is configured you'll get the following message "Welcome to your new project."
How to Create Tables in the Database
Now let's create a script to create tables in our database.
Go to the SQL section from the sidebar and click on New query.
Click on New Query and copy-paste the following code:
Let's understand this Profiles script piece by piece.
First, we create table profiles that relate to users in our TodoApp. To understand how to set up unique in a table, we have set up username as a unique constraint and primary key as id.
After that, we set up the row-level security and assign policies so that each individual can access only their data.
After that, we enable real-time for our database. Realtime gives an event whenever there are any changes in the row, and we can update the UI accordingly.
Now click on the RUN button at the right corner and you'll get the following message:
Success. No rows returned
Now let's create our todos table. For generating the table, click on the New query button and copy-paste the following script:
Now click on the RUN button at the right corner, and you'll get the following message:
Success. No rows returned
To confirm our tables are generated, go to the table editor section from the sidebar.
Inside the table editor, you'll find our tables generated successfully.
As you can see in the above Todos Script, we didn't enable real-time. To enable a real-time server, we need to go to the Database > Replication section.
Here you'll see the following view:
Click on the 1 table button under source and then toggle the switch for todos. It will enable a real-time server for our todos also.
Now suppose we want to disable row-level security for todos (note that this is not advisable), but just for the scope of the article to understand how to, we will disable it.
Go to the Authentication section and, inside that, go to the Policies.
Now you'll see the todos section with RLS enabled written in the green box. Click on the Disable RLS option at the top right-hand corner of this box. It will disable the row-level security for our application.
How to Implement Sign In Using Supabase
The code for this section is available under this commit if you need to refer to it in the future for reference.
First, let's create our application using the following command:
npx create-next-app todo_app
It's time to install our dependencies and have a basic configuration in place.
How to Install Chakra UI
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^4
Note: If you use zsh you'll need to add the escape character (\) after @ as follows:
npm i @chakra-ui/react @emotion/react@\^11 @emotion/styled@\^11 framer-motion@\^4
Now let's clean up our code by removing the code that is not required and configuring ChakraUI into our application.
As per Chakra's documentation, we need to wrap <Component /> with ChakraProvider in the pages/_app.js. Go to the _app.js and copy-paste the following code:
Let's create a directory under our root directory and name it lib. Under this directory, create a file named theme.js.
Copy-paste the following code inside this file:
Now under the pages directory, create a _document.js file and copy-paste the following code:
By creating _document.js and theme.js we have just set our color to be light by default.
From ChakraUI version 1.6.12, it sets the system chosen color by default. So for some users who have dark mode enabled for the browser, the application will have a dark color theme. Dark mode is nice, but for starting purposes, we only want the color to be light.
Go to index.js and copy paste the following code:
How to Install the Supabase Client Library
npm i @supabase/supabase-js
Under lib directory create a file named client.js.
Under that file copy paste the following code:
Here we are just creating a Supabase Client which will be used throughout the project.
Now under the root directory create a .env.local file and copy-paste the following part with Supabase URL and anon key:
You can find the Supabase URL and anon key under the Settings > API section.
Under Project API keys is the anon key and under Config is the URL.
With this, our Supabase client is configured and ready to use.
Let's run our application using the following command:
npm run dev
You'll get the following output:
Now under the pages directory, create a file named signin.js and copy-paste the following code:
Here we have created a form and used a supabase auth method to sign in the user.
Note: In supabaseClient.auth.signIn method, when you don't pass a password, it considers the authentication method as the magic link.
Now go to the _app.js and copy paste the following code:
Now inside the API directory, remove the hello.js file and create a new file called auth.js. Copy-paste the following code in that new file:
The code under _app.js is crucial for authentication when the user clicks on the magic link.
Supabase provides a listener method under the hood auth.onAuthStateChange which gives two events SIGNED_IN and SIGNED_OUT.
We use SIGNED_IN event to set a cookie by calling /api/auth which uses another method exposed by supabase. This method auth.api.setAuthCookie is useful to set cookies via server-side. Once the user is authenticated we push the user to the / page where all todos will be.
Now let's restart our server using npm run dev and then go to http://localhost:3000/signin. You'll see the following UI:
Add your email and click the submit button. Go to the email and click on verify, and you'll be redirected to the / page.
How to Show All Todos, Add New Todos, and Update and Delete Todos
The code is available under this commit if you need to refer to it in the future for reference.
Before implementing Todo crud operations, let's implement the logout functionality. Go to index.js and replace the existing code with the following code:
Create a component directory under the root directory, and inside the component directory create a file named Navbar.js. Copy-paste the following content under that file:
We have created a navbar component with a Profile link, Add Todo button, and a Logout button.
The logoutHandler uses a Supabase method called signOut to clear the session and log us out of the application.
Go to http://localhost:3000 and click on the Logout button.
The cookie will be cleared from the browser, and the user will get redirected to the sign-in page.
How to Add a Todo
Go to the Navbar.js and copy paste the following code:
Here we have just assigned an onClick handler to our Add Todo button which will open a modal to add a todo.
Now create a file named ManageTodo.js under the components directory and copy paste the following code:
This part will be responsible for adding and updating the todos. Here we have created a modal with a form and 3 form control elements.
Once the form is submited we call a supabase server with following code:
This useEffect is a real time subscription with the Supabase real time server. Whenever a new todo is added we get the payload event which we use to add the todo in our local state.
Note: theSupabase docs suggest not using real time subscription on a server-side application.
Now go to http://localhost:3000 and add a todo. You'll see the following view:
How to Update a Todo
Updating the todo mechanism might be complex for beginners. So I'll explain the process as simply as I can:
We create a todo state in our parent component of index.js. This todo state is updated when user clicks on SingleTodo.
We pass an openHandler function for doing that. This function updates the todo state with the clicked todo detail and opens the modal.
In ManageTodo.js we have written a useEffect with a dependency of todo which updates the values of title, description, and isComplete whenever the todo changes.
At last we update the todo in our table using the Supbase update method on the basis of todo id.
Time to implement the code. Under the components directory, go to SingleTodo.js and replace the code with the following:
Under the components directory go to ManageTodo.js and replace the code with the following code:
Let's understand the code above. Here we are checking if the user has clicked on the update button ( By checking if todo exists ) and then showing the data in the initial object.
Based upon the condition, showing update text instead of Save text on the button. Also, based upon condition, we execute supabase update if todo exists and if not then insert.
Go to pages > index.js and replace the existing code with the following code:
Here we add the ManageTodo component that we created and pass props that are used by this component.
Now go to http://localhost:3000 and click on any todo to update it and you'll see the following view:
How to Delete a Todo
This functionality will need us to update our some of existing code. First we will do that and then understand how it works and why changes are required.
Go to the SingleTodo.js inside the components directory and replace the existing code with the following code:
Here we have added a delete button with an onClick event. Now this delete event is under another event which opens the modal. So whenever we click on delete it will open the modal too.
We don't want this behavour, so we use a method from event called stopPropagation. This method doesn't allow events from children to be passed to the parent.
Now go to the index.js inside the pages directory and replace the existing code with the following code:
Let's understand the deleteHandler method first. In this method we use the Supabase client to delete a record from the todos table. Once it's successfully deleted, we use the filter method to remove the todo from our local state.
For the useEffect which has the todoListener we add an if condition based on an event type. We don't want to do anything on the DELETE event as we are updating the local state in deleteHandler.
Go to http://localhost:3000 and you'll see the following view:
Click the Delete button and you'll see that the todo is gone from our todos view.
With this we have completed our TODO CRUD operation flow.
How to Update Profile Details and Avatars
The Profile Update Operationcode is available under this commit if you need to refer to it in the future for reference.
Before working on the profile section we have to make our TodoApp Heading as a route so we can go back to the home page from the profile page.
In Navbar.js under the components directory replace the existing code with the following code:
Let's start working on building the last part of our app which is the profile section. This section will have a form which can update the username, website, bio and an avatar.
To store our pictures we will be using Supabase storage. By default these storage buckets are private and can be accessed using a token. But for the sake of this article we will make the bucket public. But if you are storing any sensitive information make sure to keep that bucket private.
Click on the three dots and select the Make public option.
Back to our code: inside the pages directory create a file named profile.js and copy paste the following code:
Here we have 4 FormControl elements, and each is pre-filled if a value exists. This is possible because on render useEffect runs which uses the Supabase client to fetch the user record from the auth and profiles tables.
Note: the auth table is maintained by Supabase and can be accessed via client using following command:
Except images, other records can be updated using the updateHandler function. This function updates the user record using id.
The uploadHandler function is responsible for uploading the image to the storage bucket and setting the avatarurl in the profiles table for a record based on id.
The upload method from Supabase uploads the image while the getPublicUrl method gives us a public URL of the image. We use the from('profiles').update method to update the record.
Visit http://localhost:3000 and click on profile link. You'll see the following view:
Now you can use the update method to update your username, website URL, and bio.
With this our TodoApp is completed and ready for production.
How to Deploy the App to Vercel and Configure Supabase Authentication
Before deploying the application on Vercel we need to run the npm run build command and check the terminal output to see if we have any errors.
There are two ways to configure an application on Vercel: